refactor: separate multi-select and single-select

This commit is contained in:
appflowy 2022-07-07 18:20:12 +08:00
parent da0a7f01b3
commit e8e719b73f
27 changed files with 850 additions and 700 deletions

View File

@ -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"
]

View File

@ -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<Arc<GridFilterRevision>> 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);
}
}
}

View File

@ -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<Arc<GridFilterRevision>> 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);
}

View File

@ -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<String>,
}
impl GridSelectOptionFilter {
pub fn apply(&self, _ids: &SelectOptionIds) -> bool {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
#[repr(u8)]
pub enum SelectOptionCondition {

View File

@ -13,8 +13,9 @@ pub struct GridTextFilter {
}
impl GridTextFilter {
pub fn apply(&self, s: &str) -> bool {
let s = s.to_lowercase();
pub fn apply<T: AsRef<str>>(&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<Arc<GridFilterRevision>> 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);

View File

@ -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};

View File

@ -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};

View File

@ -1,4 +1,5 @@
mod field_builder;
pub mod select_option;
pub(crate) mod type_options;
pub use field_builder::*;

View File

@ -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<CellRevision>, options: &[SelectOption]) -> Vec<SelectOption> {
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<CellRevision>) -> SelectOptionCellData;
fn options(&self) -> &Vec<SelectOption>;
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
}
pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult<Box<dyn SelectOptionOperation>> {
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<String>);
impl std::convert::TryFrom<AnyCellData> for SelectOptionIds {
type Error = FlowyError;
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
let ids = select_option_ids(value.cell_data);
Ok(Self(ids))
}
}
impl std::convert::From<String> for SelectOptionIds {
fn from(s: String) -> Self {
let ids = select_option_ids(s);
Self(ids)
}
}
impl std::ops::Deref for SelectOptionIds {
type Target = Vec<String>;
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<String> {
data.split(SELECTION_IDS_SEPARATOR)
.map(|id| id.to_string())
.collect::<Vec<String>>()
}
#[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<String>,
#[pb(index = 3, one_of)]
pub delete_option_id: Option<String>,
}
pub struct SelectOptionCellChangesetParams {
pub cell_identifier: CellIdentifier,
pub insert_option_id: Option<String>,
pub delete_option_id: Option<String>,
}
impl std::convert::From<SelectOptionCellChangesetParams> 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<SelectOptionCellChangesetParams> for SelectOptionCellChangesetPayload {
type Error = ErrorCode;
fn try_into(self) -> Result<SelectOptionCellChangesetParams, Self::Error> {
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<String>,
pub delete_option_id: Option<String>,
}
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<SelectOption>,
#[pb(index = 2)]
pub select_options: Vec<SelectOption>,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct SelectOptionChangesetPayload {
#[pb(index = 1)]
pub cell_identifier: CellIdentifierPayload,
#[pb(index = 2, one_of)]
pub insert_option: Option<SelectOption>,
#[pb(index = 3, one_of)]
pub update_option: Option<SelectOption>,
#[pb(index = 4, one_of)]
pub delete_option: Option<SelectOption>,
}
pub struct SelectOptionChangeset {
pub cell_identifier: CellIdentifier,
pub insert_option: Option<SelectOption>,
pub update_option: Option<SelectOption>,
pub delete_option: Option<SelectOption>,
}
impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayload {
type Error = ErrorCode;
fn try_into(self) -> Result<SelectOptionChangeset, Self::Error> {
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,
})
}
}

View File

@ -44,8 +44,11 @@ const NO: &str = "No";
impl CellFilterOperation<GridCheckboxFilter> for CheckboxTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridCheckboxFilter) -> FlowyResult<bool> {
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<AnyCellData> for CheckboxCellData {
type Error = FlowyError;
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
fn try_from(_value: AnyCellData) -> Result<Self, Self::Error> {
todo!()
}
}

View File

@ -118,7 +118,10 @@ impl DateTypeOption {
}
impl CellFilterOperation<GridDateFilter> for DateTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridDateFilter) -> FlowyResult<bool> {
fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridDateFilter) -> FlowyResult<bool> {
if !any_cell_data.is_date() {
return Ok(true);
}
Ok(false)
}
}

