diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart index d9924d38bb..fab9c1a724 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/cell_service.dart @@ -8,10 +8,10 @@ import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart index 84a71f7d05..77a7c7f066 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart @@ -5,6 +5,7 @@ import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:table_calendar/table_calendar.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart index 6e98091a90..d42769549a 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart @@ -1,4 +1,4 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart index 3811c90ea5..ed81f697f8 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_bloc.dart @@ -1,4 +1,4 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart index 59435b6a38..e69a150a38 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/url_cell_editor_bloc.dart @@ -1,4 +1,4 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart index 7258854ec6..00deff56e7 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/field/type_option/date_bloc.dart @@ -1,5 +1,6 @@ import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart index f15b73f75c..39408c9ee3 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/header/type_option/date.dart @@ -9,7 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'builder.dart'; diff --git a/frontend/rust-lib/flowy-grid/Flowy.toml b/frontend/rust-lib/flowy-grid/Flowy.toml index 94bd0c0225..00e9cf7c04 100644 --- a/frontend/rust-lib/flowy-grid/Flowy.toml +++ b/frontend/rust-lib/flowy-grid/Flowy.toml @@ -2,7 +2,6 @@ proto_input = [ "src/event_map.rs", "src/services/field/type_options", - "src/services/field/select_option.rs", "src/entities", "src/dart_notification.rs" ] diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs index 73c39cb657..9eb4ff3fe9 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/select_option_filter.rs @@ -1,4 +1,4 @@ -use crate::services::field::select_option::SelectOptionIds; +use crate::services::field::SelectOptionIds; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::revision::GridFilterRevision; diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index a3b1471a4e..52c3c3424c 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -1,10 +1,13 @@ use crate::entities::*; use crate::manager::GridManager; use crate::services::cell::AnyCellData; -use crate::services::field::select_option::*; use crate::services::field::{ - default_type_option_builder_from_type, type_option_builder_from_json_str, DateChangesetParams, DateChangesetPayload, + default_type_option_builder_from_type, select_option_operation, type_option_builder_from_json_str, + DateChangesetParams, DateChangesetPayload, SelectOption, SelectOptionCellChangeset, + SelectOptionCellChangesetParams, SelectOptionCellChangesetPayload, SelectOptionCellData, SelectOptionChangeset, + SelectOptionChangesetPayload, }; +use crate::services::row::make_row_from_row_rev; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::FieldRevision; use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; @@ -229,10 +232,12 @@ pub(crate) async fn get_row_handler( ) -> DataResult { let params: GridRowId = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id)?; - let row = OptionalRow { - row: editor.get_row(¶ms.row_id).await?, - }; - data_result(row) + let row = editor + .get_row_rev(¶ms.row_id) + .await? + .and_then(make_row_from_row_rev); + + data_result(OptionalRow { row }) } #[tracing::instrument(level = "debug", skip(data, manager), err)] @@ -373,7 +378,7 @@ pub(crate) async fn get_select_option_handler( }, Some(cell_rev) => cell_rev.try_into()?, }; - let option_context = type_option.selected_select_option(any_cell_data); + let option_context = type_option.selected_select_option(any_cell_data.into()); data_result(option_context) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs index 259fcc79ef..8ebffcbedc 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/any_cell_data.rs @@ -1,9 +1,11 @@ use crate::entities::FieldType; +use crate::services::cell::{CellData, FromCellString}; use bytes::Bytes; use flowy_error::{internal_error, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::CellRevision; use serde::{Deserialize, Serialize}; use std::str::FromStr; + /// AnyCellData is a generic CellData, you can parse the cell_data according to the field_type. /// When the type of field is changed, it's different from the field_type of AnyCellData. /// So it will return an empty data. You could check the CellDataOperation trait for more information. @@ -46,6 +48,15 @@ impl std::convert::TryFrom for AnyCellData { } } +impl std::convert::From for CellData +where + T: FromCellString, +{ + fn from(any_call_data: AnyCellData) -> Self { + CellData::from(any_call_data.data) + } +} + impl AnyCellData { pub fn new(content: String, field_type: FieldType) -> Self { AnyCellData { @@ -100,36 +111,45 @@ impl AnyCellData { /// * Use String to parse the data when the FieldType is RichText, Number, or Checkbox. /// * Check out the implementation of CellDataOperation trait for more information. #[derive(Default)] -pub struct DecodedCellData { - pub data: Vec, +pub struct CellBytes(pub Bytes); + +pub trait CellBytesParser { + type Object; + fn parse(&self, bytes: &Bytes) -> FlowyResult; } -impl DecodedCellData { +impl CellBytes { pub fn new>(data: T) -> Self { - Self { - data: data.as_ref().to_vec(), - } + let bytes = Bytes::from(data.as_ref().to_vec()); + Self(bytes) } - pub fn try_from_bytes>(bytes: T) -> FlowyResult + pub fn from>(bytes: T) -> FlowyResult where >::Error: std::fmt::Debug, { let bytes = bytes.try_into().map_err(internal_error)?; - Ok(Self { data: bytes.to_vec() }) + Ok(Self(bytes)) } - pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult + pub fn with_parser

