From e8e719b73f68dc1c36813fb1369e5834d08f8e79 Mon Sep 17 00:00:00 2001 From: appflowy Date: Thu, 7 Jul 2022 18:20:12 +0800 Subject: [PATCH] refactor: separate multi-select and single-select --- frontend/rust-lib/flowy-grid/Flowy.toml | 1 + .../filter_entities/checkbox_filter.rs | 28 + .../entities/filter_entities/number_filter.rs | 14 +- .../filter_entities/select_option_filter.rs | 7 + .../entities/filter_entities/text_filter.rs | 10 +- .../src/entities/filter_entities/util.rs | 2 +- .../rust-lib/flowy-grid/src/event_handler.rs | 6 +- .../flowy-grid/src/services/field/mod.rs | 1 + .../src/services/field/select_option.rs | 316 +++++++++ .../type_options/checkbox_type_option.rs | 15 +- .../field/type_options/date_type_option.rs | 5 +- .../src/services/field/type_options/mod.rs | 7 +- .../type_options/multi_select_type_option.rs | 216 ++++++ .../number_type_option/number_type_option.rs | 10 +- .../type_options/selection_type_option.rs | 664 ------------------ .../type_options/single_select_type_option.rs | 200 ++++++ .../field/type_options/text_type_option.rs | 11 +- .../field/type_options/url_type_option.rs | 11 +- .../{util.rs => util/cell_data_util.rs} | 0 .../services/field/type_options/util/mod.rs | 4 + .../src/services/row/cell_data_operation.rs | 4 + .../src/services/row/row_builder.rs | 2 +- .../flowy-grid/tests/grid/cell_test.rs | 3 +- .../flowy-grid/tests/grid/field_test.rs | 3 +- .../flowy-grid/tests/grid/field_util.rs | 4 +- .../flowy-grid/tests/grid/row_test.rs | 5 +- .../rust-lib/flowy-grid/tests/grid/script.rs | 1 + 27 files changed, 850 insertions(+), 700 deletions(-) create mode 100644 frontend/rust-lib/flowy-grid/src/services/field/select_option.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs delete mode 100644 frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs create mode 100644 frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs rename frontend/rust-lib/flowy-grid/src/services/field/type_options/{util.rs => util/cell_data_util.rs} (100%) create mode 100644 frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs diff --git a/frontend/rust-lib/flowy-grid/Flowy.toml b/frontend/rust-lib/flowy-grid/Flowy.toml index 00e9cf7c04..94bd0c0225 100644 --- a/frontend/rust-lib/flowy-grid/Flowy.toml +++ b/frontend/rust-lib/flowy-grid/Flowy.toml @@ -2,6 +2,7 @@ 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/checkbox_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs index bb31b7a3be..c13cad33e8 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/checkbox_filter.rs @@ -1,3 +1,4 @@ +use crate::services::field::CheckboxCellData; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::revision::GridFilterRevision; @@ -9,6 +10,16 @@ pub struct GridCheckboxFilter { pub condition: CheckboxCondition, } +impl GridCheckboxFilter { + pub fn apply(&self, cell_data: &CheckboxCellData) -> bool { + let is_check = cell_data.is_check(); + match self.condition { + CheckboxCondition::IsChecked => is_check, + CheckboxCondition::IsUnChecked => !is_check, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] #[repr(u8)] pub enum CheckboxCondition { @@ -47,3 +58,20 @@ impl std::convert::From> for GridCheckboxFilter { } } } + +#[cfg(test)] +mod tests { + use crate::entities::{CheckboxCondition, GridCheckboxFilter}; + use crate::services::field::CheckboxCellData; + + #[test] + fn checkbox_filter_is_check_test() { + let checkbox_filter = GridCheckboxFilter { + condition: CheckboxCondition::IsChecked, + }; + for (value, r) in [("true", true), ("yes", true), ("false", false), ("no", false)] { + let data = CheckboxCellData(value.to_owned()); + assert_eq!(checkbox_filter.apply(&data), r); + } + } +} diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs index 972803028d..b54f94972d 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/number_filter.rs @@ -3,7 +3,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::revision::GridFilterRevision; use rust_decimal::prelude::Zero; -use rust_decimal::{Decimal, Error}; +use rust_decimal::Decimal; use std::str::FromStr; use std::sync::Arc; @@ -25,7 +25,7 @@ impl GridNumberFilter { let content = self.content.as_ref().unwrap(); let zero_decimal = Decimal::zero(); let cell_decimal = num_cell_data.decimal().as_ref().unwrap_or(&zero_decimal); - match Decimal::from_str(&content) { + match Decimal::from_str(content) { Ok(decimal) => match self.condition { NumberFilterCondition::Equal => cell_decimal == &decimal, NumberFilterCondition::NotEqual => cell_decimal != &decimal, @@ -95,7 +95,7 @@ impl std::convert::From> for GridNumberFilter { #[cfg(test)] mod tests { use crate::entities::{GridNumberFilter, NumberFilterCondition}; - use crate::services::field::number_currency::Currency; + use crate::services::field::{NumberCellData, NumberFormat}; use std::str::FromStr; #[test] @@ -105,13 +105,13 @@ mod tests { content: Some("123".to_owned()), }; - for (num_str, r) in vec![("123", true), ("1234", false), ("", false)] { + for (num_str, r) in [("123", true), ("1234", false), ("", false)] { let data = NumberCellData::from_str(num_str).unwrap(); assert_eq!(number_filter.apply(&data), r); } let format = NumberFormat::USD; - for (num_str, r) in vec![("$123", true), ("1234", false), ("", false)] { + for (num_str, r) in [("$123", true), ("1234", false), ("", false)] { let data = NumberCellData::from_format_str(num_str, true, &format).unwrap(); assert_eq!(number_filter.apply(&data), r); } @@ -122,7 +122,7 @@ mod tests { condition: NumberFilterCondition::GreaterThan, content: Some("12".to_owned()), }; - for (num_str, r) in vec![("123", true), ("10", false), ("30", true), ("", false)] { + for (num_str, r) in [("123", true), ("10", false), ("30", true), ("", false)] { let data = NumberCellData::from_str(num_str).unwrap(); assert_eq!(number_filter.apply(&data), r); } @@ -134,7 +134,7 @@ mod tests { condition: NumberFilterCondition::LessThan, content: Some("100".to_owned()), }; - for (num_str, r) in vec![("12", true), ("1234", false), ("30", true), ("", true)] { + for (num_str, r) in [("12", true), ("1234", false), ("30", true), ("", true)] { let data = NumberCellData::from_str(num_str).unwrap(); assert_eq!(number_filter.apply(&data), r); } 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 14fc5b2ed9..c0fcf2cac6 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,3 +1,4 @@ +use crate::services::field::select_option::SelectOptionIds; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; use flowy_grid_data_model::revision::GridFilterRevision; @@ -12,6 +13,12 @@ pub struct GridSelectOptionFilter { pub content: Option, } +impl GridSelectOptionFilter { + pub fn apply(&self, _ids: &SelectOptionIds) -> bool { + false + } +} + #[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] #[repr(u8)] pub enum SelectOptionCondition { diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs index 84c4e54024..65253d2faa 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/text_filter.rs @@ -13,8 +13,9 @@ pub struct GridTextFilter { } impl GridTextFilter { - pub fn apply(&self, s: &str) -> bool { - let s = s.to_lowercase(); + pub fn apply>(&self, cell_data: T) -> bool { + let cell_data = cell_data.as_ref(); + let s = cell_data.to_lowercase(); if let Some(content) = self.content.as_ref() { match self.condition { TextFilterCondition::Is => &s == content, @@ -27,7 +28,7 @@ impl GridTextFilter { TextFilterCondition::TextIsNotEmpty => !s.is_empty(), } } else { - return false; + false } } } @@ -85,6 +86,7 @@ impl std::convert::From> for GridTextFilter { #[cfg(test)] mod tests { + #![allow(clippy::all)] use crate::entities::{GridTextFilter, TextFilterCondition}; #[test] @@ -94,7 +96,7 @@ mod tests { content: Some("appflowy".to_owned()), }; - assert_eq!(text_filter.apply("AppFlowy"), true); + assert!(text_filter.apply("AppFlowy")); assert_eq!(text_filter.apply("appflowy"), true); assert_eq!(text_filter.apply("Appflowy"), true); assert_eq!(text_filter.apply("AppFlowy.io"), false); diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs index a219478162..32ac38f986 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs @@ -2,7 +2,7 @@ use crate::entities::{ CheckboxCondition, DateFilterCondition, FieldType, NumberFilterCondition, SelectOptionCondition, TextFilterCondition, }; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use flowy_grid_data_model::parser::NotEmptyStr; use flowy_grid_data_model::revision::{FieldRevision, GridFilterRevision}; diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index cbe43c8f82..5e0919e595 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -1,7 +1,9 @@ use crate::entities::*; use crate::manager::GridManager; -use crate::services::field::type_options::*; -use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_json_str}; +use crate::services::field::select_option::*; +use crate::services::field::{ + default_type_option_builder_from_type, type_option_builder_from_json_str, DateChangesetParams, DateChangesetPayload, +}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::revision::FieldRevision; use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams}; 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 c1b689fbf4..8a01d90d1f 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/mod.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/mod.rs @@ -1,4 +1,5 @@ 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/select_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/select_option.rs new file mode 100644 index 0000000000..86939d070e --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/select_option.rs @@ -0,0 +1,316 @@ +use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType}; +use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; +use crate::services::row::AnyCellData; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_grid_data_model::parser::NotEmptyStr; +use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataEntry}; +use nanoid::nanoid; +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +pub const SELECTION_IDS_SEPARATOR: &str = ","; + +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ProtoBuf)] +pub struct SelectOption { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub name: String, + + #[pb(index = 3)] + pub color: SelectOptionColor, +} + +impl SelectOption { + pub fn new(name: &str) -> Self { + SelectOption { + id: nanoid!(4), + name: name.to_owned(), + color: SelectOptionColor::default(), + } + } + + pub fn with_color(name: &str, color: SelectOptionColor) -> Self { + SelectOption { + id: nanoid!(4), + name: name.to_owned(), + color, + } + } +} + +#[derive(ProtoBuf_Enum, PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] +#[repr(u8)] +pub enum SelectOptionColor { + Purple = 0, + Pink = 1, + LightPink = 2, + Orange = 3, + Yellow = 4, + Lime = 5, + Green = 6, + Aqua = 7, + Blue = 8, +} + +impl std::default::Default for SelectOptionColor { + fn default() -> Self { + SelectOptionColor::Purple + } +} + +pub fn make_select_context_from(cell_rev: &Option, options: &[SelectOption]) -> Vec { + match cell_rev { + None => vec![], + Some(cell_rev) => { + if let Ok(type_option_cell_data) = AnyCellData::from_str(&cell_rev.data) { + select_option_ids(type_option_cell_data.cell_data) + .into_iter() + .flat_map(|option_id| options.iter().find(|option| option.id == option_id).cloned()) + .collect() + } else { + vec![] + } + } + } +} + +pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync { + fn insert_option(&mut self, new_option: SelectOption) { + let options = self.mut_options(); + if let Some(index) = options + .iter() + .position(|option| option.id == new_option.id || option.name == new_option.name) + { + options.remove(index); + options.insert(index, new_option); + } else { + options.insert(0, new_option); + } + } + + fn delete_option(&mut self, delete_option: SelectOption) { + let options = self.mut_options(); + if let Some(index) = options.iter().position(|option| option.id == delete_option.id) { + options.remove(index); + } + } + + fn create_option(&self, name: &str) -> SelectOption { + let color = select_option_color_from_index(self.options().len()); + SelectOption::with_color(name, color) + } + + fn select_option_cell_data(&self, cell_rev: &Option) -> SelectOptionCellData; + + fn options(&self) -> &Vec; + + fn mut_options(&mut self) -> &mut Vec; +} + +pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult> { + let field_type: FieldType = field_rev.field_type_rev.into(); + match &field_type { + FieldType::SingleSelect => { + let type_option = SingleSelectTypeOption::from(field_rev); + Ok(Box::new(type_option)) + } + FieldType::MultiSelect => { + let type_option = MultiSelectTypeOption::from(field_rev); + Ok(Box::new(type_option)) + } + ty => { + tracing::error!("Unsupported field type: {:?} for this handler", ty); + Err(ErrorCode::FieldInvalidOperation.into()) + } + } +} + +pub fn select_option_color_from_index(index: usize) -> SelectOptionColor { + match index % 8 { + 0 => SelectOptionColor::Purple, + 1 => SelectOptionColor::Pink, + 2 => SelectOptionColor::LightPink, + 3 => SelectOptionColor::Orange, + 4 => SelectOptionColor::Yellow, + 5 => SelectOptionColor::Lime, + 6 => SelectOptionColor::Green, + 7 => SelectOptionColor::Aqua, + 8 => SelectOptionColor::Blue, + _ => SelectOptionColor::Purple, + } +} +pub struct SelectOptionIds(Vec); +impl std::convert::TryFrom for SelectOptionIds { + type Error = FlowyError; + + fn try_from(value: AnyCellData) -> Result { + let ids = select_option_ids(value.cell_data); + Ok(Self(ids)) + } +} + +impl std::convert::From for SelectOptionIds { + fn from(s: String) -> Self { + let ids = select_option_ids(s); + Self(ids) + } +} + +impl std::ops::Deref for SelectOptionIds { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for SelectOptionIds { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +fn select_option_ids(data: String) -> Vec { + data.split(SELECTION_IDS_SEPARATOR) + .map(|id| id.to_string()) + .collect::>() +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct SelectOptionCellChangesetPayload { + #[pb(index = 1)] + pub cell_identifier: CellIdentifierPayload, + + #[pb(index = 2, one_of)] + pub insert_option_id: Option, + + #[pb(index = 3, one_of)] + pub delete_option_id: Option, +} + +pub struct SelectOptionCellChangesetParams { + pub cell_identifier: CellIdentifier, + pub insert_option_id: Option, + pub delete_option_id: Option, +} + +impl std::convert::From for CellChangeset { + fn from(params: SelectOptionCellChangesetParams) -> Self { + let changeset = SelectOptionCellContentChangeset { + insert_option_id: params.insert_option_id, + delete_option_id: params.delete_option_id, + }; + 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, + cell_content_changeset: Some(s), + } + } +} + +impl TryInto for SelectOptionCellChangesetPayload { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?; + let insert_option_id = match self.insert_option_id { + None => None, + Some(insert_option_id) => Some( + NotEmptyStr::parse(insert_option_id) + .map_err(|_| ErrorCode::OptionIdIsEmpty)? + .0, + ), + }; + + let delete_option_id = match self.delete_option_id { + None => None, + Some(delete_option_id) => Some( + NotEmptyStr::parse(delete_option_id) + .map_err(|_| ErrorCode::OptionIdIsEmpty)? + .0, + ), + }; + + Ok(SelectOptionCellChangesetParams { + cell_identifier, + insert_option_id, + delete_option_id, + }) + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct SelectOptionCellContentChangeset { + pub insert_option_id: Option, + pub delete_option_id: Option, +} + +impl SelectOptionCellContentChangeset { + pub fn from_insert(option_id: &str) -> Self { + SelectOptionCellContentChangeset { + insert_option_id: Some(option_id.to_string()), + delete_option_id: None, + } + } + + pub fn from_delete(option_id: &str) -> Self { + SelectOptionCellContentChangeset { + insert_option_id: None, + delete_option_id: Some(option_id.to_string()), + } + } + + pub fn to_str(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct SelectOptionCellData { + #[pb(index = 1)] + pub options: Vec, + + #[pb(index = 2)] + pub select_options: Vec, +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct SelectOptionChangesetPayload { + #[pb(index = 1)] + pub cell_identifier: CellIdentifierPayload, + + #[pb(index = 2, one_of)] + pub insert_option: Option, + + #[pb(index = 3, one_of)] + pub update_option: Option, + + #[pb(index = 4, one_of)] + pub delete_option: Option, +} + +pub struct SelectOptionChangeset { + pub cell_identifier: CellIdentifier, + pub insert_option: Option, + pub update_option: Option, + pub delete_option: Option, +} + +impl TryInto for SelectOptionChangesetPayload { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let cell_identifier = self.cell_identifier.try_into()?; + Ok(SelectOptionChangeset { + cell_identifier, + insert_option: self.insert_option, + update_option: self.update_option, + delete_option: self.delete_option, + }) + } +} 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 index edc140eb11..bb8f43c6fb 100644 --- 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 @@ -44,8 +44,11 @@ const NO: &str = "No"; impl CellFilterOperation for CheckboxTypeOption { fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridCheckboxFilter) -> FlowyResult { + if !any_cell_data.is_checkbox() { + return Ok(true); + } let checkbox_cell_data: CheckboxCellData = any_cell_data.try_into()?; - Ok(false) + Ok(filter.apply(&checkbox_cell_data)) } } @@ -97,11 +100,17 @@ fn string_to_bool(bool_str: &str) -> bool { } } -pub struct CheckboxCellData(String); +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 { + fn try_from(_value: AnyCellData) -> Result { todo!() } } 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 index e05f4fbf60..21c608f3b4 100644 --- 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 @@ -118,7 +118,10 @@ impl DateTypeOption { } impl CellFilterOperation for DateTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridDateFilter) -> FlowyResult { + fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridDateFilter) -> FlowyResult { + if !any_cell_data.is_date() { + return Ok(true); + } Ok(false) } } 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 3cfe390b38..754e441876 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,14 +1,17 @@ mod checkbox_type_option; mod date_type_option; +mod multi_select_type_option; mod number_type_option; -mod selection_type_option; +mod single_select_type_option; mod text_type_option; 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 selection_type_option::*; +pub use single_select_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/multi_select_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs new file mode 100644 index 0000000000..3d2c1f7c4e --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/multi_select_type_option.rs @@ -0,0 +1,216 @@ +use crate::entities::{FieldType, GridSelectOptionFilter}; + +use crate::impl_type_option; +use crate::services::field::select_option::{ + make_select_context_from, SelectOption, SelectOptionCellContentChangeset, SelectOptionCellData, SelectOptionIds, + SelectOptionOperation, SELECTION_IDS_SEPARATOR, +}; +use crate::services::field::type_options::util::get_cell_data; +use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::row::{ + AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData, +}; +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 +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct MultiSelectTypeOption { + #[pb(index = 1)] + pub options: Vec, + + #[pb(index = 2)] + pub disable_color: bool, +} +impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect); + +impl SelectOptionOperation for MultiSelectTypeOption { + fn select_option_cell_data(&self, cell_rev: &Option) -> SelectOptionCellData { + let select_options = make_select_context_from(cell_rev, &self.options); + SelectOptionCellData { + options: self.options.clone(), + select_options, + } + } + + fn options(&self) -> &Vec { + &self.options + } + + fn mut_options(&mut self) -> &mut Vec { + &mut self.options + } +} +impl CellFilterOperation for MultiSelectTypeOption { + fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridSelectOptionFilter) -> FlowyResult { + if !any_cell_data.is_multi_select() { + return Ok(true); + } + let _ids: SelectOptionIds = any_cell_data.try_into()?; + Ok(false) + } +} +impl CellDataOperation for MultiSelectTypeOption { + fn decode_cell_data( + &self, + cell_data: T, + decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult + where + T: Into, + { + if !decoded_field_type.is_select_option() { + return Ok(DecodedCellData::default()); + } + + let encoded_data = cell_data.into(); + let ids: SelectOptionIds = encoded_data.into(); + 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) + } + + fn apply_changeset(&self, changeset: T, cell_rev: Option) -> Result + where + T: Into, + { + let content_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset.into())?; + let new_cell_data: String; + match cell_rev { + None => { + new_cell_data = content_changeset.insert_option_id.unwrap_or_else(|| "".to_owned()); + } + Some(cell_rev) => { + let cell_data = get_cell_data(&cell_rev); + let mut select_ids: SelectOptionIds = cell_data.into(); + if let Some(insert_option_id) = content_changeset.insert_option_id { + tracing::trace!("Insert multi select option: {}", &insert_option_id); + if select_ids.contains(&insert_option_id) { + select_ids.retain(|id| id != &insert_option_id); + } else { + select_ids.push(insert_option_id); + } + } + + if let Some(delete_option_id) = content_changeset.delete_option_id { + tracing::trace!("Delete multi select option: {}", &delete_option_id); + select_ids.retain(|id| id != &delete_option_id); + } + + new_cell_data = select_ids.join(SELECTION_IDS_SEPARATOR); + tracing::trace!("Multi select cell data: {}", &new_cell_data); + } + } + + Ok(new_cell_data) + } +} + +#[derive(Default)] +pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOption); +impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOption); +impl MultiSelectTypeOptionBuilder { + pub fn option(mut self, opt: SelectOption) -> Self { + self.0.options.push(opt); + self + } +} + +impl TypeOptionBuilder for MultiSelectTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::MultiSelect + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::field::select_option::*; + use crate::services::field::FieldBuilder; + use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder}; + use crate::services::row::CellDataOperation; + use flowy_grid_data_model::revision::FieldRevision; + + #[test] + fn multi_select_test() { + let google_option = SelectOption::new("Google"); + let facebook_option = SelectOption::new("Facebook"); + let twitter_option = SelectOption::new("Twitter"); + let multi_select = MultiSelectTypeOptionBuilder::default() + .option(google_option.clone()) + .option(facebook_option.clone()) + .option(twitter_option); + + let field_rev = FieldBuilder::new(multi_select) + .name("Platform") + .visibility(true) + .build(); + + let type_option = MultiSelectTypeOption::from(&field_rev); + + let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR); + let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str(); + let cell_data = type_option.apply_changeset(data, None).unwrap(); + assert_multi_select_options( + cell_data, + &type_option, + &field_rev, + vec![google_option.clone(), facebook_option], + ); + + let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str(); + let cell_data = type_option.apply_changeset(data, None).unwrap(); + assert_multi_select_options(cell_data, &type_option, &field_rev, vec![google_option]); + + // Invalid option id + let cell_data = type_option + .apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None) + .unwrap(); + assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]); + + // Invalid option id + let cell_data = type_option + .apply_changeset(SelectOptionCellContentChangeset::from_insert("123,456").to_str(), None) + .unwrap(); + assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]); + + // Invalid changeset + assert!(type_option.apply_changeset("123", None).is_err()); + } + + fn assert_multi_select_options( + cell_data: String, + type_option: &MultiSelectTypeOption, + field_rev: &FieldRevision, + expected: Vec, + ) { + let field_type: FieldType = field_rev.field_type_rev.into(); + assert_eq!( + expected, + type_option + .decode_cell_data(cell_data, &field_type, field_rev) + .unwrap() + .parse::() + .unwrap() + .select_options, + ); + } +} 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 30c9993547..2b9717458b 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 @@ -11,8 +11,8 @@ 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::prelude::Zero; -use rust_decimal::{Decimal, Error}; + +use rust_decimal::Decimal; use rusty_money::Money; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -107,6 +107,10 @@ pub(crate) fn strip_currency_symbol(s: T) -> String { } impl CellFilterOperation for NumberTypeOption { fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridNumberFilter) -> FlowyResult { + if !any_cell_data.is_number() { + return Ok(true); + } + let cell_data = any_cell_data.cell_data; let num_cell_data = self.format_cell_data(&cell_data)?; @@ -209,7 +213,7 @@ impl NumberCellData { pub fn from_money(money: Money) -> Self { Self { - decimal: Some(money.amount().clone()), + decimal: Some(*money.amount()), money: Some(money.to_string()), } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs deleted file mode 100644 index 8657da61ef..0000000000 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/selection_type_option.rs +++ /dev/null @@ -1,664 +0,0 @@ -use crate::entities::{CellChangeset, FieldType, GridSelectOptionFilter}; -use crate::entities::{CellIdentifier, CellIdentifierPayload}; -use crate::impl_type_option; -use crate::services::field::type_options::util::get_cell_data; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; -use crate::services::row::{ - AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData, -}; -use bytes::Bytes; -use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_grid_data_model::parser::NotEmptyStr; -use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry}; -use nanoid::nanoid; -use serde::{Deserialize, Serialize}; -use std::str::FromStr; - -pub const SELECTION_IDS_SEPARATOR: &str = ","; - -pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync { - fn insert_option(&mut self, new_option: SelectOption) { - let options = self.mut_options(); - if let Some(index) = options - .iter() - .position(|option| option.id == new_option.id || option.name == new_option.name) - { - options.remove(index); - options.insert(index, new_option); - } else { - options.insert(0, new_option); - } - } - - fn delete_option(&mut self, delete_option: SelectOption) { - let options = self.mut_options(); - if let Some(index) = options.iter().position(|option| option.id == delete_option.id) { - options.remove(index); - } - } - - fn create_option(&self, name: &str) -> SelectOption { - let color = select_option_color_from_index(self.options().len()); - SelectOption::with_color(name, color) - } - - fn select_option_cell_data(&self, cell_rev: &Option) -> SelectOptionCellData; - - fn options(&self) -> &Vec; - - fn mut_options(&mut self) -> &mut Vec; -} - -pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult> { - let field_type: FieldType = field_rev.field_type_rev.into(); - match &field_type { - FieldType::SingleSelect => { - let type_option = SingleSelectTypeOption::from(field_rev); - Ok(Box::new(type_option)) - } - FieldType::MultiSelect => { - let type_option = MultiSelectTypeOption::from(field_rev); - Ok(Box::new(type_option)) - } - ty => { - tracing::error!("Unsupported field type: {:?} for this handler", ty); - Err(ErrorCode::FieldInvalidOperation.into()) - } - } -} - -// Single select -#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct SingleSelectTypeOption { - #[pb(index = 1)] - pub options: Vec, - - #[pb(index = 2)] - pub disable_color: bool, -} -impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect); - -impl SelectOptionOperation for SingleSelectTypeOption { - fn select_option_cell_data(&self, cell_rev: &Option) -> SelectOptionCellData { - let select_options = make_select_context_from(cell_rev, &self.options); - SelectOptionCellData { - options: self.options.clone(), - select_options, - } - } - - fn options(&self) -> &Vec { - &self.options - } - - fn mut_options(&mut self) -> &mut Vec { - &mut self.options - } -} - -impl CellFilterOperation for SingleSelectTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult { - let ids: SelectOptionIds = any_cell_data.try_into()?; - Ok(false) - } -} - -impl CellDataOperation for SingleSelectTypeOption { - fn decode_cell_data( - &self, - cell_data: T, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult - where - T: Into, - { - if !decoded_field_type.is_select_option() { - return Ok(DecodedCellData::default()); - } - - let encoded_data = cell_data.into(); - let mut cell_data = SelectOptionCellData { - options: self.options.clone(), - select_options: vec![], - }; - if let Some(option_id) = select_option_ids(encoded_data).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) - } - - fn apply_changeset(&self, changeset: C, _cell_rev: Option) -> Result - where - C: Into, - { - let changeset = changeset.into(); - let select_option_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset)?; - let new_cell_data: String; - if let Some(insert_option_id) = select_option_changeset.insert_option_id { - tracing::trace!("Insert single select option: {}", &insert_option_id); - new_cell_data = insert_option_id; - } else { - tracing::trace!("Delete single select option"); - new_cell_data = "".to_string() - } - - Ok(new_cell_data) - } -} - -#[derive(Default)] -pub struct SingleSelectTypeOptionBuilder(SingleSelectTypeOption); -impl_into_box_type_option_builder!(SingleSelectTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(SingleSelectTypeOptionBuilder, SingleSelectTypeOption); - -impl SingleSelectTypeOptionBuilder { - pub fn option(mut self, opt: SelectOption) -> Self { - self.0.options.push(opt); - self - } -} - -impl TypeOptionBuilder for SingleSelectTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::SingleSelect - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -// Multiple select -#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct MultiSelectTypeOption { - #[pb(index = 1)] - pub options: Vec, - - #[pb(index = 2)] - pub disable_color: bool, -} -impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect); - -impl SelectOptionOperation for MultiSelectTypeOption { - fn select_option_cell_data(&self, cell_rev: &Option) -> SelectOptionCellData { - let select_options = make_select_context_from(cell_rev, &self.options); - SelectOptionCellData { - options: self.options.clone(), - select_options, - } - } - - fn options(&self) -> &Vec { - &self.options - } - - fn mut_options(&mut self) -> &mut Vec { - &mut self.options - } -} -impl CellFilterOperation for MultiSelectTypeOption { - fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult { - let ids: SelectOptionIds = any_cell_data.try_into()?; - Ok(false) - } -} -impl CellDataOperation for MultiSelectTypeOption { - fn decode_cell_data( - &self, - cell_data: T, - decoded_field_type: &FieldType, - _field_rev: &FieldRevision, - ) -> FlowyResult - where - T: Into, - { - if !decoded_field_type.is_select_option() { - return Ok(DecodedCellData::default()); - } - - let encoded_data = cell_data.into(); - let select_options = select_option_ids(encoded_data) - .into_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) - } - - fn apply_changeset(&self, changeset: T, cell_rev: Option) -> Result - where - T: Into, - { - let content_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset.into())?; - let new_cell_data: String; - match cell_rev { - None => { - new_cell_data = content_changeset.insert_option_id.unwrap_or_else(|| "".to_owned()); - } - Some(cell_rev) => { - let cell_data = get_cell_data(&cell_rev); - let mut selected_options = select_option_ids(cell_data); - if let Some(insert_option_id) = content_changeset.insert_option_id { - tracing::trace!("Insert multi select option: {}", &insert_option_id); - if selected_options.contains(&insert_option_id) { - selected_options.retain(|id| id != &insert_option_id); - } else { - selected_options.push(insert_option_id); - } - } - - if let Some(delete_option_id) = content_changeset.delete_option_id { - tracing::trace!("Delete multi select option: {}", &delete_option_id); - selected_options.retain(|id| id != &delete_option_id); - } - - new_cell_data = selected_options.join(SELECTION_IDS_SEPARATOR); - tracing::trace!("Multi select cell data: {}", &new_cell_data); - } - } - - Ok(new_cell_data) - } -} - -#[derive(Default)] -pub struct MultiSelectTypeOptionBuilder(MultiSelectTypeOption); -impl_into_box_type_option_builder!(MultiSelectTypeOptionBuilder); -impl_builder_from_json_str_and_from_bytes!(MultiSelectTypeOptionBuilder, MultiSelectTypeOption); -impl MultiSelectTypeOptionBuilder { - pub fn option(mut self, opt: SelectOption) -> Self { - self.0.options.push(opt); - self - } -} - -impl TypeOptionBuilder for MultiSelectTypeOptionBuilder { - fn field_type(&self) -> FieldType { - FieldType::MultiSelect - } - - fn entry(&self) -> &dyn TypeOptionDataEntry { - &self.0 - } -} - -pub struct SelectOptionIds(Vec); -impl std::convert::TryFrom for SelectOptionIds { - type Error = FlowyError; - - fn try_from(value: AnyCellData) -> Result { - let ids = select_option_ids(value.cell_data); - Ok(Self(ids)) - } -} - -fn select_option_ids(data: String) -> Vec { - data.split(SELECTION_IDS_SEPARATOR) - .map(|id| id.to_string()) - .collect::>() -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, ProtoBuf)] -pub struct SelectOption { - #[pb(index = 1)] - pub id: String, - - #[pb(index = 2)] - pub name: String, - - #[pb(index = 3)] - pub color: SelectOptionColor, -} - -impl SelectOption { - pub fn new(name: &str) -> Self { - SelectOption { - id: nanoid!(4), - name: name.to_owned(), - color: SelectOptionColor::default(), - } - } - - pub fn with_color(name: &str, color: SelectOptionColor) -> Self { - SelectOption { - id: nanoid!(4), - name: name.to_owned(), - color, - } - } -} - -#[derive(Clone, Debug, Default, ProtoBuf)] -pub struct SelectOptionChangesetPayload { - #[pb(index = 1)] - pub cell_identifier: CellIdentifierPayload, - - #[pb(index = 2, one_of)] - pub insert_option: Option, - - #[pb(index = 3, one_of)] - pub update_option: Option, - - #[pb(index = 4, one_of)] - pub delete_option: Option, -} - -pub struct SelectOptionChangeset { - pub cell_identifier: CellIdentifier, - pub insert_option: Option, - pub update_option: Option, - pub delete_option: Option, -} - -impl TryInto for SelectOptionChangesetPayload { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let cell_identifier = self.cell_identifier.try_into()?; - Ok(SelectOptionChangeset { - cell_identifier, - insert_option: self.insert_option, - update_option: self.update_option, - delete_option: self.delete_option, - }) - } -} - -#[derive(Clone, Debug, Default, ProtoBuf)] -pub struct SelectOptionCellChangesetPayload { - #[pb(index = 1)] - pub cell_identifier: CellIdentifierPayload, - - #[pb(index = 2, one_of)] - pub insert_option_id: Option, - - #[pb(index = 3, one_of)] - pub delete_option_id: Option, -} - -pub struct SelectOptionCellChangesetParams { - pub cell_identifier: CellIdentifier, - pub insert_option_id: Option, - pub delete_option_id: Option, -} - -impl std::convert::From for CellChangeset { - fn from(params: SelectOptionCellChangesetParams) -> Self { - let changeset = SelectOptionCellContentChangeset { - insert_option_id: params.insert_option_id, - delete_option_id: params.delete_option_id, - }; - 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, - cell_content_changeset: Some(s), - } - } -} - -impl TryInto for SelectOptionCellChangesetPayload { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?; - let insert_option_id = match self.insert_option_id { - None => None, - Some(insert_option_id) => Some( - NotEmptyStr::parse(insert_option_id) - .map_err(|_| ErrorCode::OptionIdIsEmpty)? - .0, - ), - }; - - let delete_option_id = match self.delete_option_id { - None => None, - Some(delete_option_id) => Some( - NotEmptyStr::parse(delete_option_id) - .map_err(|_| ErrorCode::OptionIdIsEmpty)? - .0, - ), - }; - - Ok(SelectOptionCellChangesetParams { - cell_identifier, - insert_option_id, - delete_option_id, - }) - } -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct SelectOptionCellContentChangeset { - pub insert_option_id: Option, - pub delete_option_id: Option, -} - -impl SelectOptionCellContentChangeset { - pub fn from_insert(option_id: &str) -> Self { - SelectOptionCellContentChangeset { - insert_option_id: Some(option_id.to_string()), - delete_option_id: None, - } - } - - pub fn from_delete(option_id: &str) -> Self { - SelectOptionCellContentChangeset { - insert_option_id: None, - delete_option_id: Some(option_id.to_string()), - } - } - - pub fn to_str(&self) -> String { - serde_json::to_string(self).unwrap() - } -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] -pub struct SelectOptionCellData { - #[pb(index = 1)] - pub options: Vec, - - #[pb(index = 2)] - pub select_options: Vec, -} - -#[derive(ProtoBuf_Enum, PartialEq, Eq, Serialize, Deserialize, Debug, Clone)] -#[repr(u8)] -pub enum SelectOptionColor { - Purple = 0, - Pink = 1, - LightPink = 2, - Orange = 3, - Yellow = 4, - Lime = 5, - Green = 6, - Aqua = 7, - Blue = 8, -} - -pub fn select_option_color_from_index(index: usize) -> SelectOptionColor { - match index % 8 { - 0 => SelectOptionColor::Purple, - 1 => SelectOptionColor::Pink, - 2 => SelectOptionColor::LightPink, - 3 => SelectOptionColor::Orange, - 4 => SelectOptionColor::Yellow, - 5 => SelectOptionColor::Lime, - 6 => SelectOptionColor::Green, - 7 => SelectOptionColor::Aqua, - 8 => SelectOptionColor::Blue, - _ => SelectOptionColor::Purple, - } -} - -impl std::default::Default for SelectOptionColor { - fn default() -> Self { - SelectOptionColor::Purple - } -} - -fn make_select_context_from(cell_rev: &Option, options: &[SelectOption]) -> Vec { - match cell_rev { - None => vec![], - Some(cell_rev) => { - if let Ok(type_option_cell_data) = AnyCellData::from_str(&cell_rev.data) { - select_option_ids(type_option_cell_data.cell_data) - .into_iter() - .flat_map(|option_id| options.iter().find(|option| option.id == option_id).cloned()) - .collect() - } else { - vec![] - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::entities::FieldType; - use crate::services::field::FieldBuilder; - use crate::services::field::{ - MultiSelectTypeOption, MultiSelectTypeOptionBuilder, SelectOption, SelectOptionCellContentChangeset, - SelectOptionCellData, SingleSelectTypeOption, SingleSelectTypeOptionBuilder, SELECTION_IDS_SEPARATOR, - }; - use crate::services::row::CellDataOperation; - use flowy_grid_data_model::revision::FieldRevision; - - #[test] - fn single_select_test() { - let google_option = SelectOption::new("Google"); - let facebook_option = SelectOption::new("Facebook"); - let twitter_option = SelectOption::new("Twitter"); - let single_select = SingleSelectTypeOptionBuilder::default() - .option(google_option.clone()) - .option(facebook_option.clone()) - .option(twitter_option); - - let field_rev = FieldBuilder::new(single_select) - .name("Platform") - .visibility(true) - .build(); - - let type_option = SingleSelectTypeOption::from(&field_rev); - - let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR); - let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str(); - let cell_data = type_option.apply_changeset(data, None).unwrap(); - assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option.clone()]); - - let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str(); - let cell_data = type_option.apply_changeset(data, None).unwrap(); - assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option]); - - // Invalid option id - let cell_data = type_option - .apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None) - .unwrap(); - assert_single_select_options(cell_data, &type_option, &field_rev, vec![]); - - // Invalid option id - let cell_data = type_option - .apply_changeset(SelectOptionCellContentChangeset::from_insert("123").to_str(), None) - .unwrap(); - - assert_single_select_options(cell_data, &type_option, &field_rev, vec![]); - - // Invalid changeset - assert!(type_option.apply_changeset("123", None).is_err()); - } - - #[test] - fn multi_select_test() { - let google_option = SelectOption::new("Google"); - let facebook_option = SelectOption::new("Facebook"); - let twitter_option = SelectOption::new("Twitter"); - let multi_select = MultiSelectTypeOptionBuilder::default() - .option(google_option.clone()) - .option(facebook_option.clone()) - .option(twitter_option); - - let field_rev = FieldBuilder::new(multi_select) - .name("Platform") - .visibility(true) - .build(); - - let type_option = MultiSelectTypeOption::from(&field_rev); - - let option_ids = vec![google_option.id.clone(), facebook_option.id.clone()].join(SELECTION_IDS_SEPARATOR); - let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str(); - let cell_data = type_option.apply_changeset(data, None).unwrap(); - assert_multi_select_options( - cell_data, - &type_option, - &field_rev, - vec![google_option.clone(), facebook_option], - ); - - let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str(); - let cell_data = type_option.apply_changeset(data, None).unwrap(); - assert_multi_select_options(cell_data, &type_option, &field_rev, vec![google_option]); - - // Invalid option id - let cell_data = type_option - .apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None) - .unwrap(); - assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]); - - // Invalid option id - let cell_data = type_option - .apply_changeset(SelectOptionCellContentChangeset::from_insert("123,456").to_str(), None) - .unwrap(); - assert_multi_select_options(cell_data, &type_option, &field_rev, vec![]); - - // Invalid changeset - assert!(type_option.apply_changeset("123", None).is_err()); - } - - fn assert_multi_select_options( - cell_data: String, - type_option: &MultiSelectTypeOption, - field_rev: &FieldRevision, - expected: Vec, - ) { - let field_type: FieldType = field_rev.field_type_rev.into(); - assert_eq!( - expected, - type_option - .decode_cell_data(cell_data, &field_type, field_rev) - .unwrap() - .parse::() - .unwrap() - .select_options, - ); - } - - fn assert_single_select_options( - cell_data: String, - type_option: &SingleSelectTypeOption, - field_rev: &FieldRevision, - expected: Vec, - ) { - let field_type: FieldType = field_rev.field_type_rev.into(); - assert_eq!( - expected, - type_option - .decode_cell_data(cell_data, &field_type, field_rev) - .unwrap() - .parse::() - .unwrap() - .select_options, - ); - } -} 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/single_select_type_option.rs new file mode 100644 index 0000000000..391fe9fe49 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/single_select_type_option.rs @@ -0,0 +1,200 @@ +use crate::entities::{FieldType, GridSelectOptionFilter}; + +use crate::impl_type_option; +use crate::services::field::select_option::{ + make_select_context_from, SelectOption, SelectOptionCellContentChangeset, SelectOptionCellData, SelectOptionIds, + SelectOptionOperation, +}; + +use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::row::{ + AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData, +}; +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}; + +// Single select +#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)] +pub struct SingleSelectTypeOption { + #[pb(index = 1)] + pub options: Vec, + + #[pb(index = 2)] + pub disable_color: bool, +} +impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect); + +impl SelectOptionOperation for SingleSelectTypeOption { + fn select_option_cell_data(&self, cell_rev: &Option) -> SelectOptionCellData { + let select_options = make_select_context_from(cell_rev, &self.options); + SelectOptionCellData { + options: self.options.clone(), + select_options, + } + } + + fn options(&self) -> &Vec { + &self.options + } + + fn mut_options(&mut self) -> &mut Vec { + &mut self.options + } +} + +impl CellFilterOperation for SingleSelectTypeOption { + fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridSelectOptionFilter) -> FlowyResult { + if !any_cell_data.is_single_select() { + return Ok(true); + } + let _ids: SelectOptionIds = any_cell_data.try_into()?; + Ok(false) + } +} + +impl CellDataOperation for SingleSelectTypeOption { + fn decode_cell_data( + &self, + cell_data: T, + decoded_field_type: &FieldType, + _field_rev: &FieldRevision, + ) -> FlowyResult + where + T: Into, + { + if !decoded_field_type.is_select_option() { + return Ok(DecodedCellData::default()); + } + + let encoded_data = cell_data.into(); + let mut cell_data = SelectOptionCellData { + options: self.options.clone(), + select_options: vec![], + }; + + let ids: SelectOptionIds = encoded_data.into(); + 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) + } + + fn apply_changeset(&self, changeset: C, _cell_rev: Option) -> Result + where + C: Into, + { + let changeset = changeset.into(); + let select_option_changeset: SelectOptionCellContentChangeset = serde_json::from_str(&changeset)?; + let new_cell_data: String; + if let Some(insert_option_id) = select_option_changeset.insert_option_id { + tracing::trace!("Insert single select option: {}", &insert_option_id); + new_cell_data = insert_option_id; + } else { + tracing::trace!("Delete single select option"); + new_cell_data = "".to_string() + } + + Ok(new_cell_data) + } +} + +#[derive(Default)] +pub struct SingleSelectTypeOptionBuilder(SingleSelectTypeOption); +impl_into_box_type_option_builder!(SingleSelectTypeOptionBuilder); +impl_builder_from_json_str_and_from_bytes!(SingleSelectTypeOptionBuilder, SingleSelectTypeOption); + +impl SingleSelectTypeOptionBuilder { + pub fn option(mut self, opt: SelectOption) -> Self { + self.0.options.push(opt); + self + } +} + +impl TypeOptionBuilder for SingleSelectTypeOptionBuilder { + fn field_type(&self) -> FieldType { + FieldType::SingleSelect + } + + fn entry(&self) -> &dyn TypeOptionDataEntry { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use crate::entities::FieldType; + use crate::services::field::select_option::*; + use crate::services::field::type_options::*; + use crate::services::field::FieldBuilder; + use crate::services::row::CellDataOperation; + use flowy_grid_data_model::revision::FieldRevision; + + #[test] + fn single_select_test() { + let google_option = SelectOption::new("Google"); + let facebook_option = SelectOption::new("Facebook"); + let twitter_option = SelectOption::new("Twitter"); + let single_select = SingleSelectTypeOptionBuilder::default() + .option(google_option.clone()) + .option(facebook_option.clone()) + .option(twitter_option); + + let field_rev = FieldBuilder::new(single_select) + .name("Platform") + .visibility(true) + .build(); + + let type_option = SingleSelectTypeOption::from(&field_rev); + + let option_ids = vec![google_option.id.clone(), facebook_option.id].join(SELECTION_IDS_SEPARATOR); + let data = SelectOptionCellContentChangeset::from_insert(&option_ids).to_str(); + let cell_data = type_option.apply_changeset(data, None).unwrap(); + assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option.clone()]); + + let data = SelectOptionCellContentChangeset::from_insert(&google_option.id).to_str(); + let cell_data = type_option.apply_changeset(data, None).unwrap(); + assert_single_select_options(cell_data, &type_option, &field_rev, vec![google_option]); + + // Invalid option id + let cell_data = type_option + .apply_changeset(SelectOptionCellContentChangeset::from_insert("").to_str(), None) + .unwrap(); + assert_single_select_options(cell_data, &type_option, &field_rev, vec![]); + + // Invalid option id + let cell_data = type_option + .apply_changeset(SelectOptionCellContentChangeset::from_insert("123").to_str(), None) + .unwrap(); + + assert_single_select_options(cell_data, &type_option, &field_rev, vec![]); + + // Invalid changeset + assert!(type_option.apply_changeset("123", None).is_err()); + } + + fn assert_single_select_options( + cell_data: String, + type_option: &SingleSelectTypeOption, + field_rev: &FieldRevision, + expected: Vec, + ) { + let field_type: FieldType = field_rev.field_type_rev.into(); + assert_eq!( + expected, + type_option + .decode_cell_data(cell_data, &field_type, field_rev) + .unwrap() + .parse::() + .unwrap() + .select_options, + ); + } +} 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.rs index be8cd4acb3..8b0579d6c9 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.rs @@ -39,7 +39,7 @@ impl CellFilterOperation for RichTextTypeOption { } let text_cell_data: TextCellData = any_cell_data.try_into()?; - Ok(filter.apply(&text_cell_data.0)) + Ok(filter.apply(text_cell_data)) } } @@ -78,7 +78,13 @@ impl CellDataOperation for RichTextTypeOption { } } -pub struct TextCellData(String); +pub struct TextCellData(pub String); +impl AsRef for TextCellData { + fn as_ref(&self) -> &str { + &self.0 + } +} + impl std::convert::TryFrom for TextCellData { type Error = FlowyError; @@ -90,6 +96,7 @@ impl std::convert::TryFrom for TextCellData { #[cfg(test)] mod tests { use crate::entities::FieldType; + use crate::services::field::select_option::*; use crate::services::field::FieldBuilder; use crate::services::field::*; use crate::services::row::CellDataOperation; 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 index a2c7132fd4..09af0cda8d 100644 --- 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 @@ -1,6 +1,6 @@ use crate::entities::{FieldType, GridTextFilter}; use crate::impl_type_option; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; +use crate::services::field::{BoxTypeOptionBuilder, TextCellData, TypeOptionBuilder}; use crate::services::row::{ AnyCellData, CellContentChangeset, CellDataOperation, CellFilterOperation, DecodedCellData, EncodedCellData, }; @@ -37,7 +37,12 @@ impl_type_option!(URLTypeOption, FieldType::URL); impl CellFilterOperation for URLTypeOption { fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridTextFilter) -> FlowyResult { - Ok(false) + if !any_cell_data.is_url() { + return Ok(true); + } + + let text_cell_data: TextCellData = any_cell_data.try_into()?; + Ok(filter.apply(&text_cell_data)) } } @@ -130,7 +135,7 @@ impl FromStr for URLCellData { impl std::convert::TryFrom for URLCellData { type Error = (); - fn try_from(value: AnyCellData) -> Result { + fn try_from(_value: AnyCellData) -> Result { todo!() } } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/util.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/cell_data_util.rs similarity index 100% rename from frontend/rust-lib/flowy-grid/src/services/field/type_options/util.rs rename to frontend/rust-lib/flowy-grid/src/services/field/type_options/util/cell_data_util.rs 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 new file mode 100644 index 0000000000..c7aa386fe9 --- /dev/null +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/util/mod.rs @@ -0,0 +1,4 @@ +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/row/cell_data_operation.rs b/frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs index 80d9222679..090c8d4e8d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/row/cell_data_operation.rs @@ -119,6 +119,10 @@ impl AnyCellData { self.field_type == FieldType::MultiSelect } + pub fn is_url(&self) -> bool { + self.field_type == FieldType::URL + } + pub fn is_select_option(&self) -> bool { self.field_type == FieldType::MultiSelect || self.field_type == FieldType::SingleSelect } 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 568c8ed12a..99c6617c7e 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,4 +1,4 @@ -use crate::services::field::SelectOptionCellContentChangeset; +use crate::services::field::select_option::SelectOptionCellContentChangeset; use crate::services::row::apply_cell_data_changeset; use flowy_error::{FlowyError, FlowyResult}; use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT}; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/cell_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/cell_test.rs index 98b50436ab..803e17e2b5 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/cell_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/cell_test.rs @@ -2,7 +2,8 @@ use crate::grid::field_util::make_date_cell_string; use crate::grid::script::EditorScript::*; use crate::grid::script::*; use flowy_grid::entities::{CellChangeset, FieldType}; -use flowy_grid::services::field::{MultiSelectTypeOption, SelectOptionCellContentChangeset, SingleSelectTypeOption}; +use flowy_grid::services::field::select_option::SelectOptionCellContentChangeset; +use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption}; #[tokio::test] async fn grid_cell_update() { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test.rs index c8eb1a541a..00428a8fc4 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test.rs @@ -1,7 +1,8 @@ use crate::grid::field_util::*; use crate::grid::script::EditorScript::*; use crate::grid::script::*; -use flowy_grid::services::field::{SelectOption, SingleSelectTypeOption}; +use flowy_grid::services::field::select_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_util.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_util.rs index ea16cde778..ca094651f5 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_util.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_util.rs @@ -1,6 +1,6 @@ -use flowy_grid::services::field::*; - use flowy_grid::entities::*; +use flowy_grid::services::field::select_option::SelectOption; +use flowy_grid::services::field::*; use flowy_grid_data_model::revision::*; pub fn create_text_field(grid_id: &str) -> (InsertFieldParams, FieldRevision) { diff --git a/frontend/rust-lib/flowy-grid/tests/grid/row_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/row_test.rs index 7aa7450120..c28c2a7537 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/row_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/row_test.rs @@ -4,9 +4,8 @@ use crate::grid::script::EditorScript::*; use crate::grid::script::*; use chrono::NaiveDateTime; use flowy_grid::entities::FieldType; -use flowy_grid::services::field::{ - DateCellData, MultiSelectTypeOption, SingleSelectTypeOption, SELECTION_IDS_SEPARATOR, -}; +use flowy_grid::services::field::select_option::SELECTION_IDS_SEPARATOR; +use flowy_grid::services::field::{DateCellData, MultiSelectTypeOption, SingleSelectTypeOption}; use flowy_grid::services::row::{decode_any_cell_data, CreateRowRevisionBuilder}; use flowy_grid_data_model::revision::RowMetaChangeset; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/script.rs index e6c8b901a5..e0ec211ba3 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/script.rs @@ -18,6 +18,7 @@ use std::sync::Arc; use std::time::Duration; use strum::EnumCount; use tokio::time::sleep; +use flowy_grid::services::field::select_option::SelectOption; use flowy_sync::entities::grid::{CreateGridFilterParams, DeleteFilterParams, FieldChangesetParams, GridSettingChangesetParams}; pub enum EditorScript {