View File

@ -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::*;

View File

@ -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<SelectOption>,
#[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<CellRevision>) -> SelectOptionCellData {
let select_options = make_select_context_from(cell_rev, &self.options);
SelectOptionCellData {
options: self.options.clone(),
select_options,
}
}
fn options(&self) -> &Vec<SelectOption> {
&self.options
}
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
&mut self.options
}
}
impl CellFilterOperation<GridSelectOptionFilter> for MultiSelectTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridSelectOptionFilter) -> FlowyResult<bool> {
if !any_cell_data.is_multi_select() {
return Ok(true);
}
let _ids: SelectOptionIds = any_cell_data.try_into()?;
Ok(false)
}
}
impl CellDataOperation<String> for MultiSelectTypeOption {
fn decode_cell_data<T>(
&self,
cell_data: T,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData>
where
T: Into<String>,
{
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::<Vec<SelectOption>>();
let cell_data = SelectOptionCellData {
options: self.options.clone(),
select_options,
};
DecodedCellData::try_from_bytes(cell_data)
}
fn apply_changeset<T>(&self, changeset: T, cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
where
T: Into<CellContentChangeset>,
{
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<SelectOption>,
) {
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::<SelectOptionCellData>()
.unwrap()
.select_options,
);
}
}

View File

@ -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<T: ToString>(s: T) -> String {
}
impl CellFilterOperation<GridNumberFilter> for NumberTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridNumberFilter) -> FlowyResult<bool> {
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<Currency>) -> Self {
Self {
decimal: Some(money.amount().clone()),
decimal: Some(*money.amount()),
money: Some(money.to_string()),
}
}

View File

@ -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<CellRevision>) -> SelectOptionCellData;
fn options(&self) -> &Vec<SelectOption>;
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
}
pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult<Box<dyn SelectOptionOperation>> {
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<SelectOption>,
#[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<CellRevision>) -> SelectOptionCellData {
let select_options = make_select_context_from(cell_rev, &self.options);
SelectOptionCellData {
options: self.options.clone(),
select_options,
}
}
fn options(&self) -> &Vec<SelectOption> {
&self.options
}
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
&mut self.options
}
}
impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult<bool> {
let ids: SelectOptionIds = any_cell_data.try_into()?;
Ok(false)
}
}
impl CellDataOperation<String> for SingleSelectTypeOption {
fn decode_cell_data<T>(
&self,
cell_data: T,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData>
where
T: Into<String>,
{
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<C>(&self, changeset: C, _cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
where
C: Into<CellContentChangeset>,
{
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<SelectOption>,
#[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<CellRevision>) -> SelectOptionCellData {
let select_options = make_select_context_from(cell_rev, &self.options);
SelectOptionCellData {
options: self.options.clone(),
select_options,
}
}
fn options(&self) -> &Vec<SelectOption> {
&self.options
}
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
&mut self.options
}
}
impl CellFilterOperation<GridSelectOptionFilter> for MultiSelectTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridSelectOptionFilter) -> FlowyResult<bool> {
let ids: SelectOptionIds = any_cell_data.try_into()?;
Ok(false)
}
}
impl CellDataOperation<String> for MultiSelectTypeOption {
fn decode_cell_data<T>(
&self,
cell_data: T,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData>
where
T: Into<String>,
{
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::<Vec<SelectOption>>();
let cell_data = SelectOptionCellData {
options: self.options.clone(),
select_options,
};
DecodedCellData::try_from_bytes(cell_data)
}
fn apply_changeset<T>(&self, changeset: T, cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
where
T: Into<CellContentChangeset>,
{
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<String>);
impl std::convert::TryFrom<AnyCellData> for SelectOptionIds {
type Error = FlowyError;
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
let ids = select_option_ids(value.cell_data);
Ok(Self(ids))
}
}
fn select_option_ids(data: String) -> Vec<String> {
data.split(SELECTION_IDS_SEPARATOR)
.map(|id| id.to_string())
.collect::<Vec<String>>()
}
#[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<SelectOption>,
#[pb(index = 3, one_of)]
pub update_option: Option<SelectOption>,
#[pb(index = 4, one_of)]
pub delete_option: Option<SelectOption>,
}
pub struct SelectOptionChangeset {
pub cell_identifier: CellIdentifier,
pub insert_option: Option<SelectOption>,
pub update_option: Option<SelectOption>,
pub delete_option: Option<SelectOption>,
}
impl TryInto<SelectOptionChangeset> for SelectOptionChangesetPayload {
type Error = ErrorCode;
fn try_into(self) -> Result<SelectOptionChangeset, Self::Error> {
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<String>,
#[pb(index = 3, one_of)]
pub delete_option_id: Option<String>,
}
pub struct SelectOptionCellChangesetParams {
pub cell_identifier: CellIdentifier,
pub insert_option_id: Option<String>,
pub delete_option_id: Option<String>,
}
impl std::convert::From<SelectOptionCellChangesetParams> 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<SelectOptionCellChangesetParams> for SelectOptionCellChangesetPayload {
type Error = ErrorCode;
fn try_into(self) -> Result<SelectOptionCellChangesetParams, Self::Error> {
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<String>,
pub delete_option_id: Option<String>,
}
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<SelectOption>,
#[pb(index = 2)]
pub select_options: Vec<SelectOption>,
}
#[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<CellRevision>, options: &[SelectOption]) -> Vec<SelectOption> {
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<SelectOption>,
) {
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::<SelectOptionCellData>()
.unwrap()
.select_options,
);
}
fn assert_single_select_options(
cell_data: String,
type_option: &SingleSelectTypeOption,
field_rev: &FieldRevision,
expected: Vec<SelectOption>,
) {
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::<SelectOptionCellData>()
.unwrap()
.select_options,
);
}
}