(&self, parser: P) -> FlowyResult where - >::Error: std::fmt::Debug, + P: CellBytesParser, { - T::try_from(self.data.as_ref()).map_err(internal_error) + parser.parse(&self.0) } + + // pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult + // where + // >::Error: std::fmt::Debug, + // { + // T::try_from(self.0.as_ref()).map_err(internal_error) + // } } -impl ToString for DecodedCellData { +impl ToString for CellBytes { fn to_string(&self) -> String { - match String::from_utf8(self.data.clone()) { + match String::from_utf8(self.0.to_vec()) { Ok(s) => s, Err(e) => { tracing::error!("DecodedCellData to string failed: {:?}", e); @@ -138,3 +158,11 @@ impl ToString for DecodedCellData { } } } + +impl std::ops::Deref for CellBytes { + type Target = Bytes; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs index 5b74c26aa4..b4729c6925 100644 --- a/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/cell/cell_operation.rs @@ -1,33 +1,51 @@ +use crate::entities::FieldType; +use crate::services::cell::{AnyCellData, CellBytes}; +use crate::services::field::*; + use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, FieldTypeRevision}; -use crate::entities::FieldType; -use crate::services::cell::{AnyCellData, DecodedCellData}; -use crate::services::field::*; - +/// This trait is used when doing filter/search on the grid. pub trait CellFilterOperation { /// Return true if any_cell_data match the filter condition. fn apply_filter(&self, any_cell_data: AnyCellData, filter: &T) -> FlowyResult; } -pub trait CellDataOperation { - /// The cell_data is able to parse into the specific data that was impl the FromCellData trait. +/// Return object that describes the cell. +pub trait CellDisplayable { + fn display_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult; +} + +// CD: Short for CellData. This type is the type return by apply_changeset function. +// CS: Short for Changeset. Parse the string into specific Changeset type. +pub trait CellDataOperation { + /// The cell_data is able to parse into the specific data if CD impl the FromCellData trait. /// For example: /// URLCellData, DateCellData. etc. fn decode_cell_data( &self, - cell_data: CellData, + cell_data: CellData, decoded_field_type: &FieldType, field_rev: &FieldRevision, - ) -> FlowyResult; + ) -> FlowyResult; - /// The changeset is able to parse into the specific data that was impl the FromCellChangeset trait. + /// The changeset is able to parse into the specific data if CS impl the FromCellChangeset trait. /// For example: /// SelectOptionCellChangeset,DateCellChangeset. etc. - fn apply_changeset(&self, changeset: CellDataChangeset, cell_rev: Option) -> FlowyResult; + fn apply_changeset(&self, changeset: CellDataChangeset, cell_rev: Option) -> FlowyResult; } -/// The changeset will be deserialized into specific data base on the FieldType. -/// For example, it's String on FieldType::RichText, and SelectOptionChangeset on FieldType::SingleSelect + +/// changeset: It will be deserialized into specific data base on the FieldType. +/// For example, +/// FieldType::RichText => String +/// FieldType::SingleSelect => SelectOptionChangeset +/// +/// cell_rev: It will be None if the cell does not contain any data. pub fn apply_cell_data_changeset>( changeset: C, cell_rev: Option, @@ -49,23 +67,20 @@ pub fn apply_cell_data_changeset>( Ok(AnyCellData::new(s, field_type).json()) } -pub fn decode_any_cell_data>(data: T, field_rev: &FieldRevision) -> DecodedCellData { +pub fn decode_any_cell_data>(data: T, field_rev: &FieldRevision) -> CellBytes { if let Ok(any_cell_data) = data.try_into() { - let AnyCellData { - data: cell_data, - field_type, - } = any_cell_data; + let AnyCellData { data, field_type } = any_cell_data; let to_field_type = field_rev.field_type_rev.into(); - match try_decode_cell_data(CellData(Some(cell_data)), field_rev, &field_type, &to_field_type) { - Ok(cell_data) => cell_data, + match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) { + Ok(cell_bytes) => cell_bytes, Err(e) => { tracing::error!("Decode cell data failed, {:?}", e); - DecodedCellData::default() + CellBytes::default() } } } else { tracing::error!("Decode type option data failed"); - DecodedCellData::default() + CellBytes::default() } } @@ -74,7 +89,7 @@ pub fn try_decode_cell_data( field_rev: &FieldRevision, s_field_type: &FieldType, t_field_type: &FieldType, -) -> FlowyResult { +) -> FlowyResult { let cell_data = cell_data.try_into_inner()?; let get_cell_data = || { let field_type: FieldTypeRevision = t_field_type.into(); @@ -108,20 +123,22 @@ pub fn try_decode_cell_data( Some(Ok(data)) => Ok(data), Some(Err(err)) => { tracing::error!("{:?}", err); - Ok(DecodedCellData::default()) + Ok(CellBytes::default()) } - None => Ok(DecodedCellData::default()), + None => Ok(CellBytes::default()), } } +/// If the cell data is not String type, it should impl this trait. +/// Deserialize the String into cell specific data type. pub trait FromCellString { fn from_cell_str(s: &str) -> FlowyResult where Self: Sized; } +/// CellData is a helper struct. String will be parser into Option only if the T impl the FromCellString trait. pub struct CellData(pub Option); - impl CellData { pub fn try_into_inner(self) -> FlowyResult { match self.0 { @@ -146,9 +163,9 @@ where } } -impl std::convert::From for CellData { - fn from(s: String) -> Self { - CellData(Some(s)) +impl std::convert::From for CellData { + fn from(val: T) -> Self { + CellData(Some(val)) } } @@ -158,7 +175,8 @@ impl std::convert::From> for String { } } -// CellChangeset +/// If the changeset applying to the cell is not String type, it should impl this trait. +/// Deserialize the string into cell specific changeset. pub trait FromCellChangeset { fn from_changeset(changeset: String) -> FlowyResult where diff --git a/frontend/rust-lib/flowy-grid/src/services/field/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/mod.rs index 8a01d90d1f..c1b689fbf4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/mod.rs @@ -1,5 +1,4 @@ mod field_builder; -pub mod select_option; pub(crate) mod type_options; pub use field_builder::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option.rs deleted file mode 100644 index 1808f0dc82..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option.rs +++ /dev/null @@ -1,133 +0,0 @@ -use crate::entities::FieldType; -use crate::impl_type_option; -use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; -use bytes::Bytes; -use flowy_derive::ProtoBuf; -use flowy_error::{FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; -use serde::{Deserialize, Serialize}; - -#[derive(Default)] -pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption); -impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption); - -impl CheckboxTypeOptionBuilder { - pub fn set_selected(mut self, is_selected: bool) -> Self { - self.0.is_selected = is_selected; - self - } -} - -impl TypeOptionBuilder for CheckboxTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::Checkbox - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] -pub struct CheckboxTypeOption { - #[pb(index = 1)] - pub is_selected: bool, -} -impl_type_option!(CheckboxTypeOption, FieldType::Checkbox); - -const YES: &str = "Yes"; -const NO: &str = "No"; - -impl CellDataOperation for CheckboxTypeOption { - fn decode_cell_data( - &self, - cell_data: CellData, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { - if !decoded_field_type.is_checkbox() { - return Ok(DecodedCellData::default()); - } - - let s: String = cell_data.try_into_inner()?; - if s == YES || s == NO { - return Ok(DecodedCellData::new(s)); - } - - Ok(DecodedCellData::default()) - } - - fn apply_changeset( - &self, - changeset: CellDataChangeset, - _cell_rev: Option, - ) -> Result { - let changeset = changeset.try_into_inner()?; - let s = match string_to_bool(&changeset) { - true => YES, - false => NO, - }; - Ok(s.to_string()) - } -} - -fn string_to_bool(bool_str: &str) -> bool { - let lower_case_str: &str = &bool_str.to_lowercase(); - match lower_case_str { - "1" => true, - "true" => true, - "yes" => true, - "0" => false, - "false" => false, - "no" => false, - _ => false, - } -} - -pub struct CheckboxCellData(pub String); - -impl CheckboxCellData { - pub fn is_check(&self) -> bool { - string_to_bool(&self.0) - } -} -impl std::convert::TryFrom for CheckboxCellData { - type Error = FlowyError; - - fn try_from(_value: AnyCellData) -> Result { - todo!() - } -} - -#[cfg(test)] -mod tests { - use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data}; - use crate::services::field::type_options::checkbox_type_option::{NO, YES}; - use crate::services::field::FieldBuilder; - - use crate::entities::FieldType; - - #[test] - fn checkout_box_description_test() { - let field_rev = FieldBuilder::from_field_type(&FieldType::Checkbox).build(); - let data = apply_cell_data_changeset("true", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev).to_string(), YES); - - let data = apply_cell_data_changeset("1", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); - - let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); - - let data = apply_cell_data_changeset("false", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); - - let data = apply_cell_data_changeset("no", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); - - let data = apply_cell_data_changeset("12", None, &field_rev).unwrap(); - assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs new file mode 100644 index 0000000000..1a60a0a5a8 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_tests.rs @@ -0,0 +1,30 @@ +#[cfg(test)] +mod tests { + use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data}; + use crate::services::field::type_options::checkbox_type_option::{NO, YES}; + use crate::services::field::FieldBuilder; + + use crate::entities::FieldType; + + #[test] + fn checkout_box_description_test() { + let field_rev = FieldBuilder::from_field_type(&FieldType::Checkbox).build(); + let data = apply_cell_data_changeset("true", None, &field_rev).unwrap(); + assert_eq!(decode_any_cell_data(data, &field_rev).to_string(), YES); + + let data = apply_cell_data_changeset("1", None, &field_rev).unwrap(); + assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); + + let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap(); + assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES); + + let data = apply_cell_data_changeset("false", None, &field_rev).unwrap(); + assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); + + let data = apply_cell_data_changeset("no", None, &field_rev).unwrap(); + assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO); + + let data = apply_cell_data_changeset("12", None, &field_rev).unwrap(); + assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), ""); + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs new file mode 100644 index 0000000000..155965f409 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -0,0 +1,76 @@ +use crate::entities::FieldType; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{BoxTypeOptionBuilder, CheckboxCellData, TypeOptionBuilder}; +use bytes::Bytes; +use flowy_derive::ProtoBuf; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Default)] +pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption); +impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption); + +impl CheckboxTypeOptionBuilder { + pub fn set_selected(mut self, is_selected: bool) -> Self { + self.0.is_selected = is_selected; + self + } +} + +impl TypeOptionBuilder for CheckboxTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::Checkbox + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] +pub struct CheckboxTypeOption { + #[pb(index = 1)] + pub is_selected: bool, +} +impl_type_option!(CheckboxTypeOption, FieldType::Checkbox); + +impl CellDisplayable for CheckboxTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_data = cell_data.try_into_inner()?; + Ok(CellBytes::new(cell_data)) + } +} + +impl CellDataOperation for CheckboxTypeOption { + fn decode_cell_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult { + if !decoded_field_type.is_checkbox() { + return Ok(CellBytes::default()); + } + + self.display_data(cell_data, decoded_field_type, field_rev) + } + + fn apply_changeset( + &self, + changeset: CellDataChangeset, + _cell_rev: Option, + ) -> Result { + let changeset = changeset.try_into_inner()?; + let cell_data = CheckboxCellData::from_str(&changeset)?; + Ok(cell_data.to_string()) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs new file mode 100644 index 0000000000..233c481eb6 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option_entities.rs @@ -0,0 +1,69 @@ +use crate::services::cell::{CellBytesParser, FromCellString}; +use bytes::Bytes; +use flowy_error::{FlowyError, FlowyResult}; +use std::str::FromStr; + +pub const YES: &str = "Yes"; +pub const NO: &str = "No"; + +pub struct CheckboxCellData(String); + +impl CheckboxCellData { + pub fn is_check(&self) -> bool { + self.0 == YES + } +} + +impl AsRef<[u8]> for CheckboxCellData { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl FromStr for CheckboxCellData { + type Err = FlowyError; + + fn from_str(s: &str) -> Result { + let lower_case_str: &str = &s.to_lowercase(); + let val = match lower_case_str { + "1" => Some(true), + "true" => Some(true), + "yes" => Some(true), + "0" => Some(false), + "false" => Some(false), + "no" => Some(false), + _ => None, + }; + + match val { + Some(true) => Ok(Self(YES.to_string())), + Some(false) => Ok(Self(NO.to_string())), + None => Ok(Self("".to_string())), + } + } +} + +impl FromCellString for CheckboxCellData { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + Self::from_str(s) + } +} + +impl ToString for CheckboxCellData { + fn to_string(&self) -> String { + self.0.clone() + } +} +pub struct CheckboxCellDataParser(); +impl CellBytesParser for CheckboxCellDataParser { + type Object = CheckboxCellData; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => CheckboxCellData::from_str(&s), + Err(_) => Ok(CheckboxCellData("".to_string())), + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/mod.rs new file mode 100644 index 0000000000..ebe5d1a6a8 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/mod.rs @@ -0,0 +1,7 @@ +#![allow(clippy::module_inception)] +mod checkbox_tests; +mod checkbox_type_option; +mod checkbox_type_option_entities; + +pub use checkbox_type_option::*; +pub use checkbox_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs deleted file mode 100644 index 2200dad63c..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs +++ /dev/null @@ -1,661 +0,0 @@ -use crate::entities::{CellChangeset, FieldType}; -use crate::entities::{CellIdentifier, CellIdentifierPayload}; -use crate::impl_type_option; -use crate::services::cell::{ - AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData, FromCellChangeset, FromCellString, -}; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; -use bytes::Bytes; -use chrono::format::strftime::StrftimeItems; -use chrono::{NaiveDateTime, Timelike}; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; -use serde::{Deserialize, Serialize}; -use strum_macros::EnumIter; - -// Date -#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct DateTypeOption { - #[pb(index = 1)] - pub date_format: DateFormat, - - #[pb(index = 2)] - pub time_format: TimeFormat, - - #[pb(index = 3)] - pub include_time: bool, -} -impl_type_option!(DateTypeOption, FieldType::DateTime); - -impl DateTypeOption { - #[allow(dead_code)] - pub fn new() -> Self { - Self::default() - } - - fn today_desc_from_timestamp(&self, timestamp: i64) -> DateCellData { - let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0); - self.date_from_native(native) - } - - fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellData { - if native.timestamp() == 0 { - return DateCellData::default(); - } - - let time = native.time(); - let has_time = time.hour() != 0 || time.second() != 0; - - let utc = self.utc_date_time_from_native(native); - let fmt = self.date_format.format_str(); - let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt))); - - let mut time = "".to_string(); - if has_time { - let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str()); - time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, ""); - } - - let timestamp = native.timestamp(); - DateCellData { date, time, timestamp } - } - - fn date_fmt(&self, time: &Option) -> String { - if self.include_time { - match time.as_ref() { - None => self.date_format.format_str().to_string(), - Some(time_str) => { - if time_str.is_empty() { - self.date_format.format_str().to_string() - } else { - format!("{} {}", self.date_format.format_str(), self.time_format.format_str()) - } - } - } - } else { - self.date_format.format_str().to_string() - } - } - - fn timestamp_from_utc_with_time( - &self, - utc: &chrono::DateTime, - time: &Option, - ) -> FlowyResult { - if let Some(time_str) = time.as_ref() { - if !time_str.is_empty() { - let date_str = format!( - "{}{}", - utc.format_with_items(StrftimeItems::new(self.date_format.format_str())), - &time_str - ); - - return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) { - Ok(native) => { - let utc = self.utc_date_time_from_native(native); - Ok(utc.timestamp()) - } - Err(_e) => { - let msg = format!("Parse {} failed", date_str); - Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)) - } - }; - } - } - - Ok(utc.timestamp()) - } - - fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime { - let native = NaiveDateTime::from_timestamp(timestamp, 0); - let native2 = NaiveDateTime::from_timestamp(timestamp, 0); - - if native > native2 {} - - self.utc_date_time_from_native(native) - } - - fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime { - chrono::DateTime::::from_utc(naive, chrono::Utc) - } -} - -impl CellDataOperation for DateTypeOption { - fn decode_cell_data( - &self, - cell_data: CellData, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { - // Return default data if the type_option_cell_data is not FieldType::DateTime. - // It happens when switching from one field to another. - // For example: - // FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen. - if !decoded_field_type.is_date() { - return Ok(DecodedCellData::default()); - } - let timestamp = cell_data.try_into_inner()?; - let date = self.today_desc_from_timestamp(timestamp.0); - DecodedCellData::try_from_bytes(date) - } - - fn apply_changeset( - &self, - changeset: CellDataChangeset, - _cell_rev: Option, - ) -> Result { - let changeset = changeset.try_into_inner()?; - let cell_data = match changeset.date_timestamp() { - None => 0, - Some(date_timestamp) => match (self.include_time, changeset.time) { - (true, Some(time)) => { - let time = Some(time.trim().to_uppercase()); - let utc = self.utc_date_time_from_timestamp(date_timestamp); - self.timestamp_from_utc_with_time(&utc, &time)? - } - _ => date_timestamp, - }, - }; - - Ok(cell_data.to_string()) - } -} - -pub struct DateTimestamp(i64); -impl AsRef for DateTimestamp { - fn as_ref(&self) -> &i64 { - &self.0 - } -} - -impl std::convert::From for i64 { - fn from(timestamp: DateTimestamp) -> Self { - timestamp.0 - } -} - -impl FromCellString for DateTimestamp { - fn from_cell_str(s: &str) -> FlowyResult - where - Self: Sized, - { - let num = s.parse::().unwrap_or(0); - Ok(DateTimestamp(num)) - } -} - -impl std::convert::From for DateTimestamp { - fn from(data: AnyCellData) -> Self { - let num = data.data.parse::().unwrap_or(0); - DateTimestamp(num) - } -} - -#[derive(Default)] -pub struct DateTypeOptionBuilder(DateTypeOption); -impl_into_box_type_option_builder!(DateTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption); - -impl DateTypeOptionBuilder { - pub fn date_format(mut self, date_format: DateFormat) -> Self { - self.0.date_format = date_format; - self - } - - pub fn time_format(mut self, time_format: TimeFormat) -> Self { - self.0.time_format = time_format; - self - } -} -impl TypeOptionBuilder for DateTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::DateTime - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)] -pub enum DateFormat { - Local = 0, - US = 1, - ISO = 2, - Friendly = 3, -} -impl std::default::Default for DateFormat { - fn default() -> Self { - DateFormat::Friendly - } -} - -impl std::convert::From for DateFormat { - fn from(value: i32) -> Self { - match value { - 0 => DateFormat::Local, - 1 => DateFormat::US, - 2 => DateFormat::ISO, - 3 => DateFormat::Friendly, - _ => { - tracing::error!("Unsupported date format, fallback to friendly"); - DateFormat::Friendly - } - } - } -} - -impl DateFormat { - pub fn value(&self) -> i32 { - *self as i32 - } - // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html - pub fn format_str(&self) -> &'static str { - match self { - DateFormat::Local => "%Y/%m/%d", - DateFormat::US => "%Y/%m/%d", - DateFormat::ISO => "%Y-%m-%d", - DateFormat::Friendly => "%b %d,%Y", - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum)] -pub enum TimeFormat { - TwelveHour = 0, - TwentyFourHour = 1, -} - -impl std::convert::From for TimeFormat { - fn from(value: i32) -> Self { - match value { - 0 => TimeFormat::TwelveHour, - 1 => TimeFormat::TwentyFourHour, - _ => { - tracing::error!("Unsupported time format, fallback to TwentyFourHour"); - TimeFormat::TwentyFourHour - } - } - } -} - -impl TimeFormat { - pub fn value(&self) -> i32 { - *self as i32 - } - - // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html - pub fn format_str(&self) -> &'static str { - match self { - TimeFormat::TwelveHour => "%I:%M %p", - TimeFormat::TwentyFourHour => "%R", - } - } -} - -impl std::default::Default for TimeFormat { - fn default() -> Self { - TimeFormat::TwentyFourHour - } -} - -#[derive(Clone, Debug, Default, ProtoBuf)] -pub struct DateCellData { - #[pb(index = 1)] - pub date: String, - - #[pb(index = 2)] - pub time: String, - - #[pb(index = 3)] - pub timestamp: i64, -} - -#[derive(Clone, Debug, Default, ProtoBuf)] -pub struct DateChangesetPayload { - #[pb(index = 1)] - pub cell_identifier: CellIdentifierPayload, - - #[pb(index = 2, one_of)] - pub date: Option, - - #[pb(index = 3, one_of)] - pub time: Option, -} - -pub struct DateChangesetParams { - pub cell_identifier: CellIdentifier, - pub date: Option, - pub time: Option, -} - -impl TryInto for DateChangesetPayload { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?; - Ok(DateChangesetParams { - cell_identifier, - date: self.date, - time: self.time, - }) - } -} - -impl std::convert::From for CellChangeset { - fn from(params: DateChangesetParams) -> Self { - let changeset = DateCellChangeset { - date: params.date, - time: params.time, - }; - let s = serde_json::to_string(&changeset).unwrap(); - CellChangeset { - grid_id: params.cell_identifier.grid_id, - row_id: params.cell_identifier.row_id, - field_id: params.cell_identifier.field_id, - content: Some(s), - } - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct DateCellChangeset { - pub date: Option, - pub time: Option, -} - -impl DateCellChangeset { - pub fn date_timestamp(&self) -> Option { - if let Some(date) = &self.date { - match date.parse::() { - Ok(date_timestamp) => Some(date_timestamp), - Err(_) => None, - } - } else { - None - } - } -} - -impl FromCellChangeset for DateCellChangeset { - fn from_changeset(changeset: String) -> FlowyResult - where - Self: Sized, - { - serde_json::from_str::(&changeset).map_err(internal_error) - } -} - -#[cfg(test)] -mod tests { - use crate::entities::FieldType; - use crate::services::cell::{CellDataChangeset, CellDataOperation}; - use crate::services::field::FieldBuilder; - use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat}; - use flowy_grid_data_model::revision::FieldRevision; - use strum::IntoEnumIterator; - - #[test] - fn date_type_option_invalid_input_test() { - let type_option = DateTypeOption::default(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some("1e".to_string()), - time: Some("23:00".to_owned()), - }, - &field_type, - &field_rev, - "", - ); - } - - #[test] - fn date_type_option_date_format_test() { - let mut type_option = DateTypeOption::default(); - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - for date_format in DateFormat::iter() { - type_option.date_format = date_format; - match date_format { - DateFormat::Friendly => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022"); - } - DateFormat::US => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); - } - DateFormat::ISO => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14"); - } - DateFormat::Local => { - assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); - } - } - } - } - - #[test] - fn date_type_option_time_format_test() { - let mut type_option = DateTypeOption::default(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for time_format in TimeFormat::iter() { - type_option.time_format = time_format; - type_option.include_time = true; - match time_format { - TimeFormat::TwentyFourHour => { - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: Some("23:00".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 23:00", - ); - } - TimeFormat::TwelveHour => { - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - // - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: Some("".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022", - ); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(1653609600.to_string()), - time: Some("11:23 pm".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 11:23 PM", - ); - } - } - } - } - - #[test] - fn date_type_option_apply_changeset_test() { - let mut type_option = DateTypeOption::new(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - - type_option.include_time = true; - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: None, - }, - &field_type, - &field_rev, - "May 27,2022", - ); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: Some("1:00".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 01:00", - ); - - type_option.time_format = TimeFormat::TwelveHour; - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp), - time: Some("1:00 am".to_owned()), - }, - &field_type, - &field_rev, - "May 27,2022 01:00 AM", - ); - } - - #[test] - #[should_panic] - fn date_type_option_apply_changeset_error_test() { - let mut type_option = DateTypeOption::new(); - type_option.include_time = true; - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp.clone()), - time: Some("1:".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp), - time: Some("1:00".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - } - - #[test] - #[should_panic] - fn date_type_option_twelve_hours_to_twenty_four_hours() { - let mut type_option = DateTypeOption::new(); - type_option.include_time = true; - let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); - let date_timestamp = "1653609600".to_owned(); - - assert_changeset_result( - &type_option, - DateCellChangeset { - date: Some(date_timestamp), - time: Some("1:00 am".to_owned()), - }, - &FieldType::DateTime, - &field_rev, - "May 27,2022 01:00", - ); - } - - fn assert_changeset_result( - type_option: &DateTypeOption, - changeset: DateCellChangeset, - _field_type: &FieldType, - field_rev: &FieldRevision, - expected: &str, - ) { - let changeset = CellDataChangeset(Some(changeset)); - let encoded_data = type_option.apply_changeset(changeset, None).unwrap(); - assert_eq!( - expected.to_owned(), - decode_cell_data(encoded_data, type_option, field_rev) - ); - } - - fn assert_decode_timestamp( - timestamp: i64, - type_option: &DateTypeOption, - field_rev: &FieldRevision, - expected: &str, - ) { - let s = serde_json::to_string(&DateCellChangeset { - date: Some(timestamp.to_string()), - time: None, - }) - .unwrap(); - let encoded_data = type_option.apply_changeset(s.into(), None).unwrap(); - - assert_eq!( - expected.to_owned(), - decode_cell_data(encoded_data, type_option, field_rev) - ); - } - - fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String { - let decoded_data = type_option - .decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev) - .unwrap() - .parse::() - .unwrap(); - - if type_option.include_time { - format!("{}{}", decoded_data.date, decoded_data.time) - } else { - decoded_data.date - } - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs new file mode 100644 index 0000000000..0c81268f77 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_tests.rs @@ -0,0 +1,272 @@ +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::{CellDataChangeset, CellDataOperation}; + use crate::services::field::*; + // use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat}; + use flowy_grid_data_model::revision::FieldRevision; + use strum::IntoEnumIterator; + + #[test] + fn date_type_option_invalid_input_test() { + let type_option = DateTypeOption::default(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some("1e".to_string()), + time: Some("23:00".to_owned()), + }, + &field_type, + &field_rev, + "", + ); + } + + #[test] + fn date_type_option_date_format_test() { + let mut type_option = DateTypeOption::default(); + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + for date_format in DateFormat::iter() { + type_option.date_format = date_format; + match date_format { + DateFormat::Friendly => { + assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022"); + } + DateFormat::US => { + assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); + } + DateFormat::ISO => { + assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14"); + } + DateFormat::Local => { + assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14"); + } + } + } + } + + #[test] + fn date_type_option_time_format_test() { + let mut type_option = DateTypeOption::default(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for time_format in TimeFormat::iter() { + type_option.time_format = time_format; + type_option.include_time = true; + match time_format { + TimeFormat::TwentyFourHour => { + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(1653609600.to_string()), + time: None, + }, + &field_type, + &field_rev, + "May 27,2022", + ); + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(1653609600.to_string()), + time: Some("23:00".to_owned()), + }, + &field_type, + &field_rev, + "May 27,2022 23:00", + ); + } + TimeFormat::TwelveHour => { + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(1653609600.to_string()), + time: None, + }, + &field_type, + &field_rev, + "May 27,2022", + ); + // + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(1653609600.to_string()), + time: Some("".to_owned()), + }, + &field_type, + &field_rev, + "May 27,2022", + ); + + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(1653609600.to_string()), + time: Some("11:23 pm".to_owned()), + }, + &field_type, + &field_rev, + "May 27,2022 11:23 PM", + ); + } + } + } + } + + #[test] + fn date_type_option_apply_changeset_test() { + let mut type_option = DateTypeOption::new(); + let field_type = FieldType::DateTime; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + let date_timestamp = "1653609600".to_owned(); + + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(date_timestamp.clone()), + time: None, + }, + &field_type, + &field_rev, + "May 27,2022", + ); + + type_option.include_time = true; + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(date_timestamp.clone()), + time: None, + }, + &field_type, + &field_rev, + "May 27,2022", + ); + + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(date_timestamp.clone()), + time: Some("1:00".to_owned()), + }, + &field_type, + &field_rev, + "May 27,2022 01:00", + ); + + type_option.time_format = TimeFormat::TwelveHour; + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(date_timestamp), + time: Some("1:00 am".to_owned()), + }, + &field_type, + &field_rev, + "May 27,2022 01:00 AM", + ); + } + + #[test] + #[should_panic] + fn date_type_option_apply_changeset_error_test() { + let mut type_option = DateTypeOption::new(); + type_option.include_time = true; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + let date_timestamp = "1653609600".to_owned(); + + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(date_timestamp.clone()), + time: Some("1:".to_owned()), + }, + &FieldType::DateTime, + &field_rev, + "May 27,2022 01:00", + ); + + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(date_timestamp), + time: Some("1:00".to_owned()), + }, + &FieldType::DateTime, + &field_rev, + "May 27,2022 01:00", + ); + } + + #[test] + #[should_panic] + fn date_type_option_twelve_hours_to_twenty_four_hours() { + let mut type_option = DateTypeOption::new(); + type_option.include_time = true; + let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build(); + let date_timestamp = "1653609600".to_owned(); + + assert_changeset_result( + &type_option, + DateCellChangeset { + date: Some(date_timestamp), + time: Some("1:00 am".to_owned()), + }, + &FieldType::DateTime, + &field_rev, + "May 27,2022 01:00", + ); + } + + fn assert_changeset_result( + type_option: &DateTypeOption, + changeset: DateCellChangeset, + _field_type: &FieldType, + field_rev: &FieldRevision, + expected: &str, + ) { + let changeset = CellDataChangeset(Some(changeset)); + let encoded_data = type_option.apply_changeset(changeset, None).unwrap(); + assert_eq!( + expected.to_owned(), + decode_cell_data(encoded_data, type_option, field_rev) + ); + } + + fn assert_decode_timestamp( + timestamp: i64, + type_option: &DateTypeOption, + field_rev: &FieldRevision, + expected: &str, + ) { + let s = serde_json::to_string(&DateCellChangeset { + date: Some(timestamp.to_string()), + time: None, + }) + .unwrap(); + let encoded_data = type_option.apply_changeset(s.into(), None).unwrap(); + + assert_eq!( + expected.to_owned(), + decode_cell_data(encoded_data, type_option, field_rev) + ); + } + + fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String { + let decoded_data = type_option + .decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev) + .unwrap() + .with_parser(DateCellDataParser()) + .unwrap(); + + if type_option.include_time { + format!("{}{}", decoded_data.date, decoded_data.time) + } else { + decoded_data.date + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs new file mode 100644 index 0000000000..783b9faa7c --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option.rs @@ -0,0 +1,195 @@ +use crate::entities::FieldType; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{ + BoxTypeOptionBuilder, DateCellChangeset, DateCellData, DateFormat, DateTimestamp, TimeFormat, TypeOptionBuilder, +}; +use bytes::Bytes; +use chrono::format::strftime::StrftimeItems; +use chrono::{NaiveDateTime, Timelike}; +use flowy_derive::ProtoBuf; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; +use serde::{Deserialize, Serialize}; + +// Date +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct DateTypeOption { + #[pb(index = 1)] + pub date_format: DateFormat, + + #[pb(index = 2)] + pub time_format: TimeFormat, + + #[pb(index = 3)] + pub include_time: bool, +} +impl_type_option!(DateTypeOption, FieldType::DateTime); + +impl DateTypeOption { + #[allow(dead_code)] + pub fn new() -> Self { + Self::default() + } + + fn today_desc_from_timestamp>(&self, timestamp: T) -> DateCellData { + let timestamp = *timestamp.as_ref(); + let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0); + self.date_from_native(native) + } + + fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellData { + if native.timestamp() == 0 { + return DateCellData::default(); + } + + let time = native.time(); + let has_time = time.hour() != 0 || time.second() != 0; + + let utc = self.utc_date_time_from_native(native); + let fmt = self.date_format.format_str(); + let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt))); + + let mut time = "".to_string(); + if has_time { + let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str()); + time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, ""); + } + + let timestamp = native.timestamp(); + DateCellData { date, time, timestamp } + } + + fn date_fmt(&self, time: &Option) -> String { + if self.include_time { + match time.as_ref() { + None => self.date_format.format_str().to_string(), + Some(time_str) => { + if time_str.is_empty() { + self.date_format.format_str().to_string() + } else { + format!("{} {}", self.date_format.format_str(), self.time_format.format_str()) + } + } + } + } else { + self.date_format.format_str().to_string() + } + } + + fn timestamp_from_utc_with_time( + &self, + utc: &chrono::DateTime, + time: &Option, + ) -> FlowyResult { + if let Some(time_str) = time.as_ref() { + if !time_str.is_empty() { + let date_str = format!( + "{}{}", + utc.format_with_items(StrftimeItems::new(self.date_format.format_str())), + &time_str + ); + + return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) { + Ok(native) => { + let utc = self.utc_date_time_from_native(native); + Ok(utc.timestamp()) + } + Err(_e) => { + let msg = format!("Parse {} failed", date_str); + Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)) + } + }; + } + } + + Ok(utc.timestamp()) + } + + fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime { + let native = NaiveDateTime::from_timestamp(timestamp, 0); + self.utc_date_time_from_native(native) + } + + fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime { + chrono::DateTime::::from_utc(naive, chrono::Utc) + } +} + +impl CellDisplayable for DateTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let timestamp = cell_data.try_into_inner()?; + let date_cell_data = self.today_desc_from_timestamp(timestamp); + CellBytes::from(date_cell_data) + } +} + +impl CellDataOperation for DateTypeOption { + fn decode_cell_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult { + // Return default data if the type_option_cell_data is not FieldType::DateTime. + // It happens when switching from one field to another. + // For example: + // FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen. + if !decoded_field_type.is_date() { + return Ok(CellBytes::default()); + } + self.display_data(cell_data, decoded_field_type, field_rev) + } + + fn apply_changeset( + &self, + changeset: CellDataChangeset, + _cell_rev: Option, + ) -> Result { + let changeset = changeset.try_into_inner()?; + let cell_data = match changeset.date_timestamp() { + None => 0, + Some(date_timestamp) => match (self.include_time, changeset.time) { + (true, Some(time)) => { + let time = Some(time.trim().to_uppercase()); + let utc = self.utc_date_time_from_timestamp(date_timestamp); + self.timestamp_from_utc_with_time(&utc, &time)? + } + _ => date_timestamp, + }, + }; + + Ok(cell_data.to_string()) + } +} + +#[derive(Default)] +pub struct DateTypeOptionBuilder(DateTypeOption); +impl_into_box_type_option_builder!(DateTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption); + +impl DateTypeOptionBuilder { + pub fn date_format(mut self, date_format: DateFormat) -> Self { + self.0.date_format = date_format; + self + } + + pub fn time_format(mut self, time_format: TimeFormat) -> Self { + self.0.time_format = time_format; + self + } +} +impl TypeOptionBuilder for DateTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::DateTime + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs new file mode 100644 index 0000000000..b73c89a9b2 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -0,0 +1,210 @@ +use crate::entities::CellChangeset; +use crate::entities::{CellIdentifier, CellIdentifierPayload}; +use crate::services::cell::{CellBytesParser, FromCellChangeset, FromCellString}; +use bytes::Bytes; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::{internal_error, ErrorCode, FlowyResult}; + +use serde::{Deserialize, Serialize}; +use strum_macros::EnumIter; + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct DateCellData { + #[pb(index = 1)] + pub date: String, + + #[pb(index = 2)] + pub time: String, + + #[pb(index = 3)] + pub timestamp: i64, +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct DateChangesetPayload { + #[pb(index = 1)] + pub cell_identifier: CellIdentifierPayload, + + #[pb(index = 2, one_of)] + pub date: Option, + + #[pb(index = 3, one_of)] + pub time: Option, +} + +pub struct DateChangesetParams { + pub cell_identifier: CellIdentifier, + pub date: Option, + pub time: Option, +} + +impl TryInto for DateChangesetPayload { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?; + Ok(DateChangesetParams { + cell_identifier, + date: self.date, + time: self.time, + }) + } +} + +impl std::convert::From for CellChangeset { + fn from(params: DateChangesetParams) -> Self { + let changeset = DateCellChangeset { + date: params.date, + time: params.time, + }; + let s = serde_json::to_string(&changeset).unwrap(); + CellChangeset { + grid_id: params.cell_identifier.grid_id, + row_id: params.cell_identifier.row_id, + field_id: params.cell_identifier.field_id, + content: Some(s), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct DateCellChangeset { + pub date: Option, + pub time: Option, +} + +impl DateCellChangeset { + pub fn date_timestamp(&self) -> Option { + if let Some(date) = &self.date { + match date.parse::() { + Ok(date_timestamp) => Some(date_timestamp), + Err(_) => None, + } + } else { + None + } + } +} + +impl FromCellChangeset for DateCellChangeset { + fn from_changeset(changeset: String) -> FlowyResult + where + Self: Sized, + { + serde_json::from_str::(&changeset).map_err(internal_error) + } +} +pub struct DateTimestamp(i64); +impl AsRef for DateTimestamp { + fn as_ref(&self) -> &i64 { + &self.0 + } +} + +impl std::convert::From for i64 { + fn from(timestamp: DateTimestamp) -> Self { + timestamp.0 + } +} + +impl FromCellString for DateTimestamp { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + let num = s.parse::().unwrap_or(0); + Ok(DateTimestamp(num)) + } +} + +#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)] +pub enum DateFormat { + Local = 0, + US = 1, + ISO = 2, + Friendly = 3, +} +impl std::default::Default for DateFormat { + fn default() -> Self { + DateFormat::Friendly + } +} + +impl std::convert::From for DateFormat { + fn from(value: i32) -> Self { + match value { + 0 => DateFormat::Local, + 1 => DateFormat::US, + 2 => DateFormat::ISO, + 3 => DateFormat::Friendly, + _ => { + tracing::error!("Unsupported date format, fallback to friendly"); + DateFormat::Friendly + } + } + } +} + +impl DateFormat { + pub fn value(&self) -> i32 { + *self as i32 + } + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + DateFormat::Local => "%Y/%m/%d", + DateFormat::US => "%Y/%m/%d", + DateFormat::ISO => "%Y-%m-%d", + DateFormat::Friendly => "%b %d,%Y", + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum)] +pub enum TimeFormat { + TwelveHour = 0, + TwentyFourHour = 1, +} + +impl std::convert::From for TimeFormat { + fn from(value: i32) -> Self { + match value { + 0 => TimeFormat::TwelveHour, + 1 => TimeFormat::TwentyFourHour, + _ => { + tracing::error!("Unsupported time format, fallback to TwentyFourHour"); + TimeFormat::TwentyFourHour + } + } + } +} + +impl TimeFormat { + pub fn value(&self) -> i32 { + *self as i32 + } + + // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html + pub fn format_str(&self) -> &'static str { + match self { + TimeFormat::TwelveHour => "%I:%M %p", + TimeFormat::TwentyFourHour => "%R", + } + } +} + +impl std::default::Default for TimeFormat { + fn default() -> Self { + TimeFormat::TwentyFourHour + } +} + +pub struct DateCellDataParser(); +impl CellBytesParser for DateCellDataParser { + type Object = DateCellData; + + fn parse(&self, bytes: &Bytes) -> FlowyResult { + DateCellData::try_from(bytes.as_ref()).map_err(internal_error) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/mod.rs new file mode 100644 index 0000000000..395f2c9104 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option/mod.rs @@ -0,0 +1,7 @@ +#![allow(clippy::module_inception)] +mod date_tests; +mod date_type_option; +mod date_type_option_entities; + +pub use date_type_option::*; +pub use date_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs index 754e441876..32fd9ef0a4 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/mod.rs @@ -1,17 +1,14 @@ -mod checkbox_type_option; -mod date_type_option; -mod multi_select_type_option; -mod number_type_option; -mod single_select_type_option; -mod text_type_option; -mod url_type_option; +pub mod checkbox_type_option; +pub mod date_type_option; +pub mod number_type_option; +pub mod selection_type_option; +pub mod text_type_option; +pub mod url_type_option; mod util; pub use checkbox_type_option::*; pub use date_type_option::*; -pub use multi_select_type_option::*; -pub use multi_select_type_option::*; pub use number_type_option::*; -pub use single_select_type_option::*; +pub use selection_type_option::*; pub use text_type_option::*; pub use url_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs index fffbad97bf..4b2bcc1ecd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/mod.rs @@ -1,6 +1,9 @@ #![allow(clippy::module_inception)] mod format; +mod number_tests; mod number_type_option; +mod number_type_option_entities; pub use format::*; pub use number_type_option::*; +pub use number_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs new file mode 100644 index 0000000000..6ea1e8f302 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_tests.rs @@ -0,0 +1,139 @@ +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::CellDataOperation; + use crate::services::field::FieldBuilder; + use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption}; + use flowy_grid_data_model::revision::FieldRevision; + use strum::IntoEnumIterator; + + #[test] + fn number_type_option_invalid_input_test() { + let type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_equal(&type_option, "", "", &field_type, &field_rev); + assert_equal(&type_option, "abc", "", &field_type, &field_rev); + } + + #[test] + fn number_type_option_strip_symbol_test() { + let mut type_option = NumberTypeOption::new(); + type_option.format = NumberFormat::USD; + assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned()); + + type_option.format = NumberFormat::Yuan; + assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned()); + } + + #[test] + fn number_type_option_format_number_test() { + let mut type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); + } + NumberFormat::USD => { + assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev); + } + NumberFormat::Yen => { + assert_equal(&type_option, "18443", "Â¥18,443", &field_type, &field_rev); + } + NumberFormat::Yuan => { + assert_equal(&type_option, "18443", "CNÂ¥18,443", &field_type, &field_rev); + } + NumberFormat::EUR => { + assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev); + } + _ => {} + } + } + } + + #[test] + fn number_type_option_format_str_test() { + let mut type_option = NumberTypeOption::default(); + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); + assert_equal(&type_option, "0.2", "0.2", &field_type, &field_rev); + } + NumberFormat::USD => { + assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_rev); + assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_rev); + assert_equal(&type_option, "", "", &field_type, &field_rev); + assert_equal(&type_option, "abc", "", &field_type, &field_rev); + } + NumberFormat::Yen => { + assert_equal(&type_option, "Â¥18,44", "Â¥1,844", &field_type, &field_rev); + assert_equal(&type_option, "Â¥1844", "Â¥1,844", &field_type, &field_rev); + } + NumberFormat::Yuan => { + assert_equal(&type_option, "CNÂ¥18,44", "CNÂ¥1,844", &field_type, &field_rev); + assert_equal(&type_option, "CNÂ¥1844", "CNÂ¥1,844", &field_type, &field_rev); + } + NumberFormat::EUR => { + assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_rev); + assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_rev); + assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_rev); + } + _ => {} + } + } + } + + #[test] + fn number_description_sign_test() { + let mut type_option = NumberTypeOption { + sign_positive: false, + ..Default::default() + }; + let field_type = FieldType::Number; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + + for format in NumberFormat::iter() { + type_option.format = format; + match format { + NumberFormat::Num => { + assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); + } + NumberFormat::USD => { + assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev); + } + NumberFormat::Yen => { + assert_equal(&type_option, "18443", "-Â¥18,443", &field_type, &field_rev); + } + NumberFormat::EUR => { + assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev); + } + _ => {} + } + } + } + + fn assert_equal( + type_option: &NumberTypeOption, + cell_data: &str, + expected_str: &str, + field_type: &FieldType, + field_rev: &FieldRevision, + ) { + assert_eq!( + type_option + .decode_cell_data(cell_data.to_owned().into(), field_type, field_rev) + .unwrap() + .to_string(), + expected_str.to_owned() + ); + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs index c8f79df20c..26d1d64248 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs @@ -1,17 +1,15 @@ -use crate::impl_type_option; - use crate::entities::FieldType; -use crate::services::cell::{CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::number_currency::Currency; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation}; use crate::services::field::type_options::number_type_option::format::*; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBuilder}; use bytes::Bytes; use flowy_derive::ProtoBuf; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; use rust_decimal::Decimal; -use rusty_money::Money; + use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -110,15 +108,15 @@ impl CellDataOperation for NumberTypeOption { cell_data: CellData, decoded_field_type: &FieldType, _field_rev: &FieldRevision, - ) -> FlowyResult { + ) -> FlowyResult { if decoded_field_type.is_date() { - return Ok(DecodedCellData::default()); + return Ok(CellBytes::default()); } let cell_data: String = cell_data.try_into_inner()?; match self.format_cell_data(&cell_data) { - Ok(num) => Ok(DecodedCellData::new(num.to_string())), - Err(_) => Ok(DecodedCellData::default()), + Ok(num) => Ok(CellBytes::new(num.to_string())), + Err(_) => Ok(CellBytes::default()), } } @@ -147,230 +145,3 @@ impl std::default::Default for NumberTypeOption { } } } - -#[derive(Default)] -pub struct NumberCellData { - decimal: Option, - money: Option, -} - -impl NumberCellData { - pub fn new() -> Self { - Self { - decimal: Default::default(), - money: None, - } - } - - pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult { - let mut num_str = strip_currency_symbol(s); - let currency = format.currency(); - if num_str.is_empty() { - return Ok(Self::default()); - } - match Decimal::from_str(&num_str) { - Ok(mut decimal) => { - decimal.set_sign_positive(sign_positive); - let money = Money::from_decimal(decimal, currency); - Ok(Self::from_money(money)) - } - Err(_) => match Money::from_str(&num_str, currency) { - Ok(money) => Ok(NumberCellData::from_money(money)), - Err(_) => { - num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string())); - if num_str.chars().all(char::is_numeric) { - Self::from_format_str(&num_str, sign_positive, format) - } else { - Err(FlowyError::invalid_data().context("Should only contain numbers")) - } - } - }, - } - } - - pub fn from_decimal(decimal: Decimal) -> Self { - Self { - decimal: Some(decimal), - money: None, - } - } - - pub fn from_money(money: Money) -> Self { - Self { - decimal: Some(*money.amount()), - money: Some(money.to_string()), - } - } - - pub fn decimal(&self) -> &Option { - &self.decimal - } - - pub fn is_empty(&self) -> bool { - self.decimal.is_none() - } -} - -impl FromStr for NumberCellData { - type Err = rust_decimal::Error; - - fn from_str(s: &str) -> Result { - if s.is_empty() { - return Ok(Self::default()); - } - let decimal = Decimal::from_str(s)?; - Ok(Self::from_decimal(decimal)) - } -} - -impl ToString for NumberCellData { - fn to_string(&self) -> String { - match &self.money { - None => match self.decimal { - None => String::default(), - Some(decimal) => decimal.to_string(), - }, - Some(money) => money.to_string(), - } - } -} - -#[cfg(test)] -mod tests { - use crate::entities::FieldType; - use crate::services::cell::CellDataOperation; - use crate::services::field::FieldBuilder; - use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption}; - use flowy_grid_data_model::revision::FieldRevision; - use strum::IntoEnumIterator; - - #[test] - fn number_type_option_invalid_input_test() { - let type_option = NumberTypeOption::default(); - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_equal(&type_option, "", "", &field_type, &field_rev); - assert_equal(&type_option, "abc", "", &field_type, &field_rev); - } - - #[test] - fn number_type_option_strip_symbol_test() { - let mut type_option = NumberTypeOption::new(); - type_option.format = NumberFormat::USD; - assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned()); - - type_option.format = NumberFormat::Yuan; - assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned()); - } - - #[test] - fn number_type_option_format_number_test() { - let mut type_option = NumberTypeOption::default(); - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for format in NumberFormat::iter() { - type_option.format = format; - match format { - NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); - } - NumberFormat::USD => { - assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev); - } - NumberFormat::Yen => { - assert_equal(&type_option, "18443", "Â¥18,443", &field_type, &field_rev); - } - NumberFormat::Yuan => { - assert_equal(&type_option, "18443", "CNÂ¥18,443", &field_type, &field_rev); - } - NumberFormat::EUR => { - assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev); - } - _ => {} - } - } - } - - #[test] - fn number_type_option_format_str_test() { - let mut type_option = NumberTypeOption::default(); - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for format in NumberFormat::iter() { - type_option.format = format; - match format { - NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); - assert_equal(&type_option, "0.2", "0.2", &field_type, &field_rev); - } - NumberFormat::USD => { - assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_rev); - assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_rev); - assert_equal(&type_option, "", "", &field_type, &field_rev); - assert_equal(&type_option, "abc", "", &field_type, &field_rev); - } - NumberFormat::Yen => { - assert_equal(&type_option, "Â¥18,44", "Â¥1,844", &field_type, &field_rev); - assert_equal(&type_option, "Â¥1844", "Â¥1,844", &field_type, &field_rev); - } - NumberFormat::Yuan => { - assert_equal(&type_option, "CNÂ¥18,44", "CNÂ¥1,844", &field_type, &field_rev); - assert_equal(&type_option, "CNÂ¥1844", "CNÂ¥1,844", &field_type, &field_rev); - } - NumberFormat::EUR => { - assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_rev); - assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_rev); - assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_rev); - } - _ => {} - } - } - } - - #[test] - fn number_description_sign_test() { - let mut type_option = NumberTypeOption { - sign_positive: false, - ..Default::default() - }; - let field_type = FieldType::Number; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - - for format in NumberFormat::iter() { - type_option.format = format; - match format { - NumberFormat::Num => { - assert_equal(&type_option, "18443", "18443", &field_type, &field_rev); - } - NumberFormat::USD => { - assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev); - } - NumberFormat::Yen => { - assert_equal(&type_option, "18443", "-Â¥18,443", &field_type, &field_rev); - } - NumberFormat::EUR => { - assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev); - } - _ => {} - } - } - } - - fn assert_equal( - type_option: &NumberTypeOption, - cell_data: &str, - expected_str: &str, - field_type: &FieldType, - field_rev: &FieldRevision, - ) { - assert_eq!( - type_option - .decode_cell_data(cell_data.to_owned().into(), field_type, field_rev) - .unwrap() - .to_string(), - expected_str.to_owned() - ); - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs new file mode 100644 index 0000000000..6297114a07 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option_entities.rs @@ -0,0 +1,105 @@ +use crate::services::cell::CellBytesParser; +use crate::services::field::number_currency::Currency; +use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL}; +use bytes::Bytes; +use flowy_error::{FlowyError, FlowyResult}; +use rust_decimal::Decimal; +use rusty_money::Money; +use std::str::FromStr; + +#[derive(Default)] +pub struct NumberCellData { + decimal: Option, + money: Option, +} + +impl NumberCellData { + pub fn new() -> Self { + Self { + decimal: Default::default(), + money: None, + } + } + + pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult { + let mut num_str = strip_currency_symbol(s); + let currency = format.currency(); + if num_str.is_empty() { + return Ok(Self::default()); + } + match Decimal::from_str(&num_str) { + Ok(mut decimal) => { + decimal.set_sign_positive(sign_positive); + let money = Money::from_decimal(decimal, currency); + Ok(Self::from_money(money)) + } + Err(_) => match Money::from_str(&num_str, currency) { + Ok(money) => Ok(NumberCellData::from_money(money)), + Err(_) => { + num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string())); + if num_str.chars().all(char::is_numeric) { + Self::from_format_str(&num_str, sign_positive, format) + } else { + Err(FlowyError::invalid_data().context("Should only contain numbers")) + } + } + }, + } + } + + pub fn from_decimal(decimal: Decimal) -> Self { + Self { + decimal: Some(decimal), + money: None, + } + } + + pub fn from_money(money: Money) -> Self { + Self { + decimal: Some(*money.amount()), + money: Some(money.to_string()), + } + } + + pub fn decimal(&self) -> &Option { + &self.decimal + } + + pub fn is_empty(&self) -> bool { + self.decimal.is_none() + } +} + +// impl FromStr for NumberCellData { +// type Err = FlowyError; +// +// fn from_str(s: &str) -> Result { +// if s.is_empty() { +// return Ok(Self::default()); +// } +// let decimal = Decimal::from_str(s).map_err(internal_error)?; +// Ok(Self::from_decimal(decimal)) +// } +// } + +impl ToString for NumberCellData { + fn to_string(&self) -> String { + match &self.money { + None => match self.decimal { + None => String::default(), + Some(decimal) => decimal.to_string(), + }, + Some(money) => money.to_string(), + } + } +} +pub struct NumberCellDataParser(pub NumberFormat); +impl CellBytesParser for NumberCellDataParser { + type Object = NumberCellData; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => NumberCellData::from_format_str(&s, true, &self.0), + Err(_) => Ok(NumberCellData::default()), + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs new file mode 100644 index 0000000000..45d2b96e83 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/mod.rs @@ -0,0 +1,7 @@ +mod multi_select_type_option; +mod select_option; +mod single_select_type_option; + +pub use multi_select_type_option::*; +pub use select_option::*; +pub use single_select_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs similarity index 83% rename from frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 209ed234f7..759114ca5a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -1,19 +1,15 @@ use crate::entities::FieldType; - use crate::impl_type_option; -use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::select_option::{ - make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds, - SelectOptionOperation, SELECTION_IDS_SEPARATOR, -}; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; use crate::services::field::type_options::util::get_cell_data; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::field::{ + make_selected_select_options, BoxTypeOptionBuilder, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, + SelectOptionIds, SelectOptionOperation, TypeOptionBuilder, SELECTION_IDS_SEPARATOR, +}; use bytes::Bytes; use flowy_derive::ProtoBuf; use flowy_error::{FlowyError, FlowyResult}; - use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; - use serde::{Deserialize, Serialize}; // Multiple select @@ -28,8 +24,8 @@ pub struct MultiSelectTypeOption { impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect); impl SelectOptionOperation for MultiSelectTypeOption { - fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData { - let select_options = make_selected_select_options(any_cell_data, &self.options); + fn selected_select_option(&self, cell_data: CellData) -> SelectOptionCellData { + let select_options = make_selected_select_options(cell_data, &self.options); SelectOptionCellData { options: self.options.clone(), select_options, @@ -50,24 +46,13 @@ impl CellDataOperation for MultiSele &self, cell_data: CellData, decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { + field_rev: &FieldRevision, + ) -> FlowyResult { if !decoded_field_type.is_select_option() { - return Ok(DecodedCellData::default()); + return Ok(CellBytes::default()); } - let ids: SelectOptionIds = cell_data.try_into_inner()?; - let select_options = ids - .iter() - .flat_map(|option_id| self.options.iter().find(|option| &option.id == option_id).cloned()) - .collect::>(); - - let cell_data = SelectOptionCellData { - options: self.options.clone(), - select_options, - }; - - DecodedCellData::try_from_bytes(cell_data) + self.display_data(cell_data, decoded_field_type, field_rev) } fn apply_changeset( @@ -131,7 +116,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder { mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; - use crate::services::field::select_option::*; + use crate::services::field::type_options::selection_type_option::*; use crate::services::field::FieldBuilder; use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder}; use flowy_grid_data_model::revision::FieldRevision; @@ -195,7 +180,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &field_type, field_rev) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, ); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs similarity index 87% rename from frontend/rust-lib/flowy-grid/src/services/field/select_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs index 3e974e9f56..c06bf25957 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/select_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/select_option.rs @@ -1,8 +1,9 @@ use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType}; -use crate::services::cell::{AnyCellData, FromCellChangeset, FromCellString}; +use crate::services::cell::{CellBytes, CellBytesParser, CellData, CellDisplayable, FromCellChangeset, FromCellString}; use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; +use bytes::Bytes; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; +use flowy_error::{internal_error, ErrorCode, FlowyResult}; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry}; use nanoid::nanoid; @@ -60,12 +61,11 @@ impl std::default::Default for SelectOptionColor { } } -pub fn make_selected_select_options>( - any_cell_data: T, +pub fn make_selected_select_options( + cell_data: CellData, options: &[SelectOption], ) -> Vec { - if let Ok(type_option_cell_data) = any_cell_data.try_into() { - let ids = SelectOptionIds::from(type_option_cell_data.data); + if let Ok(ids) = cell_data.try_into_inner() { ids.iter() .flat_map(|option_id| options.iter().find(|option| &option.id == option_id).cloned()) .collect() @@ -100,13 +100,27 @@ pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync { SelectOption::with_color(name, color) } - fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData; + fn selected_select_option(&self, cell_data: CellData) -> SelectOptionCellData; fn options(&self) -> &Vec; fn mut_options(&mut self) -> &mut Vec; } +impl CellDisplayable for T +where + T: SelectOptionOperation, +{ + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + CellBytes::from(self.selected_select_option(cell_data)) + } +} + pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult> { let field_type: FieldType = field_rev.field_type_rev.into(); match &field_type { @@ -147,14 +161,6 @@ impl SelectOptionIds { } } -impl std::convert::TryFrom for SelectOptionIds { - type Error = FlowyError; - - fn try_from(value: AnyCellData) -> Result { - Ok(Self::from(value.data)) - } -} - impl FromCellString for SelectOptionIds { fn from_cell_str(s: &str) -> FlowyResult where @@ -196,6 +202,25 @@ impl std::ops::DerefMut for SelectOptionIds { &mut self.0 } } +pub struct SelectOptionIdsParser(); +impl CellBytesParser for SelectOptionIdsParser { + type Object = SelectOptionIds; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => Ok(SelectOptionIds::from(s)), + Err(_) => Ok(SelectOptionIds::from("".to_owned())), + } + } +} + +pub struct SelectOptionCellDataParser(); +impl CellBytesParser for SelectOptionCellDataParser { + type Object = SelectOptionCellData; + + fn parse(&self, bytes: &Bytes) -> FlowyResult { + SelectOptionCellData::try_from(bytes.as_ref()).map_err(internal_error) + } +} #[derive(Clone, Debug, Default, ProtoBuf)] pub struct SelectOptionCellChangesetPayload { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs similarity index 84% rename from frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs index 6094f8567b..8aae4bf677 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -1,7 +1,7 @@ use crate::entities::FieldType; use crate::impl_type_option; -use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData}; -use crate::services::field::select_option::{ +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{ make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds, SelectOptionOperation, }; @@ -24,8 +24,10 @@ pub struct SingleSelectTypeOption { impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect); impl SelectOptionOperation for SingleSelectTypeOption { - fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData { - let select_options = make_selected_select_options(any_cell_data, &self.options); + fn selected_select_option(&self, cell_data: CellData) -> SelectOptionCellData { + let mut select_options = make_selected_select_options(cell_data, &self.options); + // only keep option in single select + select_options.truncate(1); SelectOptionCellData { options: self.options.clone(), select_options, @@ -46,24 +48,13 @@ impl CellDataOperation for SingleSel &self, cell_data: CellData, decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { + field_rev: &FieldRevision, + ) -> FlowyResult { if !decoded_field_type.is_select_option() { - return Ok(DecodedCellData::default()); + return Ok(CellBytes::default()); } - let ids: SelectOptionIds = cell_data.try_into_inner()?; - let mut cell_data = SelectOptionCellData { - options: self.options.clone(), - select_options: vec![], - }; - if let Some(option_id) = ids.first() { - if let Some(option) = self.options.iter().find(|option| &option.id == option_id) { - cell_data.select_options.push(option.clone()); - } - } - - DecodedCellData::try_from_bytes(cell_data) + self.display_data(cell_data, decoded_field_type, field_rev) } fn apply_changeset( @@ -111,7 +102,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder { mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; - use crate::services::field::select_option::*; + use crate::services::field::type_options::*; use crate::services::field::FieldBuilder; use flowy_grid_data_model::revision::FieldRevision; @@ -171,7 +162,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &field_type, field_rev) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, ); diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/mod.rs new file mode 100644 index 0000000000..c7e518f103 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/mod.rs @@ -0,0 +1,3 @@ +#![allow(clippy::module_inception)] +mod text_type_option; +pub use text_type_option::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs similarity index 79% rename from frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs index a23366e2a4..2716206dfd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/text_type_option/text_type_option.rs @@ -1,7 +1,8 @@ use crate::entities::FieldType; use crate::impl_type_option; use crate::services::cell::{ - try_decode_cell_data, AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData, + try_decode_cell_data, CellBytes, CellBytesParser, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, + FromCellString, }; use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use bytes::Bytes; @@ -32,13 +33,25 @@ pub struct RichTextTypeOption { } impl_type_option!(RichTextTypeOption, FieldType::RichText); +impl CellDisplayable for RichTextTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_str: String = cell_data.try_into_inner()?; + Ok(CellBytes::new(cell_str)) + } +} + impl CellDataOperation for RichTextTypeOption { fn decode_cell_data( &self, cell_data: CellData, decoded_field_type: &FieldType, field_rev: &FieldRevision, - ) -> FlowyResult { + ) -> FlowyResult { if decoded_field_type.is_date() || decoded_field_type.is_single_select() || decoded_field_type.is_multi_select() @@ -46,8 +59,7 @@ impl CellDataOperation for RichTextTypeOption { { try_decode_cell_data(cell_data, field_rev, decoded_field_type, decoded_field_type) } else { - let cell_data: String = cell_data.try_into_inner()?; - Ok(DecodedCellData::new(cell_data)) + self.display_data(cell_data, decoded_field_type, field_rev) } } @@ -72,11 +84,23 @@ impl AsRef for TextCellData { } } -impl std::convert::TryFrom for TextCellData { - type Error = FlowyError; +impl FromCellString for TextCellData { + fn from_cell_str(s: &str) -> FlowyResult + where + Self: Sized, + { + Ok(TextCellData(s.to_owned())) + } +} - fn try_from(value: AnyCellData) -> Result { - Ok(TextCellData(value.data)) +pub struct TextCellDataParser(); +impl CellBytesParser for TextCellDataParser { + type Object = TextCellData; + fn parse(&self, bytes: &Bytes) -> FlowyResult { + match String::from_utf8(bytes.to_vec()) { + Ok(s) => Ok(TextCellData(s)), + Err(_) => Ok(TextCellData("".to_owned())), + } } } @@ -84,7 +108,7 @@ impl std::convert::TryFrom for TextCellData { mod tests { use crate::entities::FieldType; use crate::services::cell::CellDataOperation; - use crate::services::field::select_option::*; + use crate::services::field::FieldBuilder; use crate::services::field::*; @@ -100,7 +124,7 @@ mod tests { type_option .decode_cell_data(1647251762.to_string().into(), &field_type, &date_time_field_rev) .unwrap() - .parse::() + .with_parser(DateCellDataParser()) .unwrap() .date, "Mar 14,2022".to_owned() @@ -120,7 +144,7 @@ mod tests { &single_select_field_rev ) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, vec![done_option], @@ -143,7 +167,7 @@ mod tests { type_option .decode_cell_data(cell_data.into(), &FieldType::MultiSelect, &multi_select_field_rev) .unwrap() - .parse::() + .with_parser(SelectOptionCellDataParser()) .unwrap() .select_options, vec![google_option, facebook_option] diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs deleted file mode 100644 index 74fc9fc26b..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::entities::FieldType; -use crate::impl_type_option; -use crate::services::cell::{ - AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData, FromCellString, -}; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; -use bytes::Bytes; -use fancy_regex::Regex; -use flowy_derive::ProtoBuf; -use flowy_error::{internal_error, FlowyError, FlowyResult}; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; - -#[derive(Default)] -pub struct URLTypeOptionBuilder(URLTypeOption); -impl_into_box_type_option_builder!(URLTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption); - -impl TypeOptionBuilder for URLTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::URL - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] -pub struct URLTypeOption { - #[pb(index = 1)] - data: String, //It's not used yet. -} -impl_type_option!(URLTypeOption, FieldType::URL); - -impl CellDataOperation for URLTypeOption { - fn decode_cell_data( - &self, - cell_data: CellData, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult { - if !decoded_field_type.is_url() { - return Ok(DecodedCellData::default()); - } - let cell_data: URLCellData = cell_data.try_into_inner()?; - DecodedCellData::try_from_bytes(cell_data) - } - - fn apply_changeset( - &self, - changeset: CellDataChangeset, - _cell_rev: Option, - ) -> Result { - let changeset = changeset.try_into_inner()?; - let mut url = "".to_string(); - if let Ok(Some(m)) = URL_REGEX.find(&changeset) { - url = auto_append_scheme(m.as_str()); - } - URLCellData { - url, - content: changeset, - } - .to_json() - } -} - -fn auto_append_scheme(s: &str) -> String { - // Only support https scheme by now - match url::Url::parse(s) { - Ok(url) => { - if url.scheme() == "https" { - url.into() - } else { - format!("https://{}", s) - } - } - Err(_) => { - format!("https://{}", s) - } - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct URLCellData { - #[pb(index = 1)] - pub url: String, - - #[pb(index = 2)] - pub content: String, -} - -impl URLCellData { - pub fn new(s: &str) -> Self { - Self { - url: "".to_string(), - content: s.to_string(), - } - } - - fn to_json(&self) -> FlowyResult { - serde_json::to_string(self).map_err(internal_error) - } -} - -impl FromCellString for URLCellData { - fn from_cell_str(s: &str) -> FlowyResult { - serde_json::from_str::(s).map_err(internal_error) - } -} - -impl std::convert::TryFrom for URLCellData { - type Error = FlowyError; - - fn try_from(data: AnyCellData) -> Result { - serde_json::from_str::(&data.data).map_err(internal_error) - } -} - -lazy_static! { - static ref URL_REGEX: Regex = Regex::new( - "[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" - ) - .unwrap(); -} - -#[cfg(test)] -mod tests { - use crate::entities::FieldType; - use crate::services::cell::{CellData, CellDataOperation}; - use crate::services::field::FieldBuilder; - use crate::services::field::{URLCellData, URLTypeOption}; - use flowy_grid_data_model::revision::FieldRevision; - - #[test] - fn url_type_option_test_no_url() { - let type_option = URLTypeOption::default(); - let field_type = FieldType::URL; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset(&type_option, "123", &field_type, &field_rev, "123", ""); - } - - #[test] - fn url_type_option_test_contains_url() { - let type_option = URLTypeOption::default(); - let field_type = FieldType::URL; - let field_rev = FieldBuilder::from_field_type(&field_type).build(); - assert_changeset( - &type_option, - "AppFlowy website - https://www.appflowy.io", - &field_type, - &field_rev, - "AppFlowy website - https://www.appflowy.io", - "https://www.appflowy.io/", - ); - - assert_changeset( - &type_option, - "AppFlowy website appflowy.io", - &field_type, - &field_rev, - "AppFlowy website appflowy.io", - "https://appflowy.io", - ); - } - - fn assert_changeset( - type_option: &URLTypeOption, - cell_data: &str, - field_type: &FieldType, - field_rev: &FieldRevision, - expected: &str, - expected_url: &str, - ) { - let encoded_data = type_option.apply_changeset(cell_data.to_owned().into(), None).unwrap(); - let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type); - assert_eq!(expected.to_owned(), decode_cell_data.content); - assert_eq!(expected_url.to_owned(), decode_cell_data.url); - } - - fn decode_cell_data>>( - encoded_data: T, - type_option: &URLTypeOption, - field_rev: &FieldRevision, - field_type: &FieldType, - ) -> URLCellData { - type_option - .decode_cell_data(encoded_data.into(), field_type, field_rev) - .unwrap() - .parse::() - .unwrap() - } -} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/mod.rs new file mode 100644 index 0000000000..8f6cb884df --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/mod.rs @@ -0,0 +1,7 @@ +#![allow(clippy::module_inception)] +mod url_tests; +mod url_type_option; +mod url_type_option_entities; + +pub use url_type_option::*; +pub use url_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs new file mode 100644 index 0000000000..6f74bdb4d4 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_tests.rs @@ -0,0 +1,67 @@ +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::cell::{CellData, CellDataOperation}; + use crate::services::field::{FieldBuilder, URLCellDataParser}; + use crate::services::field::{URLCellData, URLTypeOption}; + use flowy_grid_data_model::revision::FieldRevision; + + #[test] + fn url_type_option_test_no_url() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_changeset(&type_option, "123", &field_type, &field_rev, "123", ""); + } + + #[test] + fn url_type_option_test_contains_url() { + let type_option = URLTypeOption::default(); + let field_type = FieldType::URL; + let field_rev = FieldBuilder::from_field_type(&field_type).build(); + assert_changeset( + &type_option, + "AppFlowy website - https://www.appflowy.io", + &field_type, + &field_rev, + "AppFlowy website - https://www.appflowy.io", + "https://www.appflowy.io/", + ); + + assert_changeset( + &type_option, + "AppFlowy website appflowy.io", + &field_type, + &field_rev, + "AppFlowy website appflowy.io", + "https://appflowy.io", + ); + } + + fn assert_changeset( + type_option: &URLTypeOption, + cell_data: &str, + field_type: &FieldType, + field_rev: &FieldRevision, + expected: &str, + expected_url: &str, + ) { + let encoded_data = type_option.apply_changeset(cell_data.to_owned().into(), None).unwrap(); + let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type); + assert_eq!(expected.to_owned(), decode_cell_data.content); + assert_eq!(expected_url.to_owned(), decode_cell_data.url); + } + + fn decode_cell_data>>( + encoded_data: T, + type_option: &URLTypeOption, + field_rev: &FieldRevision, + field_type: &FieldType, + ) -> URLCellData { + type_option + .decode_cell_data(encoded_data.into(), field_type, field_rev) + .unwrap() + .with_parser(URLCellDataParser()) + .unwrap() + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs new file mode 100644 index 0000000000..0d4ceb5caf --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs @@ -0,0 +1,95 @@ +use crate::entities::FieldType; +use crate::impl_type_option; +use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable}; +use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder, URLCellData}; +use bytes::Bytes; +use fancy_regex::Regex; +use flowy_derive::ProtoBuf; +use flowy_error::{FlowyError, FlowyResult}; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; + +#[derive(Default)] +pub struct URLTypeOptionBuilder(URLTypeOption); +impl_into_box_type_option_builder!(URLTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption); + +impl TypeOptionBuilder for URLTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::URL + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)] +pub struct URLTypeOption { + #[pb(index = 1)] + data: String, //It's not used yet. +} +impl_type_option!(URLTypeOption, FieldType::URL); + +impl CellDisplayable for URLTypeOption { + fn display_data( + &self, + cell_data: CellData, + _decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult { + let cell_data: URLCellData = cell_data.try_into_inner()?; + CellBytes::from(cell_data) + } +} + +impl CellDataOperation for URLTypeOption { + fn decode_cell_data( + &self, + cell_data: CellData, + decoded_field_type: &FieldType, + field_rev: &FieldRevision, + ) -> FlowyResult { + if !decoded_field_type.is_url() { + return Ok(CellBytes::default()); + } + self.display_data(cell_data, decoded_field_type, field_rev) + } + + fn apply_changeset( + &self, + changeset: CellDataChangeset, + _cell_rev: Option, + ) -> Result { + let content = changeset.try_into_inner()?; + let mut url = "".to_string(); + if let Ok(Some(m)) = URL_REGEX.find(&content) { + url = auto_append_scheme(m.as_str()); + } + URLCellData { url, content }.to_json() + } +} + +fn auto_append_scheme(s: &str) -> String { + // Only support https scheme by now + match url::Url::parse(s) { + Ok(url) => { + if url.scheme() == "https" { + url.into() + } else { + format!("https://{}", s) + } + } + Err(_) => { + format!("https://{}", s) + } + } +} + +lazy_static! { + static ref URL_REGEX: Regex = Regex::new( + "[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" + ) + .unwrap(); +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs new file mode 100644 index 0000000000..ddb84cf9a7 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option_entities.rs @@ -0,0 +1,42 @@ +use crate::services::cell::{CellBytesParser, FromCellString}; +use bytes::Bytes; +use flowy_derive::ProtoBuf; +use flowy_error::{internal_error, FlowyResult}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct URLCellData { + #[pb(index = 1)] + pub url: String, + + #[pb(index = 2)] + pub content: String, +} + +impl URLCellData { + pub fn new(s: &str) -> Self { + Self { + url: "".to_string(), + content: s.to_string(), + } + } + + pub(crate) fn to_json(&self) -> FlowyResult { + serde_json::to_string(self).map_err(internal_error) + } +} + +pub struct URLCellDataParser(); +impl CellBytesParser for URLCellDataParser { + type Object = URLCellData; + + fn parse(&self, bytes: &Bytes) -> FlowyResult { + URLCellData::try_from(bytes.as_ref()).map_err(internal_error) + } +} + +impl FromCellString for URLCellData { + fn from_cell_str(s: &str) -> FlowyResult { + serde_json::from_str::(s).map_err(internal_error) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs index c7aa386fe9..dfadd1c5a0 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs @@ -1,4 +1,3 @@ mod cell_data_util; -pub use crate::services::field::select_option::*; pub use cell_data_util::*; diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs index c5cf95d34b..24e21bbeb7 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/checkbox_filter.rs @@ -1,5 +1,5 @@ use crate::entities::{CheckboxCondition, GridCheckboxFilter}; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{CheckboxCellData, CheckboxTypeOption}; use flowy_error::FlowyResult; @@ -18,7 +18,8 @@ impl CellFilterOperation for CheckboxTypeOption { if !any_cell_data.is_checkbox() { return Ok(true); } - let checkbox_cell_data: CheckboxCellData = any_cell_data.try_into()?; + let cell_data: CellData = any_cell_data.into(); + let checkbox_cell_data = cell_data.try_into_inner()?; Ok(filter.is_visible(&checkbox_cell_data)) } } @@ -27,6 +28,7 @@ impl CellFilterOperation for CheckboxTypeOption { mod tests { use crate::entities::{CheckboxCondition, GridCheckboxFilter}; use crate::services::field::CheckboxCellData; + use std::str::FromStr; #[test] fn checkbox_filter_is_check_test() { @@ -34,7 +36,7 @@ mod tests { condition: CheckboxCondition::IsChecked, }; for (value, visible) in [("true", true), ("yes", true), ("false", false), ("no", false)] { - let data = CheckboxCellData(value.to_owned()); + let data = CheckboxCellData::from_str(value).unwrap(); assert_eq!(checkbox_filter.is_visible(&data), visible); } } @@ -45,7 +47,7 @@ mod tests { condition: CheckboxCondition::IsUnChecked, }; for (value, visible) in [("false", true), ("no", true), ("true", false), ("yes", false)] { - let data = CheckboxCellData(value.to_owned()); + let data = CheckboxCellData::from_str(value).unwrap(); assert_eq!(checkbox_filter.is_visible(&data), visible); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs index ae920c4edb..46e2571438 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/date_filter.rs @@ -1,5 +1,5 @@ use crate::entities::{DateFilterCondition, GridDateFilter}; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{DateTimestamp, DateTypeOption}; use flowy_error::FlowyResult; @@ -34,7 +34,8 @@ impl CellFilterOperation for DateTypeOption { if !any_cell_data.is_date() { return Ok(true); } - let timestamp: DateTimestamp = any_cell_data.into(); + let cell_data: CellData = any_cell_data.into(); + let timestamp = cell_data.try_into_inner()?; Ok(filter.is_visible(timestamp)) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs index 7f3a5dd212..f44c1d2d62 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/number_filter.rs @@ -47,9 +47,7 @@ impl CellFilterOperation for NumberTypeOption { #[cfg(test)] mod tests { use crate::entities::{GridNumberFilter, NumberFilterCondition}; - use crate::services::field::{NumberCellData, NumberFormat}; - use std::str::FromStr; #[test] fn number_filter_equal_test() { let number_filter = GridNumberFilter { @@ -58,7 +56,7 @@ mod tests { }; for (num_str, visible) in [("123", true), ("1234", false), ("", false)] { - let data = NumberCellData::from_str(num_str).unwrap(); + let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); assert_eq!(number_filter.is_visible(&data), visible); } @@ -75,7 +73,7 @@ mod tests { content: Some("12".to_owned()), }; for (num_str, visible) in [("123", true), ("10", false), ("30", true), ("", false)] { - let data = NumberCellData::from_str(num_str).unwrap(); + let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); assert_eq!(number_filter.is_visible(&data), visible); } } @@ -87,7 +85,7 @@ mod tests { content: Some("100".to_owned()), }; for (num_str, visible) in [("12", true), ("1234", false), ("30", true), ("", true)] { - let data = NumberCellData::from_str(num_str).unwrap(); + let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap(); assert_eq!(number_filter.is_visible(&data), visible); } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs index cd25bfef0e..82d5ab6a0b 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/select_option_filter.rs @@ -2,8 +2,8 @@ use crate::entities::{GridSelectOptionFilter, SelectOptionCondition}; use crate::services::cell::{AnyCellData, CellFilterOperation}; -use crate::services::field::select_option::{SelectOptionOperation, SelectedSelectOptions}; use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; +use crate::services::field::{SelectOptionOperation, SelectedSelectOptions}; use flowy_error::FlowyResult; impl GridSelectOptionFilter { @@ -45,7 +45,7 @@ impl CellFilterOperation for MultiSelectTypeOption { return Ok(true); } - let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data)); + let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data.into())); Ok(filter.is_visible(&selected_options)) } } @@ -55,7 +55,7 @@ impl CellFilterOperation for SingleSelectTypeOption { if !any_cell_data.is_single_select() { return Ok(true); } - let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data)); + let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data.into())); Ok(filter.is_visible(&selected_options)) } } @@ -64,7 +64,7 @@ impl CellFilterOperation for SingleSelectTypeOption { mod tests { #![allow(clippy::all)] use crate::entities::{GridSelectOptionFilter, SelectOptionCondition}; - use crate::services::field::select_option::{SelectOption, SelectedSelectOptions}; + use crate::services::field::selection_type_option::{SelectOption, SelectedSelectOptions}; #[test] fn select_option_filter_is_test() { diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs index 45165e35e2..25f3902ceb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/text_filter.rs @@ -1,5 +1,5 @@ use crate::entities::{GridTextFilter, TextFilterCondition}; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{RichTextTypeOption, TextCellData}; use flowy_error::FlowyResult; @@ -30,7 +30,8 @@ impl CellFilterOperation for RichTextTypeOption { return Ok(true); } - let text_cell_data: TextCellData = any_cell_data.try_into()?; + let cell_data: CellData = any_cell_data.into(); + let text_cell_data = cell_data.try_into_inner()?; Ok(filter.is_visible(text_cell_data)) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs b/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs index 50f9c815fc..15254d4713 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/impls/url_filter.rs @@ -1,5 +1,5 @@ use crate::entities::GridTextFilter; -use crate::services::cell::{AnyCellData, CellFilterOperation}; +use crate::services::cell::{AnyCellData, CellData, CellFilterOperation}; use crate::services::field::{TextCellData, URLTypeOption}; use flowy_error::FlowyResult; @@ -9,7 +9,8 @@ impl CellFilterOperation for URLTypeOption { return Ok(true); } - let text_cell_data: TextCellData = any_cell_data.try_into()?; + let cell_data: CellData = any_cell_data.into(); + let text_cell_data = cell_data.try_into_inner()?; Ok(filter.is_visible(&text_cell_data)) } } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index 1d63b6dd69..413f4812de 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -3,13 +3,12 @@ use crate::entities::CellIdentifier; use crate::entities::*; use crate::manager::{GridTaskSchedulerRwLock, GridUser}; use crate::services::block_manager::GridBlockManager; -use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data}; +use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, CellBytes}; use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder}; use crate::services::filter::{GridFilterChangeset, GridFilterService}; use crate::services::persistence::block_index::BlockIndexCache; use crate::services::row::{ - make_grid_blocks, make_row_from_row_rev, make_row_rev_from_context, make_rows_from_row_revs, - CreateRowRevisionBuilder, CreateRowRevisionPayload, GridBlockSnapshot, + make_grid_blocks, make_row_from_row_rev, make_rows_from_row_revs, GridBlockSnapshot, RowRevisionBuilder, }; use crate::services::setting::make_grid_setting; use bytes::Bytes; @@ -274,8 +273,7 @@ impl GridRevisionEditor { let block_id = self.block_id().await?; // insert empty row below the row whose id is upper_row_id - let row_rev_ctx = CreateRowRevisionBuilder::new(&field_revs).build(); - let row_rev = make_row_rev_from_context(&block_id, row_rev_ctx); + let row_rev = RowRevisionBuilder::new(&field_revs).build(&block_id); let row_order = RowInfo::from(&row_rev); // insert the row @@ -287,12 +285,11 @@ impl GridRevisionEditor { Ok(row_order) } - pub async fn insert_rows(&self, contexts: Vec) -> FlowyResult> { + pub async fn insert_rows(&self, row_revs: Vec) -> FlowyResult> { let block_id = self.block_id().await?; let mut rows_by_block_id: HashMap> = HashMap::new(); let mut row_orders = vec![]; - for ctx in contexts { - let row_rev = make_row_rev_from_context(&block_id, ctx); + for row_rev in row_revs { row_orders.push(RowInfo::from(&row_rev)); rows_by_block_id .entry(block_id.clone()) @@ -307,10 +304,7 @@ impl GridRevisionEditor { } pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> { - let field_revs = self.get_field_revs(None).await?; - self.block_manager - .update_row(changeset, |row_rev| make_row_from_row_rev(&field_revs, row_rev)) - .await + self.block_manager.update_row(changeset, make_row_from_row_rev).await } pub async fn get_rows(&self, block_id: &str) -> FlowyResult { @@ -322,26 +316,20 @@ impl GridRevisionEditor { debug_assert_eq!(grid_block_snapshot.len(), 1); if grid_block_snapshot.len() == 1 { let snapshot = grid_block_snapshot.pop().unwrap(); - let field_revs = self.get_field_revs(None).await?; - let rows = make_rows_from_row_revs(&field_revs, &snapshot.row_revs); + let rows = make_rows_from_row_revs(&snapshot.row_revs); Ok(rows.into()) } else { Ok(vec![].into()) } } - pub async fn get_row(&self, row_id: &str) -> FlowyResult> { + pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult>> { match self.block_manager.get_row_rev(row_id).await? { None => Ok(None), - Some(row_rev) => { - let field_revs = self.get_field_revs(None).await?; - let row_revs = vec![row_rev]; - let mut rows = make_rows_from_row_revs(&field_revs, &row_revs); - debug_assert!(rows.len() == 1); - Ok(rows.pop()) - } + Some(row_rev) => Ok(Some(row_rev)), } } + pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> { let _ = self.block_manager.delete_row(row_id).await?; Ok(()) @@ -352,12 +340,16 @@ impl GridRevisionEditor { } pub async fn get_cell(&self, params: &CellIdentifier) -> Option { + let cell_bytes = self.get_cell_bytes(params).await?; + Some(Cell::new(¶ms.field_id, cell_bytes.to_vec())) + } + + pub async fn get_cell_bytes(&self, params: &CellIdentifier) -> Option { let field_rev = self.get_field_rev(¶ms.field_id).await?; let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??; let cell_rev = row_rev.cells.get(¶ms.field_id)?.clone(); - let data = decode_any_cell_data(cell_rev.data, &field_rev).data; - Some(Cell::new(¶ms.field_id, data)) + Some(decode_any_cell_data(cell_rev.data, &field_rev)) } pub async fn get_cell_rev(&self, row_id: &str, field_id: &str) -> FlowyResult> { @@ -395,7 +387,6 @@ impl GridRevisionEditor { let cell_rev = self.get_cell_rev(&row_id, &field_id).await?; // Update the changeset.data property with the return value. content = Some(apply_cell_data_changeset(content.unwrap(), cell_rev, field_rev)?); - let field_revs = self.get_field_revs(None).await?; let cell_changeset = CellChangeset { grid_id, row_id, @@ -404,7 +395,7 @@ impl GridRevisionEditor { }; let _ = self .block_manager - .update_cell(cell_changeset, |row_rev| make_row_from_row_rev(&field_revs, row_rev)) + .update_cell(cell_changeset, make_row_from_row_rev) .await?; Ok(()) } @@ -561,7 +552,7 @@ impl GridRevisionEditor { drop(grid_pad); Ok(BuildGridContext { - field_revs: duplicated_fields, + field_revs: duplicated_fields.into_iter().map(Arc::new).collect(), blocks: duplicated_blocks, blocks_meta_data, }) diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs index 3302276cd8..43153f1311 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_builder.rs @@ -1,22 +1,22 @@ use crate::services::cell::apply_cell_data_changeset; -use crate::services::field::select_option::SelectOptionCellChangeset; +use crate::services::field::SelectOptionCellChangeset; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; use indexmap::IndexMap; use std::collections::HashMap; use std::sync::Arc; -pub struct CreateRowRevisionBuilder<'a> { - field_rev_map: HashMap<&'a String, &'a Arc>, +pub struct RowRevisionBuilder<'a> { + field_rev_map: HashMap<&'a String, Arc>, payload: CreateRowRevisionPayload, } -impl<'a> CreateRowRevisionBuilder<'a> { +impl<'a> RowRevisionBuilder<'a> { pub fn new(fields: &'a [Arc]) -> Self { let field_rev_map = fields .iter() - .map(|field| (&field.id, field)) - .collect::>>(); + .map(|field| (&field.id, field.clone())) + .collect::>>(); let payload = CreateRowRevisionPayload { row_id: gen_row_id(), @@ -28,10 +28,10 @@ impl<'a> CreateRowRevisionBuilder<'a> { Self { field_rev_map, payload } } - pub fn add_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { + pub fn insert_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { match self.field_rev_map.get(&field_id.to_owned()) { None => { - let msg = format!("Invalid field_id: {}", field_id); + let msg = format!("Can't find the field with id: {}", field_id); Err(FlowyError::internal().context(msg)) } Some(field_rev) => { @@ -43,7 +43,7 @@ impl<'a> CreateRowRevisionBuilder<'a> { } } - pub fn add_select_option_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { + pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> { match self.field_rev_map.get(&field_id.to_owned()) { None => { let msg = format!("Invalid field_id: {}", field_id); @@ -71,18 +71,14 @@ impl<'a> CreateRowRevisionBuilder<'a> { self } - pub fn build(self) -> CreateRowRevisionPayload { - self.payload - } -} - -pub fn make_row_rev_from_context(block_id: &str, payload: CreateRowRevisionPayload) -> RowRevision { - RowRevision { - id: payload.row_id, - block_id: block_id.to_owned(), - cells: payload.cell_by_field_id, - height: payload.height, - visibility: payload.visibility, + pub fn build(self, block_id: &str) -> RowRevision { + RowRevision { + id: self.payload.row_id, + block_id: block_id.to_owned(), + cells: self.payload.cell_by_field_id, + height: self.payload.height, + visibility: self.payload.visibility, + } } } diff --git a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs index 1b9ce80101..81d9420d88 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/row_loader.rs @@ -1,6 +1,6 @@ use crate::entities::{GridBlock, RepeatedGridBlock, Row, RowInfo}; use flowy_error::FlowyResult; -use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; +use flowy_grid_data_model::revision::RowRevision; use std::collections::HashMap; use std::sync::Arc; @@ -39,28 +39,14 @@ pub(crate) fn make_row_orders_from_row_revs(row_revs: &[Arc]) -> Ve row_revs.iter().map(RowInfo::from).collect::>() } -pub(crate) fn make_row_from_row_rev(fields: &[Arc], row_rev: Arc) -> Option { - make_rows_from_row_revs(fields, &[row_rev]).pop() +pub(crate) fn make_row_from_row_rev(row_rev: Arc) -> Option { + make_rows_from_row_revs(&[row_rev]).pop() } -pub(crate) fn make_rows_from_row_revs(_fields: &[Arc], row_revs: &[Arc]) -> Vec { - // let field_rev_map = fields - // .iter() - // .map(|field_rev| (&field_rev.id, field_rev)) - // .collect::>(); - - let make_row = |row_rev: &Arc| { - // let cell_by_field_id = row_rev - // .cells - // .clone() - // .into_iter() - // .flat_map(|(field_id, cell_rev)| make_cell_by_field_id(&field_rev_map, field_id, cell_rev)) - // .collect::>(); - - Row { - id: row_rev.id.clone(), - height: row_rev.height, - } +pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc]) -> Vec { + let make_row = |row_rev: &Arc| Row { + id: row_rev.id.clone(), + height: row_rev.height, }; row_revs.iter().map(make_row).collect::>() diff --git a/frontend/rust-lib/flowy-grid/src/util.rs b/frontend/rust-lib/flowy-grid/src/util.rs index e0055d09b6..3b48e313a9 100644 --- a/frontend/rust-lib/flowy-grid/src/util.rs +++ b/frontend/rust-lib/flowy-grid/src/util.rs @@ -4,29 +4,29 @@ use flowy_grid_data_model::revision::BuildGridContext; use flowy_sync::client_grid::GridBuilder; pub fn make_default_grid() -> BuildGridContext { + let mut grid_builder = GridBuilder::new(); // text let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) .name("Name") .visibility(true) .primary(true) .build(); + grid_builder.add_field(text_field); // single select let single_select = SingleSelectTypeOptionBuilder::default(); let single_select_field = FieldBuilder::new(single_select).name("Type").visibility(true).build(); + grid_builder.add_field(single_select_field); // checkbox let checkbox_field = FieldBuilder::from_field_type(&FieldType::Checkbox) .name("Done") .visibility(true) .build(); + grid_builder.add_field(checkbox_field); - GridBuilder::default() - .add_field(text_field) - .add_field(single_select_field) - .add_field(checkbox_field) - .add_empty_row() - .add_empty_row() - .add_empty_row() - .build() + grid_builder.add_empty_row(); + grid_builder.add_empty_row(); + grid_builder.add_empty_row(); + grid_builder.build() } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs index 96c23611b0..7b64a44b48 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/row_test.rs @@ -1,26 +1,21 @@ -use crate::grid::block_test::script::GridRowTest; use crate::grid::block_test::script::RowScript::*; -use crate::grid::block_test::util::GridRowTestBuilder; -use chrono::NaiveDateTime; +use crate::grid::block_test::script::{CreateRowScriptBuilder, GridRowTest}; +use crate::grid::grid_editor::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER}; use flowy_grid::entities::FieldType; -use flowy_grid::services::cell::decode_any_cell_data; -use flowy_grid::services::field::select_option::SELECTION_IDS_SEPARATOR; -use flowy_grid::services::field::{DateCellData, MultiSelectTypeOption, SingleSelectTypeOption}; - -use crate::grid::field_test::util::make_date_cell_string; +use flowy_grid::services::field::{NO, SELECTION_IDS_SEPARATOR}; use flowy_grid_data_model::revision::RowMetaChangeset; #[tokio::test] async fn grid_create_row_count_test() { let mut test = GridRowTest::new().await; let scripts = vec![ - AssertRowCount(3), + AssertRowCount(5), CreateEmptyRow, CreateEmptyRow, CreateRow { - payload: GridRowTestBuilder::new(&test).build(), + row_rev: test.row_builder().build(), }, - AssertRowCount(6), + AssertRowCount(8), ]; test.run_scripts(scripts).await; } @@ -28,42 +23,42 @@ async fn grid_create_row_count_test() { #[tokio::test] async fn grid_update_row() { let mut test = GridRowTest::new().await; - let payload = GridRowTestBuilder::new(&test).build(); + let row_rev = test.row_builder().build(); let changeset = RowMetaChangeset { - row_id: payload.row_id.clone(), + row_id: row_rev.id.clone(), height: None, visibility: None, cell_by_field_id: Default::default(), }; - let scripts = vec![AssertRowCount(3), CreateRow { payload }, UpdateRow { changeset }]; + let scripts = vec![AssertRowCount(5), CreateRow { row_rev }, UpdateRow { changeset }]; test.run_scripts(scripts).await; let expected_row = test.last_row().unwrap(); - let scripts = vec![AssertRow { expected_row }, AssertRowCount(4)]; + let scripts = vec![AssertRow { expected_row }, AssertRowCount(6)]; test.run_scripts(scripts).await; } #[tokio::test] async fn grid_delete_row() { let mut test = GridRowTest::new().await; - let payload1 = GridRowTestBuilder::new(&test).build(); - let payload2 = GridRowTestBuilder::new(&test).build(); - let row_ids = vec![payload1.row_id.clone(), payload2.row_id.clone()]; + let row_1 = test.row_builder().build(); + let row_2 = test.row_builder().build(); + let row_ids = vec![row_1.id.clone(), row_2.id.clone()]; let scripts = vec![ - AssertRowCount(3), - CreateRow { payload: payload1 }, - CreateRow { payload: payload2 }, + AssertRowCount(5), + CreateRow { row_rev: row_1 }, + CreateRow { row_rev: row_2 }, AssertBlockCount(1), AssertBlock { block_index: 0, - row_count: 5, + row_count: 7, start_row_index: 0, }, DeleteRows { row_ids }, AssertBlock { block_index: 0, - row_count: 3, + row_count: 5, start_row_index: 0, }, ]; @@ -73,78 +68,68 @@ async fn grid_delete_row() { #[tokio::test] async fn grid_row_add_cells_test() { let mut test = GridRowTest::new().await; - let mut builder = test.builder(); - for field in test.field_revs() { - let field_type: FieldType = field.field_type_rev.into(); - match field_type { - FieldType::RichText => { - builder.add_cell(&field.id, "hello world".to_owned()).unwrap(); - } - FieldType::Number => { - builder.add_cell(&field.id, "18,443".to_owned()).unwrap(); - } - FieldType::DateTime => { - builder - .add_cell(&field.id, make_date_cell_string("1647251762")) - .unwrap(); - } - FieldType::SingleSelect => { - let type_option = SingleSelectTypeOption::from(field); - let option = type_option.options.first().unwrap(); - builder.add_select_option_cell(&field.id, option.id.clone()).unwrap(); - } - FieldType::MultiSelect => { - let type_option = MultiSelectTypeOption::from(field); - let ops_ids = type_option - .options - .iter() - .map(|option| option.id.clone()) - .collect::>() - .join(SELECTION_IDS_SEPARATOR); - builder.add_select_option_cell(&field.id, ops_ids).unwrap(); - } - FieldType::Checkbox => { - builder.add_cell(&field.id, "false".to_string()).unwrap(); - } - FieldType::URL => { - builder.add_cell(&field.id, "1".to_string()).unwrap(); - } - } - } - let context = builder.build(); - let scripts = vec![CreateRow { payload: context }]; + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert(FieldType::RichText, "hello world", "hello world"); + builder.insert(FieldType::DateTime, "1647251762", "2022/03/14"); + builder.insert(FieldType::Number, "18,443", "$18,443.00"); + builder.insert(FieldType::Checkbox, "false", NO); + builder.insert(FieldType::URL, "https://appflowy.io", "https://appflowy.io"); + builder.insert_single_select_cell(|mut options| options.remove(0), COMPLETED); + builder.insert_multi_select_cell( + |options| options, + &vec![GOOGLE, FACEBOOK, TWITTER].join(SELECTION_IDS_SEPARATOR), + ); + let scripts = builder.build(); test.run_scripts(scripts).await; } #[tokio::test] -async fn grid_row_add_date_cell_test() { +async fn grid_row_insert_number_test() { let mut test = GridRowTest::new().await; - let mut builder = test.builder(); - let mut date_field = None; - let timestamp = 1647390674; - for field in test.field_revs() { - let field_type: FieldType = field.field_type_rev.into(); - if field_type == FieldType::DateTime { - date_field = Some(field.clone()); - NaiveDateTime::from_timestamp(123, 0); - // The data should not be empty - assert!(builder.add_cell(&field.id, "".to_string()).is_err()); - assert!(builder.add_cell(&field.id, make_date_cell_string("123")).is_ok()); - assert!(builder - .add_cell(&field.id, make_date_cell_string(×tamp.to_string())) - .is_ok()); - } + for (val, expected) in &[("1647251762", "2022/03/14"), ("2022/03/14", ""), ("", "")] { + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert(FieldType::DateTime, val, expected); + let scripts = builder.build(); + test.run_scripts(scripts).await; } - let context = builder.build(); - let date_field = date_field.unwrap(); - let cell_rev = context.cell_by_field_id.get(&date_field.id).unwrap(); - assert_eq!( - decode_any_cell_data(cell_rev, &date_field) - .parse::() - .unwrap() - .date, - "2022/03/16", - ); - let scripts = vec![CreateRow { payload: context }]; +} + +#[tokio::test] +async fn grid_row_insert_date_test() { + let mut test = GridRowTest::new().await; + for (val, expected) in &[ + ("18,443", "$18,443.00"), + ("0", "$0.00"), + ("100000", "$100,000.00"), + ("$100,000.00", "$100,000.00"), + ("", ""), + ] { + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert(FieldType::Number, val, expected); + let scripts = builder.build(); + test.run_scripts(scripts).await; + } +} +#[tokio::test] +async fn grid_row_insert_single_select_test() { + let mut test = GridRowTest::new().await; + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert_single_select_cell(|mut options| options.pop().unwrap(), PAUSED); + let scripts = builder.build(); + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_row_insert_multi_select_test() { + let mut test = GridRowTest::new().await; + let mut builder = CreateRowScriptBuilder::new(&test); + builder.insert_multi_select_cell( + |mut options| { + options.remove(0); + options + }, + &vec![FACEBOOK, TWITTER].join(SELECTION_IDS_SEPARATOR), + ); + let scripts = builder.build(); test.run_scripts(scripts).await; } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs index 288133958b..015dcfee88 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/script.rs @@ -1,15 +1,19 @@ +use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow}; +use crate::grid::block_test::util::GridRowTestBuilder; use crate::grid::grid_editor::GridEditorTest; -use flowy_grid::entities::RowInfo; -use flowy_grid::services::row::{CreateRowRevisionBuilder, CreateRowRevisionPayload}; +use flowy_grid::entities::{CellIdentifier, FieldType, RowInfo}; +use flowy_grid::services::field::*; use flowy_grid_data_model::revision::{ - FieldRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, + GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision, }; +use std::collections::HashMap; use std::sync::Arc; +use strum::IntoEnumIterator; pub enum RowScript { CreateEmptyRow, CreateRow { - payload: CreateRowRevisionPayload, + row_rev: RowRevision, }, UpdateRow { changeset: RowMetaChangeset, @@ -20,6 +24,12 @@ pub enum RowScript { DeleteRows { row_ids: Vec, }, + AssertCell { + row_id: String, + field_id: String, + field_type: FieldType, + expected: String, + }, AssertRowCount(usize), CreateBlock { block: GridBlockMetaRevision, @@ -49,10 +59,6 @@ impl GridRowTest { Self { inner: editor_test } } - pub fn field_revs(&self) -> &Vec> { - &self.field_revs - } - pub fn last_row(&self) -> Option { self.row_revs.last().map(|a| a.clone().as_ref().clone()) } @@ -63,8 +69,8 @@ impl GridRowTest { } } - pub fn builder(&self) -> CreateRowRevisionBuilder { - CreateRowRevisionBuilder::new(&self.field_revs) + pub fn row_builder(&self) -> GridRowTestBuilder { + GridRowTestBuilder::new(self.block_id(), &self.field_revs) } pub async fn run_script(&mut self, script: RowScript) { @@ -76,8 +82,8 @@ impl GridRowTest { self.row_revs = self.get_row_revs().await; self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); } - RowScript::CreateRow { payload: context } => { - let row_orders = self.editor.insert_rows(vec![context]).await.unwrap(); + RowScript::CreateRow { row_rev } => { + let row_orders = self.editor.insert_rows(vec![row_rev]).await.unwrap(); for row_order in row_orders { self.row_order_by_row_id .insert(row_order.row_id().to_owned(), row_order); @@ -96,6 +102,19 @@ impl GridRowTest { self.row_revs = self.get_row_revs().await; self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap(); } + RowScript::AssertCell { + row_id, + field_id, + field_type, + expected, + } => { + let id = CellIdentifier { + grid_id: self.grid_id.clone(), + field_id, + row_id, + }; + self.compare_cell_content(id, field_type, expected).await; + } RowScript::AssertRow { expected_row } => { let row = &*self .row_revs @@ -133,6 +152,99 @@ impl GridRowTest { } } } + + async fn compare_cell_content(&self, cell_id: CellIdentifier, field_type: FieldType, expected: String) { + match field_type { + FieldType::RichText => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(TextCellDataParser()) + .unwrap(); + + assert_eq!(cell_data.as_ref(), &expected); + } + FieldType::Number => { + let field_rev = self.editor.get_field_rev(&cell_id.field_id).await.unwrap(); + let number_type_option = field_rev + .get_type_option_entry::(FieldType::Number.into()) + .unwrap(); + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(NumberCellDataParser(number_type_option.format)) + .unwrap(); + assert_eq!(cell_data.to_string(), expected); + } + FieldType::DateTime => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(DateCellDataParser()) + .unwrap(); + + assert_eq!(cell_data.date, expected); + } + FieldType::SingleSelect => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(SelectOptionCellDataParser()) + .unwrap(); + let select_option = cell_data.select_options.first().unwrap(); + assert_eq!(select_option.name, expected); + } + FieldType::MultiSelect => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(SelectOptionCellDataParser()) + .unwrap(); + + let s = cell_data + .select_options + .into_iter() + .map(|option| option.name) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + assert_eq!(s, expected); + } + + FieldType::Checkbox => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(CheckboxCellDataParser()) + .unwrap(); + assert_eq!(cell_data.to_string(), expected); + } + FieldType::URL => { + let cell_data = self + .editor + .get_cell_bytes(&cell_id) + .await + .unwrap() + .with_parser(URLCellDataParser()) + .unwrap(); + + assert_eq!(cell_data.content, expected); + // assert_eq!(cell_data.url, expected); + } + } + } } impl std::ops::Deref for GridRowTest { @@ -148,3 +260,113 @@ impl std::ops::DerefMut for GridRowTest { &mut self.inner } } + +pub struct CreateRowScriptBuilder<'a> { + builder: GridRowTestBuilder<'a>, + data_by_field_type: HashMap, + output_by_field_type: HashMap, +} + +impl<'a> CreateRowScriptBuilder<'a> { + pub fn new(test: &'a GridRowTest) -> Self { + Self { + builder: test.row_builder(), + data_by_field_type: HashMap::new(), + output_by_field_type: HashMap::new(), + } + } + + pub fn insert(&mut self, field_type: FieldType, input: &str, expected: &str) { + self.data_by_field_type.insert( + field_type, + CellTestData { + input: input.to_string(), + expected: expected.to_owned(), + }, + ); + } + + pub fn insert_single_select_cell(&mut self, f: F, expected: &str) + where + F: Fn(Vec) -> SelectOption, + { + let field_id = self.builder.insert_single_select_cell(f); + self.output_by_field_type.insert( + FieldType::SingleSelect, + CellTestOutput { + field_id, + expected: expected.to_owned(), + }, + ); + } + + pub fn insert_multi_select_cell(&mut self, f: F, expected: &str) + where + F: Fn(Vec) -> Vec, + { + let field_id = self.builder.insert_multi_select_cell(f); + self.output_by_field_type.insert( + FieldType::MultiSelect, + CellTestOutput { + field_id, + expected: expected.to_owned(), + }, + ); + } + + pub fn build(mut self) -> Vec { + let mut scripts = vec![]; + let output_by_field_type = &mut self.output_by_field_type; + + for field_type in FieldType::iter() { + let field_type: FieldType = field_type; + if let Some(data) = self.data_by_field_type.get(&field_type) { + let field_id = match field_type { + FieldType::RichText => self.builder.insert_text_cell(&data.input), + FieldType::Number => self.builder.insert_number_cell(&data.input), + FieldType::DateTime => self.builder.insert_date_cell(&data.input), + FieldType::Checkbox => self.builder.insert_checkbox_cell(&data.input), + FieldType::URL => self.builder.insert_url_cell(&data.input), + _ => "".to_owned(), + }; + + if !field_id.is_empty() { + output_by_field_type.insert( + field_type, + CellTestOutput { + field_id, + expected: data.expected.clone(), + }, + ); + } + } + } + + let row_rev = self.builder.build(); + let row_id = row_rev.id.clone(); + scripts.push(CreateRow { row_rev }); + + for field_type in FieldType::iter() { + if let Some(data) = output_by_field_type.get(&field_type) { + let script = AssertCell { + row_id: row_id.clone(), + field_id: data.field_id.clone(), + field_type, + expected: data.expected.clone(), + }; + scripts.push(script); + } + } + scripts + } +} + +pub struct CellTestData { + pub input: String, + pub expected: String, +} + +struct CellTestOutput { + field_id: String, + expected: String, +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index 49b8383fee..7aade54a08 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs @@ -1,66 +1,109 @@ -use crate::grid::block_test::script::GridRowTest; - use flowy_grid::entities::FieldType; -use flowy_grid::services::field::DateCellChangeset; -use flowy_grid::services::row::{CreateRowRevisionBuilder, CreateRowRevisionPayload}; -use flowy_grid_data_model::revision::FieldRevision; +use std::sync::Arc; + +use flowy_grid::services::field::{ + DateCellChangeset, MultiSelectTypeOption, SelectOption, SingleSelectTypeOption, SELECTION_IDS_SEPARATOR, +}; +use flowy_grid::services::row::RowRevisionBuilder; +use flowy_grid_data_model::revision::{FieldRevision, RowRevision}; + use strum::EnumCount; pub struct GridRowTestBuilder<'a> { - test: &'a GridRowTest, - inner_builder: CreateRowRevisionBuilder<'a>, + block_id: String, + field_revs: &'a [Arc], + inner_builder: RowRevisionBuilder<'a>, } impl<'a> GridRowTestBuilder<'a> { - pub fn new(test: &'a GridRowTest) -> Self { - assert_eq!(test.field_revs().len(), FieldType::COUNT); - - let inner_builder = CreateRowRevisionBuilder::new(test.field_revs()); - Self { test, inner_builder } - } - #[allow(dead_code)] - pub fn update_text_cell(mut self, data: String) -> Self { - let text_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.add_cell(&text_field.id, data).unwrap(); - self + pub fn new(block_id: &str, field_revs: &'a [Arc]) -> Self { + assert_eq!(field_revs.len(), FieldType::COUNT); + let inner_builder = RowRevisionBuilder::new(field_revs); + Self { + block_id: block_id.to_owned(), + field_revs, + inner_builder, + } } - #[allow(dead_code)] - pub fn update_number_cell(mut self, data: String) -> Self { - let number_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.add_cell(&number_field.id, data).unwrap(); - self + pub fn insert_text_cell(&mut self, data: &str) -> String { + let text_field = self.field_rev_with_type(&FieldType::RichText); + self.inner_builder + .insert_cell(&text_field.id, data.to_string()) + .unwrap(); + + text_field.id.clone() } - #[allow(dead_code)] - pub fn update_date_cell(mut self, value: i64) -> Self { + pub fn insert_number_cell(&mut self, data: &str) -> String { + let number_field = self.field_rev_with_type(&FieldType::Number); + self.inner_builder + .insert_cell(&number_field.id, data.to_string()) + .unwrap(); + number_field.id.clone() + } + + pub fn insert_date_cell(&mut self, data: &str) -> String { let value = serde_json::to_string(&DateCellChangeset { - date: Some(value.to_string()), + date: Some(data.to_string()), time: None, }) .unwrap(); let date_field = self.field_rev_with_type(&FieldType::DateTime); - self.inner_builder.add_cell(&date_field.id, value).unwrap(); - self + self.inner_builder.insert_cell(&date_field.id, value).unwrap(); + date_field.id.clone() } - #[allow(dead_code)] - pub fn update_checkbox_cell(mut self, data: bool) -> Self { - let number_field = self.field_rev_with_type(&FieldType::Checkbox); - self.inner_builder.add_cell(&number_field.id, data.to_string()).unwrap(); - self + pub fn insert_checkbox_cell(&mut self, data: &str) -> String { + let checkbox_field = self.field_rev_with_type(&FieldType::Checkbox); + self.inner_builder + .insert_cell(&checkbox_field.id, data.to_string()) + .unwrap(); + + checkbox_field.id.clone() } - #[allow(dead_code)] - pub fn update_url_cell(mut self, data: String) -> Self { - let number_field = self.field_rev_with_type(&FieldType::Checkbox); - self.inner_builder.add_cell(&number_field.id, data).unwrap(); - self + pub fn insert_url_cell(&mut self, data: &str) -> String { + let url_field = self.field_rev_with_type(&FieldType::URL); + self.inner_builder.insert_cell(&url_field.id, data.to_string()).unwrap(); + url_field.id.clone() + } + + pub fn insert_single_select_cell(&mut self, f: F) -> String + where + F: Fn(Vec) -> SelectOption, + { + let single_select_field = self.field_rev_with_type(&FieldType::SingleSelect); + let type_option = SingleSelectTypeOption::from(&single_select_field); + let option = f(type_option.options); + self.inner_builder + .insert_select_option_cell(&single_select_field.id, option.id) + .unwrap(); + + single_select_field.id.clone() + } + + pub fn insert_multi_select_cell(&mut self, f: F) -> String + where + F: Fn(Vec) -> Vec, + { + let multi_select_field = self.field_rev_with_type(&FieldType::MultiSelect); + let type_option = MultiSelectTypeOption::from(&multi_select_field); + let options = f(type_option.options); + let ops_ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + self.inner_builder + .insert_select_option_cell(&multi_select_field.id, ops_ids) + .unwrap(); + + multi_select_field.id.clone() } pub fn field_rev_with_type(&self, field_type: &FieldType) -> FieldRevision { - self.test - .field_revs() + self.field_revs .iter() .find(|field_rev| { let t_field_type: FieldType = field_rev.field_type_rev.into(); @@ -71,7 +114,21 @@ impl<'a> GridRowTestBuilder<'a> { .clone() } - pub fn build(self) -> CreateRowRevisionPayload { - self.inner_builder.build() + pub fn build(self) -> RowRevision { + self.inner_builder.build(&self.block_id) + } +} + +impl<'a> std::ops::Deref for GridRowTestBuilder<'a> { + type Target = RowRevisionBuilder<'a>; + + fn deref(&self) -> &Self::Target { + &self.inner_builder + } +} + +impl<'a> std::ops::DerefMut for GridRowTestBuilder<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner_builder } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs index 844a230036..ccdb8ea629 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/cell_test/test.rs @@ -2,7 +2,7 @@ use crate::grid::cell_test::script::CellScript::*; use crate::grid::cell_test::script::GridCellTest; use crate::grid::field_test::util::make_date_cell_string; use flowy_grid::entities::{CellChangeset, FieldType}; -use flowy_grid::services::field::select_option::SelectOptionCellChangeset; +use flowy_grid::services::field::selection_type_option::SelectOptionCellChangeset; use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; #[tokio::test] diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs index 624979e940..c53ba2a2fc 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs @@ -1,7 +1,7 @@ use crate::grid::field_test::script::FieldScript::*; use crate::grid::field_test::script::GridFieldTest; use crate::grid::field_test::util::*; -use flowy_grid::services::field::select_option::SelectOption; +use flowy_grid::services::field::selection_type_option::SelectOption; use flowy_grid::services::field::SingleSelectTypeOption; use flowy_grid_data_model::revision::TypeOptionDataEntry; use flowy_sync::entities::grid::FieldChangesetParams; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs index d9055f93ea..42c04e41f9 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/util.rs @@ -1,5 +1,5 @@ use flowy_grid::entities::*; -use flowy_grid::services::field::select_option::SelectOption; +use flowy_grid::services::field::selection_type_option::SelectOption; use flowy_grid::services::field::*; use flowy_grid_data_model::revision::*; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs index 29614b21b2..e954ebe512 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs @@ -1,12 +1,12 @@ use crate::grid::filter_test::script::FilterScript::*; use crate::grid::filter_test::script::*; -use flowy_grid::entities::{CreateGridFilterPayload, TextFilterCondition}; +use flowy_grid::entities::{CreateGridFilterPayload, FieldType, TextFilterCondition}; use flowy_grid_data_model::revision::FieldRevision; #[tokio::test] async fn grid_filter_create_test() { let mut test = GridFilterTest::new().await; - let field_rev = test.text_field(); + let field_rev = test.get_field_rev(FieldType::RichText); let payload = CreateGridFilterPayload::new(field_rev, TextFilterCondition::TextIsEmpty, Some("abc".to_owned())); let scripts = vec![InsertGridTableFilter { payload }, AssertTableFilterCount { count: 1 }]; test.run_scripts(scripts).await; @@ -16,7 +16,7 @@ async fn grid_filter_create_test() { #[should_panic] async fn grid_filter_invalid_condition_panic_test() { let mut test = GridFilterTest::new().await; - let field_rev = test.text_field().clone(); + let field_rev = test.get_field_rev(FieldType::RichText).clone(); // 100 is not a valid condition, so this test should be panic. let payload = CreateGridFilterPayload::new(&field_rev, 100, Some("".to_owned())); @@ -27,7 +27,7 @@ async fn grid_filter_invalid_condition_panic_test() { #[tokio::test] async fn grid_filter_delete_test() { let mut test = GridFilterTest::new().await; - let field_rev = test.text_field().clone(); + let field_rev = test.get_field_rev(FieldType::RichText).clone(); let payload = create_filter(&field_rev, TextFilterCondition::TextIsEmpty, "abc"); let scripts = vec![InsertGridTableFilter { payload }, AssertTableFilterCount { count: 1 }]; test.run_scripts(scripts).await; @@ -36,7 +36,7 @@ async fn grid_filter_delete_test() { test.run_scripts(vec![ DeleteGridTableFilter { filter_id: filter.id, - field_rev, + field_rev: field_rev.as_ref().clone(), }, AssertTableFilterCount { count: 0 }, ]) diff --git a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs index d944aac6c5..2070bd6fce 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/grid_editor.rs @@ -1,12 +1,13 @@ #![allow(clippy::all)] #![allow(dead_code)] #![allow(unused_imports)] +use crate::grid::block_test::util::GridRowTestBuilder; use bytes::Bytes; use flowy_grid::entities::*; -use flowy_grid::services::field::select_option::SelectOption; +use flowy_grid::services::field::SelectOption; use flowy_grid::services::field::*; use flowy_grid::services::grid_editor::{GridPadBuilder, GridRevisionEditor}; -use flowy_grid::services::row::CreateRowRevisionPayload; +use flowy_grid::services::row::{CreateRowRevisionPayload, RowRevisionBuilder}; use flowy_grid::services::setting::GridSettingChangesetBuilder; use flowy_grid_data_model::revision::*; use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS; @@ -20,6 +21,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use strum::EnumCount; +use strum::IntoEnumIterator; use tokio::time::sleep; pub struct GridEditorTest { @@ -37,14 +39,13 @@ impl GridEditorTest { pub async fn new() -> Self { let sdk = FlowySDKTest::default(); let _ = sdk.init_user().await; - let build_context = make_all_field_test_grid(); + let build_context = make_test_grid(); let view_data: Bytes = build_context.into(); let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await; let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap(); let field_revs = editor.get_field_revs(None).await.unwrap(); let block_meta_revs = editor.get_block_meta_revs().await.unwrap(); let row_revs = editor.grid_block_snapshots(None).await.unwrap().pop().unwrap().row_revs; - assert_eq!(row_revs.len(), 3); assert_eq!(block_meta_revs.len(), 1); // It seems like you should add the field in the make_test_grid() function. @@ -64,7 +65,7 @@ impl GridEditorTest { } } - pub(crate) async fn get_row_revs(&self) -> Vec> { + pub async fn get_row_revs(&self) -> Vec> { self.editor .grid_block_snapshots(None) .await @@ -79,86 +80,179 @@ impl GridEditorTest { self.editor.get_grid_filter(&layout_type).await.unwrap() } - pub fn text_field(&self) -> &FieldRevision { + pub fn get_field_rev(&self, field_type: FieldType) -> &Arc { self.field_revs .iter() .filter(|field_rev| { let t_field_type: FieldType = field_rev.field_type_rev.into(); - t_field_type == FieldType::RichText + t_field_type == field_type }) .collect::>() .pop() .unwrap() } + + pub fn block_id(&self) -> &str { + &self.block_meta_revs.last().unwrap().block_id + } } -fn make_all_field_test_grid() -> BuildGridContext { - let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) - .name("Name") - .visibility(true) - .build(); +pub const GOOGLE: &str = "Google"; +pub const FACEBOOK: &str = "Facebook"; +pub const TWITTER: &str = "Twitter"; - // Single Select - let single_select = SingleSelectTypeOptionBuilder::default() - .option(SelectOption::new("Live")) - .option(SelectOption::new("Completed")) - .option(SelectOption::new("Planned")) - .option(SelectOption::new("Paused")); - let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); +pub const COMPLETED: &str = "Completed"; +pub const PLANNED: &str = "Planned"; +pub const PAUSED: &str = "Paused"; - // MultiSelect - let multi_select = MultiSelectTypeOptionBuilder::default() - .option(SelectOption::new("Google")) - .option(SelectOption::new("Facebook")) - .option(SelectOption::new("Twitter")); - let multi_select_field = FieldBuilder::new(multi_select) - .name("Platform") - .visibility(true) - .build(); +// This grid is assumed to contain all the Fields. +fn make_test_grid() -> BuildGridContext { + let mut grid_builder = GridBuilder::new(); + // Iterate through the FieldType to create the corresponding Field. + for field_type in FieldType::iter() { + let field_type: FieldType = field_type; - // Number - let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD); - let number_field = FieldBuilder::new(number).name("Price").visibility(true).build(); + // The + match field_type { + FieldType::RichText => { + let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default()) + .name("Name") + .visibility(true) + .build(); + grid_builder.add_field(text_field); + } + FieldType::Number => { + // Number + let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD); + let number_field = FieldBuilder::new(number).name("Price").visibility(true).build(); + grid_builder.add_field(number_field); + } + FieldType::DateTime => { + // Date + let date = DateTypeOptionBuilder::default() + .date_format(DateFormat::US) + .time_format(TimeFormat::TwentyFourHour); + let date_field = FieldBuilder::new(date).name("Time").visibility(true).build(); + grid_builder.add_field(date_field); + } + FieldType::SingleSelect => { + // Single Select + let single_select = SingleSelectTypeOptionBuilder::default() + .option(SelectOption::new(COMPLETED)) + .option(SelectOption::new(PLANNED)) + .option(SelectOption::new(PAUSED)); + let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build(); + grid_builder.add_field(single_select_field); + } + FieldType::MultiSelect => { + // MultiSelect + let multi_select = MultiSelectTypeOptionBuilder::default() + .option(SelectOption::new(GOOGLE)) + .option(SelectOption::new(FACEBOOK)) + .option(SelectOption::new(TWITTER)); + let multi_select_field = FieldBuilder::new(multi_select) + .name("Platform") + .visibility(true) + .build(); + grid_builder.add_field(multi_select_field); + } + FieldType::Checkbox => { + // Checkbox + let checkbox = CheckboxTypeOptionBuilder::default(); + let checkbox_field = FieldBuilder::new(checkbox).name("is urgent").visibility(true).build(); + grid_builder.add_field(checkbox_field); + } + FieldType::URL => { + // URL + let url = URLTypeOptionBuilder::default(); + let url_field = FieldBuilder::new(url).name("link").visibility(true).build(); + grid_builder.add_field(url_field); + } + } + } - // Date - let date = DateTypeOptionBuilder::default() - .date_format(DateFormat::US) - .time_format(TimeFormat::TwentyFourHour); - let date_field = FieldBuilder::new(date).name("Time").visibility(true).build(); + // 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 block_id = grid_builder.block_id().to_owned(); + let field_revs = grid_builder.field_revs(); + let mut row_builder = GridRowTestBuilder::new(&block_id, field_revs); + match i { + 0 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("A"), + FieldType::Number => row_builder.insert_number_cell("1"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + _ => "".to_owned(), + }; + } + } + 1 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("B"), + FieldType::Number => row_builder.insert_number_cell("2"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(0)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("true"), + _ => "".to_owned(), + }; + } + } + 2 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("C"), + FieldType::Number => row_builder.insert_number_cell("3"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + } + 3 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("D"), + FieldType::Number => row_builder.insert_number_cell("4"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(1)) + } + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + } + 4 => { + for field_type in FieldType::iter() { + match field_type { + FieldType::RichText => row_builder.insert_text_cell("E"), + FieldType::Number => row_builder.insert_number_cell("5"), + FieldType::DateTime => row_builder.insert_date_cell("1647251762"), + FieldType::SingleSelect => { + row_builder.insert_single_select_cell(|mut options| options.remove(2)) + } - // Checkbox - let checkbox = CheckboxTypeOptionBuilder::default(); - let checkbox_field = FieldBuilder::new(checkbox).name("is done").visibility(true).build(); + FieldType::Checkbox => row_builder.insert_checkbox_cell("false"), + _ => "".to_owned(), + }; + } + } + _ => {} + } - // URL - let url = URLTypeOptionBuilder::default(); - let url_field = FieldBuilder::new(url).name("link").visibility(true).build(); - - // for i in 0..3 { - // for field_type in FieldType::iter() { - // let field_type: FieldType = field_type; - // match field_type { - // FieldType::RichText => {} - // FieldType::Number => {} - // FieldType::DateTime => {} - // FieldType::SingleSelect => {} - // FieldType::MultiSelect => {} - // FieldType::Checkbox => {} - // FieldType::URL => {} - // } - // } - // } - - GridBuilder::default() - .add_field(text_field) - .add_field(single_select_field) - .add_field(multi_select_field) - .add_field(number_field) - .add_field(date_field) - .add_field(checkbox_field) - .add_field(url_field) - .add_empty_row() - .add_empty_row() - .add_empty_row() - .build() + let row_rev = row_builder.build(); + grid_builder.add_row(row_rev); + } + grid_builder.build() } diff --git a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs index edceb25d9c..671f71563f 100644 --- a/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs +++ b/shared-lib/flowy-grid-data-model/src/revision/grid_rev.rs @@ -53,7 +53,7 @@ impl GridRevision { pub fn from_build_context(grid_id: &str, context: BuildGridContext) -> Self { Self { grid_id: grid_id.to_owned(), - fields: context.field_revs.into_iter().map(Arc::new).collect(), + fields: context.field_revs, blocks: context.blocks.into_iter().map(Arc::new).collect(), setting: Default::default(), } @@ -245,7 +245,7 @@ impl CellRevision { #[derive(Clone, Default, Deserialize, Serialize)] pub struct BuildGridContext { - pub field_revs: Vec, + pub field_revs: Vec>, pub blocks: Vec, pub blocks_meta_data: Vec, } diff --git a/shared-lib/flowy-sync/src/client_grid/grid_builder.rs b/shared-lib/flowy-sync/src/client_grid/grid_builder.rs index d1fbed3f53..ea5d5a9332 100644 --- a/shared-lib/flowy-sync/src/client_grid/grid_builder.rs +++ b/shared-lib/flowy-sync/src/client_grid/grid_builder.rs @@ -26,18 +26,31 @@ impl std::default::Default for GridBuilder { } impl GridBuilder { - pub fn add_field(mut self, field: FieldRevision) -> Self { - self.build_context.field_revs.push(field); - self + pub fn new() -> Self { + Self::default() + } + pub fn add_field(&mut self, field: FieldRevision) { + self.build_context.field_revs.push(Arc::new(field)); } - pub fn add_empty_row(mut self) -> Self { - let row = RowRevision::new(&self.build_context.blocks.first().unwrap().block_id); + pub fn add_row(&mut self, row_rev: RowRevision) { let block_meta_rev = self.build_context.blocks.first_mut().unwrap(); let block_rev = self.build_context.blocks_meta_data.first_mut().unwrap(); - block_rev.rows.push(Arc::new(row)); + block_rev.rows.push(Arc::new(row_rev)); block_meta_rev.row_count += 1; - self + } + + pub fn add_empty_row(&mut self) { + let row = RowRevision::new(self.block_id()); + self.add_row(row); + } + + pub fn field_revs(&self) -> &Vec> { + &self.build_context.field_revs + } + + pub fn block_id(&self) -> &str { + &self.build_context.blocks.first().unwrap().block_id } pub fn build(self) -> BuildGridContext {