View File

@ -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<SelectOption>,
#[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<CellRevision>) -> SelectOptionCellData {
let select_options = make_select_context_from(cell_rev, &self.options);
SelectOptionCellData {
options: self.options.clone(),
select_options,
}
}
fn options(&self) -> &Vec<SelectOption> {
&self.options
}
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
&mut self.options
}
}
impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, _filter: &GridSelectOptionFilter) -> FlowyResult<bool> {
if !any_cell_data.is_single_select() {
return Ok(true);
}
let _ids: SelectOptionIds = any_cell_data.try_into()?;
Ok(false)
}
}
impl CellDataOperation<String> for SingleSelectTypeOption {
fn decode_cell_data<T>(
&self,
cell_data: T,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData>
where
T: Into<String>,
{
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<C>(&self, changeset: C, _cell_rev: Option<CellRevision>) -> Result<String, FlowyError>
where
C: Into<CellContentChangeset>,
{
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<SelectOption>,
) {
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::<SelectOptionCellData>()
.unwrap()
.select_options,
);
}
}

View File

@ -39,7 +39,7 @@ impl CellFilterOperation<GridTextFilter> 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<String> for RichTextTypeOption {
}
}
pub struct TextCellData(String);
pub struct TextCellData(pub String);
impl AsRef<str> for TextCellData {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::convert::TryFrom<AnyCellData> for TextCellData {
type Error = FlowyError;
@ -90,6 +96,7 @@ impl std::convert::TryFrom<AnyCellData> 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;

View File

@ -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<GridTextFilter> for URLTypeOption {
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &GridTextFilter) -> FlowyResult<bool> {
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<AnyCellData> for URLCellData {
type Error = ();
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
fn try_from(_value: AnyCellData) -> Result<Self, Self::Error> {
todo!()
}
}

View File

@ -0,0 +1,4 @@
mod cell_data_util;
pub use crate::services::field::select_option::*;
pub use cell_data_util::*;

View File

@ -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
}

View File

@ -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};

View File

@ -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() {

View File

@ -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;

View File

@ -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) {

View File

@ -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;

View File

@ -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 {