refactor: type options directory

This commit is contained in:
appflowy 2022-07-13 11:09:13 +08:00
parent 1e3640f8ac
commit f10e324b73
40 changed files with 1504 additions and 1452 deletions

View File

@ -2,7 +2,6 @@
proto_input = [
"src/event_map.rs",
"src/services/field/type_options",
"src/services/field/select_option.rs",
"src/entities",
"src/dart_notification.rs"
]

View File

@ -1,4 +1,4 @@
use crate::services::field::select_option::SelectOptionIds;
use crate::services::field::SelectOptionIds;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::ErrorCode;
use flowy_grid_data_model::revision::GridFilterRevision;

View File

@ -1,9 +1,11 @@
use crate::entities::*;
use crate::manager::GridManager;
use crate::services::cell::AnyCellData;
use crate::services::field::select_option::*;
use crate::services::field::{
default_type_option_builder_from_type, type_option_builder_from_json_str, DateChangesetParams, DateChangesetPayload,
default_type_option_builder_from_type, select_option_operation, type_option_builder_from_json_str,
DateChangesetParams, DateChangesetPayload, SelectOption, SelectOptionCellChangeset,
SelectOptionCellChangesetParams, SelectOptionCellChangesetPayload, SelectOptionCellData, SelectOptionChangeset,
SelectOptionChangesetPayload,
};
use crate::services::row::make_row_from_row_rev;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};

View File

@ -12,13 +12,13 @@ pub trait CellFilterOperation<T> {
}
/// Return object that describes the cell.
pub trait CellDisplayable<CD, DC> {
pub trait CellDisplayable<CD> {
fn display_data(
&self,
cell_data: CellData<CD>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<DC>;
) -> FlowyResult<CellBytes>;
}
// CD: Short for CellData. This type is the type return by apply_changeset function.

View File

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

View File

@ -1,145 +0,0 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{AnyCellData, CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption);
impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption);
impl CheckboxTypeOptionBuilder {
pub fn set_selected(mut self, is_selected: bool) -> Self {
self.0.is_selected = is_selected;
self
}
}
impl TypeOptionBuilder for CheckboxTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::Checkbox
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
pub struct CheckboxTypeOption {
#[pb(index = 1)]
pub is_selected: bool,
}
impl_type_option!(CheckboxTypeOption, FieldType::Checkbox);
const YES: &str = "Yes";
const NO: &str = "No";
impl CellDisplayable<String, bool> for CheckboxTypeOption {
fn display_data(
&self,
cell_data: CellData<String>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<bool> {
let s: String = cell_data.try_into_inner()?;
Ok(s == YES)
}
}
impl CellDataOperation<String, String> for CheckboxTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<String>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if !decoded_field_type.is_checkbox() {
return Ok(CellBytes::default());
}
let s: String = cell_data.try_into_inner()?;
if s == YES || s == NO {
return Ok(CellBytes::new(s));
}
Ok(CellBytes::default())
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let s = match string_to_bool(&changeset) {
true => YES,
false => NO,
};
Ok(s.to_string())
}
}
fn string_to_bool(bool_str: &str) -> bool {
let lower_case_str: &str = &bool_str.to_lowercase();
match lower_case_str {
"1" => true,
"true" => true,
"yes" => true,
"0" => false,
"false" => false,
"no" => false,
_ => false,
}
}
pub struct CheckboxCellData(pub String);
impl CheckboxCellData {
pub fn is_check(&self) -> bool {
string_to_bool(&self.0)
}
}
impl std::convert::TryFrom<AnyCellData> for CheckboxCellData {
type Error = FlowyError;
fn try_from(_value: AnyCellData) -> Result<Self, Self::Error> {
todo!()
}
}
#[cfg(test)]
mod tests {
use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data};
use crate::services::field::type_options::checkbox_type_option::{NO, YES};
use crate::services::field::FieldBuilder;
use crate::entities::FieldType;
#[test]
fn checkout_box_description_test() {
let field_rev = FieldBuilder::from_field_type(&FieldType::Checkbox).build();
let data = apply_cell_data_changeset("true", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev).to_string(), YES);
let data = apply_cell_data_changeset("1", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
let data = apply_cell_data_changeset("false", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
let data = apply_cell_data_changeset("no", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
let data = apply_cell_data_changeset("12", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
}
}

View File

@ -0,0 +1,75 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
use crate::services::field::{BoxTypeOptionBuilder, CheckboxCellData, TypeOptionBuilder};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption);
impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption);
impl CheckboxTypeOptionBuilder {
pub fn set_selected(mut self, is_selected: bool) -> Self {
self.0.is_selected = is_selected;
self
}
}
impl TypeOptionBuilder for CheckboxTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::Checkbox
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
pub struct CheckboxTypeOption {
#[pb(index = 1)]
pub is_selected: bool,
}
impl_type_option!(CheckboxTypeOption, FieldType::Checkbox);
impl CellDisplayable<CheckboxCellData> for CheckboxTypeOption {
fn display_data(
&self,
cell_data: CellData<CheckboxCellData>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
let cell_data = cell_data.try_into_inner()?;
Ok(CellBytes::new(cell_data))
}
}
impl CellDataOperation<CheckboxCellData, String> for CheckboxTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<CheckboxCellData>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if !decoded_field_type.is_checkbox() {
return Ok(CellBytes::default());
}
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let cell_data = CheckboxCellData::from_str(&changeset);
Ok(cell_data.to_string())
}
}

View File

@ -0,0 +1,61 @@
use crate::services::cell::{AnyCellData, FromCellString};
use flowy_error::{FlowyError, FlowyResult};
pub const YES: &str = "Yes";
pub const NO: &str = "No";
pub struct CheckboxCellData(pub String);
impl CheckboxCellData {
pub fn from_str(s: &str) -> Self {
let lower_case_str: &str = &s.to_lowercase();
let val = match lower_case_str {
"1" => Some(true),
"true" => Some(true),
"yes" => Some(true),
"0" => Some(false),
"false" => Some(false),
"no" => Some(false),
_ => None,
};
match val {
Some(true) => Self(YES.to_string()),
Some(false) => Self(NO.to_string()),
None => Self("".to_string()),
}
}
pub fn is_check(&self) -> bool {
&self.0 == YES
}
}
impl AsRef<[u8]> for CheckboxCellData {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl std::convert::TryFrom<AnyCellData> for CheckboxCellData {
type Error = FlowyError;
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
Ok(Self::from_str(&value.data))
}
}
impl FromCellString for CheckboxCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
Ok(Self::from_str(s))
}
}
impl ToString for CheckboxCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}

View File

@ -0,0 +1,6 @@
mod checkbox_option;
mod checkbox_option_entities;
mod tests;
pub use checkbox_option::*;
pub use checkbox_option_entities::*;

View File

@ -0,0 +1,30 @@
#[cfg(test)]
mod tests {
use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data};
use crate::services::field::type_options::checkbox_type_option::{NO, YES};
use crate::services::field::FieldBuilder;
use crate::entities::FieldType;
#[test]
fn checkout_box_description_test() {
let field_rev = FieldBuilder::from_field_type(&FieldType::Checkbox).build();
let data = apply_cell_data_changeset("true", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev).to_string(), YES);
let data = apply_cell_data_changeset("1", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
let data = apply_cell_data_changeset("false", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
let data = apply_cell_data_changeset("no", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
let data = apply_cell_data_changeset("12", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
}
}

View File

@ -1,672 +0,0 @@
use crate::entities::{CellChangeset, FieldType};
use crate::entities::{CellIdentifier, CellIdentifierPayload};
use crate::impl_type_option;
use crate::services::cell::{
AnyCellData, CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, FromCellChangeset,
FromCellString,
};
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes;
use chrono::format::strftime::StrftimeItems;
use chrono::{NaiveDateTime, Timelike};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
// Date
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
pub struct DateTypeOption {
#[pb(index = 1)]
pub date_format: DateFormat,
#[pb(index = 2)]
pub time_format: TimeFormat,
#[pb(index = 3)]
pub include_time: bool,
}
impl_type_option!(DateTypeOption, FieldType::DateTime);
impl DateTypeOption {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
fn today_desc_from_timestamp(&self, timestamp: i64) -> DateCellData {
let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0);
self.date_from_native(native)
}
fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellData {
if native.timestamp() == 0 {
return DateCellData::default();
}
let time = native.time();
let has_time = time.hour() != 0 || time.second() != 0;
let utc = self.utc_date_time_from_native(native);
let fmt = self.date_format.format_str();
let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt)));
let mut time = "".to_string();
if has_time {
let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str());
time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, "");
}
let timestamp = native.timestamp();
DateCellData { date, time, timestamp }
}
fn date_fmt(&self, time: &Option<String>) -> String {
if self.include_time {
match time.as_ref() {
None => self.date_format.format_str().to_string(),
Some(time_str) => {
if time_str.is_empty() {
self.date_format.format_str().to_string()
} else {
format!("{} {}", self.date_format.format_str(), self.time_format.format_str())
}
}
}
} else {
self.date_format.format_str().to_string()
}
}
fn timestamp_from_utc_with_time(
&self,
utc: &chrono::DateTime<chrono::Utc>,
time: &Option<String>,
) -> FlowyResult<i64> {
if let Some(time_str) = time.as_ref() {
if !time_str.is_empty() {
let date_str = format!(
"{}{}",
utc.format_with_items(StrftimeItems::new(self.date_format.format_str())),
&time_str
);
return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) {
Ok(native) => {
let utc = self.utc_date_time_from_native(native);
Ok(utc.timestamp())
}
Err(_e) => {
let msg = format!("Parse {} failed", date_str);
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg))
}
};
}
}
Ok(utc.timestamp())
}
fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime<chrono::Utc> {
let native = NaiveDateTime::from_timestamp(timestamp, 0);
let native2 = NaiveDateTime::from_timestamp(timestamp, 0);
if native > native2 {}
self.utc_date_time_from_native(native)
}
fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
}
}
impl CellDisplayable<DateTimestamp, DateCellData> for DateTypeOption {
fn display_data(
&self,
cell_data: CellData<DateTimestamp>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DateCellData> {
let timestamp = cell_data.try_into_inner()?;
Ok(self.today_desc_from_timestamp(timestamp.0))
}
}
impl CellDataOperation<DateTimestamp, DateCellChangeset> for DateTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<DateTimestamp>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
// Return default data if the type_option_cell_data is not FieldType::DateTime.
// It happens when switching from one field to another.
// For example:
// FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen.
if !decoded_field_type.is_date() {
return Ok(CellBytes::default());
}
CellBytes::from(self.display_data(cell_data, decoded_field_type, field_rev)?)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<DateCellChangeset>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let cell_data = match changeset.date_timestamp() {
None => 0,
Some(date_timestamp) => match (self.include_time, changeset.time) {
(true, Some(time)) => {
let time = Some(time.trim().to_uppercase());
let utc = self.utc_date_time_from_timestamp(date_timestamp);
self.timestamp_from_utc_with_time(&utc, &time)?
}
_ => date_timestamp,
},
};
Ok(cell_data.to_string())
}
}
pub struct DateTimestamp(i64);
impl AsRef<i64> for DateTimestamp {
fn as_ref(&self) -> &i64 {
&self.0
}
}
impl std::convert::From<DateTimestamp> for i64 {
fn from(timestamp: DateTimestamp) -> Self {
timestamp.0
}
}
impl FromCellString for DateTimestamp {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
let num = s.parse::<i64>().unwrap_or(0);
Ok(DateTimestamp(num))
}
}
impl std::convert::From<AnyCellData> for DateTimestamp {
fn from(data: AnyCellData) -> Self {
let num = data.data.parse::<i64>().unwrap_or(0);
DateTimestamp(num)
}
}
#[derive(Default)]
pub struct DateTypeOptionBuilder(DateTypeOption);
impl_into_box_type_option_builder!(DateTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption);
impl DateTypeOptionBuilder {
pub fn date_format(mut self, date_format: DateFormat) -> Self {
self.0.date_format = date_format;
self
}
pub fn time_format(mut self, time_format: TimeFormat) -> Self {
self.0.time_format = time_format;
self
}
}
impl TypeOptionBuilder for DateTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::DateTime
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
pub enum DateFormat {
Local = 0,
US = 1,
ISO = 2,
Friendly = 3,
}
impl std::default::Default for DateFormat {
fn default() -> Self {
DateFormat::Friendly
}
}
impl std::convert::From<i32> for DateFormat {
fn from(value: i32) -> Self {
match value {
0 => DateFormat::Local,
1 => DateFormat::US,
2 => DateFormat::ISO,
3 => DateFormat::Friendly,
_ => {
tracing::error!("Unsupported date format, fallback to friendly");
DateFormat::Friendly
}
}
}
}
impl DateFormat {
pub fn value(&self) -> i32 {
*self as i32
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
DateFormat::Local => "%Y/%m/%d",
DateFormat::US => "%Y/%m/%d",
DateFormat::ISO => "%Y-%m-%d",
DateFormat::Friendly => "%b %d,%Y",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum)]
pub enum TimeFormat {
TwelveHour = 0,
TwentyFourHour = 1,
}
impl std::convert::From<i32> for TimeFormat {
fn from(value: i32) -> Self {
match value {
0 => TimeFormat::TwelveHour,
1 => TimeFormat::TwentyFourHour,
_ => {
tracing::error!("Unsupported time format, fallback to TwentyFourHour");
TimeFormat::TwentyFourHour
}
}
}
}
impl TimeFormat {
pub fn value(&self) -> i32 {
*self as i32
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
TimeFormat::TwelveHour => "%I:%M %p",
TimeFormat::TwentyFourHour => "%R",
}
}
}
impl std::default::Default for TimeFormat {
fn default() -> Self {
TimeFormat::TwentyFourHour
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateCellData {
#[pb(index = 1)]
pub date: String,
#[pb(index = 2)]
pub time: String,
#[pb(index = 3)]
pub timestamp: i64,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateChangesetPayload {
#[pb(index = 1)]
pub cell_identifier: CellIdentifierPayload,
#[pb(index = 2, one_of)]
pub date: Option<String>,
#[pb(index = 3, one_of)]
pub time: Option<String>,
}
pub struct DateChangesetParams {
pub cell_identifier: CellIdentifier,
pub date: Option<String>,
pub time: Option<String>,
}
impl TryInto<DateChangesetParams> for DateChangesetPayload {
type Error = ErrorCode;
fn try_into(self) -> Result<DateChangesetParams, Self::Error> {
let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?;
Ok(DateChangesetParams {
cell_identifier,
date: self.date,
time: self.time,
})
}
}
impl std::convert::From<DateChangesetParams> for CellChangeset {
fn from(params: DateChangesetParams) -> Self {
let changeset = DateCellChangeset {
date: params.date,
time: params.time,
};
let s = serde_json::to_string(&changeset).unwrap();
CellChangeset {
grid_id: params.cell_identifier.grid_id,
row_id: params.cell_identifier.row_id,
field_id: params.cell_identifier.field_id,
content: Some(s),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct DateCellChangeset {
pub date: Option<String>,
pub time: Option<String>,
}
impl DateCellChangeset {
pub fn date_timestamp(&self) -> Option<i64> {
if let Some(date) = &self.date {
match date.parse::<i64>() {
Ok(date_timestamp) => Some(date_timestamp),
Err(_) => None,
}
} else {
None
}
}
}
impl FromCellChangeset for DateCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized,
{
serde_json::from_str::<DateCellChangeset>(&changeset).map_err(internal_error)
}
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellDataChangeset, CellDataOperation};
use crate::services::field::FieldBuilder;
use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat};
use flowy_grid_data_model::revision::FieldRevision;
use strum::IntoEnumIterator;
#[test]
fn date_type_option_invalid_input_test() {
let type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some("1e".to_string()),
time: Some("23:00".to_owned()),
},
&field_type,
&field_rev,
"",
);
}
#[test]
fn date_type_option_date_format_test() {
let mut type_option = DateTypeOption::default();
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
for date_format in DateFormat::iter() {
type_option.date_format = date_format;
match date_format {
DateFormat::Friendly => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022");
}
DateFormat::US => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
}
DateFormat::ISO => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14");
}
DateFormat::Local => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
}
}
}
}
#[test]
fn date_type_option_time_format_test() {
let mut type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for time_format in TimeFormat::iter() {
type_option.time_format = time_format;
type_option.include_time = true;
match time_format {
TimeFormat::TwentyFourHour => {
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("23:00".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 23:00",
);
}
TimeFormat::TwelveHour => {
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
//
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("11:23 pm".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 11:23 PM",
);
}
}
}
}
#[test]
fn date_type_option_apply_changeset_test() {
let mut type_option = DateTypeOption::new();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
type_option.include_time = true;
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: Some("1:00".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 01:00",
);
type_option.time_format = TimeFormat::TwelveHour;
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00 am".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 01:00 AM",
);
}
#[test]
#[should_panic]
fn date_type_option_apply_changeset_error_test() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: Some("1:".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
}
#[test]
#[should_panic]
fn date_type_option_twelve_hours_to_twenty_four_hours() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00 am".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
}
fn assert_changeset_result(
type_option: &DateTypeOption,
changeset: DateCellChangeset,
_field_type: &FieldType,
field_rev: &FieldRevision,
expected: &str,
) {
let changeset = CellDataChangeset(Some(changeset));
let encoded_data = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(encoded_data, type_option, field_rev)
);
}
fn assert_decode_timestamp(
timestamp: i64,
type_option: &DateTypeOption,
field_rev: &FieldRevision,
expected: &str,
) {
let s = serde_json::to_string(&DateCellChangeset {
date: Some(timestamp.to_string()),
time: None,
})
.unwrap();
let encoded_data = type_option.apply_changeset(s.into(), None).unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(encoded_data, type_option, field_rev)
);
}
fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String {
let decoded_data = type_option
.decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev)
.unwrap()
.parse::<DateCellData>()
.unwrap();
if type_option.include_time {
format!("{}{}", decoded_data.date, decoded_data.time)
} else {
decoded_data.date
}
}
}

View File

@ -0,0 +1,199 @@
use crate::entities::{FieldType};
use crate::impl_type_option;
use crate::services::cell::{
AnyCellData, CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, FromCellChangeset,
FromCellString,
};
use crate::services::field::{
BoxTypeOptionBuilder, DateCellChangeset, DateCellData, DateFormat, DateTimestamp, TimeFormat, TypeOptionBuilder,
};
use bytes::Bytes;
use chrono::format::strftime::StrftimeItems;
use chrono::{NaiveDateTime, Timelike};
use flowy_derive::{ProtoBuf};
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use serde::{Deserialize, Serialize};
// Date
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
pub struct DateTypeOption {
#[pb(index = 1)]
pub date_format: DateFormat,
#[pb(index = 2)]
pub time_format: TimeFormat,
#[pb(index = 3)]
pub include_time: bool,
}
impl_type_option!(DateTypeOption, FieldType::DateTime);
impl DateTypeOption {
#[allow(dead_code)]
pub fn new() -> Self {
Self::default()
}
fn today_desc_from_timestamp<T: AsRef<i64>>(&self, timestamp: T) -> DateCellData {
let timestamp = *timestamp.as_ref();
let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0);
self.date_from_native(native)
}
fn date_from_native(&self, native: chrono::NaiveDateTime) -> DateCellData {
if native.timestamp() == 0 {
return DateCellData::default();
}
let time = native.time();
let has_time = time.hour() != 0 || time.second() != 0;
let utc = self.utc_date_time_from_native(native);
let fmt = self.date_format.format_str();
let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt)));
let mut time = "".to_string();
if has_time {
let fmt = format!("{} {}", self.date_format.format_str(), self.time_format.format_str());
time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, "");
}
let timestamp = native.timestamp();
DateCellData { date, time, timestamp }
}
fn date_fmt(&self, time: &Option<String>) -> String {
if self.include_time {
match time.as_ref() {
None => self.date_format.format_str().to_string(),
Some(time_str) => {
if time_str.is_empty() {
self.date_format.format_str().to_string()
} else {
format!("{} {}", self.date_format.format_str(), self.time_format.format_str())
}
}
}
} else {
self.date_format.format_str().to_string()
}
}
fn timestamp_from_utc_with_time(
&self,
utc: &chrono::DateTime<chrono::Utc>,
time: &Option<String>,
) -> FlowyResult<i64> {
if let Some(time_str) = time.as_ref() {
if !time_str.is_empty() {
let date_str = format!(
"{}{}",
utc.format_with_items(StrftimeItems::new(self.date_format.format_str())),
&time_str
);
return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) {
Ok(native) => {
let utc = self.utc_date_time_from_native(native);
Ok(utc.timestamp())
}
Err(_e) => {
let msg = format!("Parse {} failed", date_str);
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg))
}
};
}
}
Ok(utc.timestamp())
}
fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime<chrono::Utc> {
let native = NaiveDateTime::from_timestamp(timestamp, 0);
self.utc_date_time_from_native(native)
}
fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
}
}
impl CellDisplayable<DateTimestamp> for DateTypeOption {
fn display_data(
&self,
cell_data: CellData<DateTimestamp>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
let timestamp = cell_data.try_into_inner()?;
CellBytes::from(self.today_desc_from_timestamp(timestamp))
}
}
impl CellDataOperation<DateTimestamp, DateCellChangeset> for DateTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<DateTimestamp>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
// Return default data if the type_option_cell_data is not FieldType::DateTime.
// It happens when switching from one field to another.
// For example:
// FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen.
if !decoded_field_type.is_date() {
return Ok(CellBytes::default());
}
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<DateCellChangeset>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let cell_data = match changeset.date_timestamp() {
None => 0,
Some(date_timestamp) => match (self.include_time, changeset.time) {
(true, Some(time)) => {
let time = Some(time.trim().to_uppercase());
let utc = self.utc_date_time_from_timestamp(date_timestamp);
self.timestamp_from_utc_with_time(&utc, &time)?
}
_ => date_timestamp,
},
};
Ok(cell_data.to_string())
}
}
#[derive(Default)]
pub struct DateTypeOptionBuilder(DateTypeOption);
impl_into_box_type_option_builder!(DateTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption);
impl DateTypeOptionBuilder {
pub fn date_format(mut self, date_format: DateFormat) -> Self {
self.0.date_format = date_format;
self
}
pub fn time_format(mut self, time_format: TimeFormat) -> Self {
self.0.time_format = time_format;
self
}
}
impl TypeOptionBuilder for DateTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::DateTime
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}

View File

@ -0,0 +1,206 @@
use crate::entities::CellChangeset;
use crate::entities::{CellIdentifier, CellIdentifierPayload};
use crate::services::cell::{AnyCellData, FromCellChangeset, FromCellString};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyResult};
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateCellData {
#[pb(index = 1)]
pub date: String,
#[pb(index = 2)]
pub time: String,
#[pb(index = 3)]
pub timestamp: i64,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateChangesetPayload {
#[pb(index = 1)]
pub cell_identifier: CellIdentifierPayload,
#[pb(index = 2, one_of)]
pub date: Option<String>,
#[pb(index = 3, one_of)]
pub time: Option<String>,
}
pub struct DateChangesetParams {
pub cell_identifier: CellIdentifier,
pub date: Option<String>,
pub time: Option<String>,
}
impl TryInto<DateChangesetParams> for DateChangesetPayload {
type Error = ErrorCode;
fn try_into(self) -> Result<DateChangesetParams, Self::Error> {
let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?;
Ok(DateChangesetParams {
cell_identifier,
date: self.date,
time: self.time,
})
}
}
impl std::convert::From<DateChangesetParams> for CellChangeset {
fn from(params: DateChangesetParams) -> Self {
let changeset = DateCellChangeset {
date: params.date,
time: params.time,
};
let s = serde_json::to_string(&changeset).unwrap();
CellChangeset {
grid_id: params.cell_identifier.grid_id,
row_id: params.cell_identifier.row_id,
field_id: params.cell_identifier.field_id,
content: Some(s),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct DateCellChangeset {
pub date: Option<String>,
pub time: Option<String>,
}
impl DateCellChangeset {
pub fn date_timestamp(&self) -> Option<i64> {
if let Some(date) = &self.date {
match date.parse::<i64>() {
Ok(date_timestamp) => Some(date_timestamp),
Err(_) => None,
}
} else {
None
}
}
}
impl FromCellChangeset for DateCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized,
{
serde_json::from_str::<DateCellChangeset>(&changeset).map_err(internal_error)
}
}
pub struct DateTimestamp(i64);
impl AsRef<i64> for DateTimestamp {
fn as_ref(&self) -> &i64 {
&self.0
}
}
impl std::convert::From<DateTimestamp> for i64 {
fn from(timestamp: DateTimestamp) -> Self {
timestamp.0
}
}
impl FromCellString for DateTimestamp {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
let num = s.parse::<i64>().unwrap_or(0);
Ok(DateTimestamp(num))
}
}
impl std::convert::From<AnyCellData> for DateTimestamp {
fn from(data: AnyCellData) -> Self {
let num = data.data.parse::<i64>().unwrap_or(0);
DateTimestamp(num)
}
}
#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
pub enum DateFormat {
Local = 0,
US = 1,
ISO = 2,
Friendly = 3,
}
impl std::default::Default for DateFormat {
fn default() -> Self {
DateFormat::Friendly
}
}
impl std::convert::From<i32> for DateFormat {
fn from(value: i32) -> Self {
match value {
0 => DateFormat::Local,
1 => DateFormat::US,
2 => DateFormat::ISO,
3 => DateFormat::Friendly,
_ => {
tracing::error!("Unsupported date format, fallback to friendly");
DateFormat::Friendly
}
}
}
}
impl DateFormat {
pub fn value(&self) -> i32 {
*self as i32
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
DateFormat::Local => "%Y/%m/%d",
DateFormat::US => "%Y/%m/%d",
DateFormat::ISO => "%Y-%m-%d",
DateFormat::Friendly => "%b %d,%Y",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum)]
pub enum TimeFormat {
TwelveHour = 0,
TwentyFourHour = 1,
}
impl std::convert::From<i32> for TimeFormat {
fn from(value: i32) -> Self {
match value {
0 => TimeFormat::TwelveHour,
1 => TimeFormat::TwentyFourHour,
_ => {
tracing::error!("Unsupported time format, fallback to TwentyFourHour");
TimeFormat::TwentyFourHour
}
}
}
}
impl TimeFormat {
pub fn value(&self) -> i32 {
*self as i32
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
TimeFormat::TwelveHour => "%I:%M %p",
TimeFormat::TwentyFourHour => "%R",
}
}
}
impl std::default::Default for TimeFormat {
fn default() -> Self {
TimeFormat::TwentyFourHour
}
}

View File

@ -0,0 +1,6 @@
mod date_option;
mod date_option_entities;
mod tests;
pub use date_option::*;
pub use date_option_entities::*;

View File

@ -0,0 +1,272 @@
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellDataChangeset, CellDataOperation};
use crate::services::field::FieldBuilder;
use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat};
use flowy_grid_data_model::revision::FieldRevision;
use strum::IntoEnumIterator;
#[test]
fn date_type_option_invalid_input_test() {
let type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some("1e".to_string()),
time: Some("23:00".to_owned()),
},
&field_type,
&field_rev,
"",
);
}
#[test]
fn date_type_option_date_format_test() {
let mut type_option = DateTypeOption::default();
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
for date_format in DateFormat::iter() {
type_option.date_format = date_format;
match date_format {
DateFormat::Friendly => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022");
}
DateFormat::US => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
}
DateFormat::ISO => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14");
}
DateFormat::Local => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
}
}
}
}
#[test]
fn date_type_option_time_format_test() {
let mut type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for time_format in TimeFormat::iter() {
type_option.time_format = time_format;
type_option.include_time = true;
match time_format {
TimeFormat::TwentyFourHour => {
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("23:00".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 23:00",
);
}
TimeFormat::TwelveHour => {
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
//
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("11:23 pm".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 11:23 PM",
);
}
}
}
}
#[test]
fn date_type_option_apply_changeset_test() {
let mut type_option = DateTypeOption::new();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
type_option.include_time = true;
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: Some("1:00".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 01:00",
);
type_option.time_format = TimeFormat::TwelveHour;
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00 am".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 01:00 AM",
);
}
#[test]
#[should_panic]
fn date_type_option_apply_changeset_error_test() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: Some("1:".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
}
#[test]
#[should_panic]
fn date_type_option_twelve_hours_to_twenty_four_hours() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00 am".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
}
fn assert_changeset_result(
type_option: &DateTypeOption,
changeset: DateCellChangeset,
_field_type: &FieldType,
field_rev: &FieldRevision,
expected: &str,
) {
let changeset = CellDataChangeset(Some(changeset));
let encoded_data = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(encoded_data, type_option, field_rev)
);
}
fn assert_decode_timestamp(
timestamp: i64,
type_option: &DateTypeOption,
field_rev: &FieldRevision,
expected: &str,
) {
let s = serde_json::to_string(&DateCellChangeset {
date: Some(timestamp.to_string()),
time: None,
})
.unwrap();
let encoded_data = type_option.apply_changeset(s.into(), None).unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(encoded_data, type_option, field_rev)
);
}
fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String {
let decoded_data = type_option
.decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev)
.unwrap()
.parse::<DateCellData>()
.unwrap();
if type_option.include_time {
format!("{}{}", decoded_data.date, decoded_data.time)
} else {
decoded_data.date
}
}
}

View File

@ -1,17 +1,14 @@
mod checkbox_type_option;
mod date_type_option;
mod multi_select_type_option;
mod number_type_option;
mod single_select_type_option;
mod text_type_option;
mod url_type_option;
pub mod checkbox_type_option;
pub mod date_type_option;
pub mod number_type_option;
pub mod selection_type_option;
pub mod text_type_option;
pub mod url_type_option;
mod util;
pub use checkbox_type_option::*;
pub use date_type_option::*;
pub use multi_select_type_option::*;
pub use multi_select_type_option::*;
pub use number_type_option::*;
pub use single_select_type_option::*;
pub use selection_type_option::*;
pub use text_type_option::*;
pub use url_type_option::*;

View File

@ -1,6 +1,9 @@
#![allow(clippy::module_inception)]
mod format;
mod number_type_option;
mod number_option;
mod number_option_entities;
mod tests;
pub use format::*;
pub use number_type_option::*;
pub use number_option::*;
pub use number_option_entities::*;

View File

@ -0,0 +1,149 @@
use crate::impl_type_option;
use crate::entities::FieldType;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation};
use crate::services::field::number_currency::Currency;
use crate::services::field::type_options::number_type_option::format::*;
use crate::services::field::{BoxTypeOptionBuilder, NumberCellData, TypeOptionBuilder};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use rust_decimal::Decimal;
use rusty_money::Money;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Default)]
pub struct NumberTypeOptionBuilder(NumberTypeOption);
impl_into_box_type_option_builder!(NumberTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOption);
impl NumberTypeOptionBuilder {
pub fn name(mut self, name: &str) -> Self {
self.0.name = name.to_string();
self
}
pub fn set_format(mut self, format: NumberFormat) -> Self {
self.0.set_format(format);
self
}
pub fn scale(mut self, scale: u32) -> Self {
self.0.scale = scale;
self
}
pub fn positive(mut self, positive: bool) -> Self {
self.0.sign_positive = positive;
self
}
}
impl TypeOptionBuilder for NumberTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::Number
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
// Number
#[derive(Clone, Debug, Serialize, Deserialize, ProtoBuf)]
pub struct NumberTypeOption {
#[pb(index = 1)]
pub format: NumberFormat,
#[pb(index = 2)]
pub scale: u32,
#[pb(index = 3)]
pub symbol: String,
#[pb(index = 4)]
pub sign_positive: bool,
#[pb(index = 5)]
pub name: String,
}
impl_type_option!(NumberTypeOption, FieldType::Number);
impl NumberTypeOption {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn format_cell_data(&self, s: &str) -> FlowyResult<NumberCellData> {
match self.format {
NumberFormat::Num | NumberFormat::Percent => match Decimal::from_str(s) {
Ok(value, ..) => Ok(NumberCellData::from_decimal(value)),
Err(_) => Ok(NumberCellData::new()),
},
_ => NumberCellData::from_format_str(s, self.sign_positive, &self.format),
}
}
pub fn set_format(&mut self, format: NumberFormat) {
self.format = format;
self.symbol = format.symbol();
}
}
pub(crate) fn strip_currency_symbol<T: ToString>(s: T) -> String {
let mut s = s.to_string();
for symbol in CURRENCY_SYMBOL.iter() {
if s.starts_with(symbol) {
s = s.strip_prefix(symbol).unwrap_or("").to_string();
break;
}
}
s
}
impl CellDataOperation<String, String> for NumberTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<String>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if decoded_field_type.is_date() {
return Ok(CellBytes::default());
}
let cell_data: String = cell_data.try_into_inner()?;
match self.format_cell_data(&cell_data) {
Ok(num) => Ok(CellBytes::new(num.to_string())),
Err(_) => Ok(CellBytes::default()),
}
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let data = changeset.trim().to_string();
let _ = self.format_cell_data(&data)?;
Ok(data)
}
}
impl std::default::Default for NumberTypeOption {
fn default() -> Self {
let format = NumberFormat::default();
let symbol = format.symbol();
NumberTypeOption {
format,
scale: 0,
symbol,
sign_positive: true,
name: "Number".to_string(),
}
}
}

View File

@ -0,0 +1,93 @@
use crate::services::field::number_currency::Currency;
use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL};
use flowy_error::{FlowyError, FlowyResult};
use rust_decimal::Decimal;
use rusty_money::Money;
use std::str::FromStr;
#[derive(Default)]
pub struct NumberCellData {
decimal: Option<Decimal>,
money: Option<String>,
}
impl NumberCellData {
pub fn new() -> Self {
Self {
decimal: Default::default(),
money: None,
}
}
pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult<Self> {
let mut num_str = strip_currency_symbol(s);
let currency = format.currency();
if num_str.is_empty() {
return Ok(Self::default());
}
match Decimal::from_str(&num_str) {
Ok(mut decimal) => {
decimal.set_sign_positive(sign_positive);
let money = Money::from_decimal(decimal, currency);
Ok(Self::from_money(money))
}
Err(_) => match Money::from_str(&num_str, currency) {
Ok(money) => Ok(NumberCellData::from_money(money)),
Err(_) => {
num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
if num_str.chars().all(char::is_numeric) {
Self::from_format_str(&num_str, sign_positive, format)
} else {
Err(FlowyError::invalid_data().context("Should only contain numbers"))
}
}
},
}
}
pub fn from_decimal(decimal: Decimal) -> Self {
Self {
decimal: Some(decimal),
money: None,
}
}
pub fn from_money(money: Money<Currency>) -> Self {
Self {
decimal: Some(*money.amount()),
money: Some(money.to_string()),
}
}
pub fn decimal(&self) -> &Option<Decimal> {
&self.decimal
}
pub fn is_empty(&self) -> bool {
self.decimal.is_none()
}
}
impl FromStr for NumberCellData {
type Err = rust_decimal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Ok(Self::default());
}
let decimal = Decimal::from_str(s)?;
Ok(Self::from_decimal(decimal))
}
}
impl ToString for NumberCellData {
fn to_string(&self) -> String {
match &self.money {
None => match self.decimal {
None => String::default(),
Some(decimal) => decimal.to_string(),
},
Some(money) => money.to_string(),
}
}
}

View File

@ -1,376 +0,0 @@
use crate::impl_type_option;
use crate::entities::FieldType;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation};
use crate::services::field::number_currency::Currency;
use crate::services::field::type_options::number_type_option::format::*;
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use rust_decimal::Decimal;
use rusty_money::Money;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Default)]
pub struct NumberTypeOptionBuilder(NumberTypeOption);
impl_into_box_type_option_builder!(NumberTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(NumberTypeOptionBuilder, NumberTypeOption);
impl NumberTypeOptionBuilder {
pub fn name(mut self, name: &str) -> Self {
self.0.name = name.to_string();
self
}
pub fn set_format(mut self, format: NumberFormat) -> Self {
self.0.set_format(format);
self
}
pub fn scale(mut self, scale: u32) -> Self {
self.0.scale = scale;
self
}
pub fn positive(mut self, positive: bool) -> Self {
self.0.sign_positive = positive;
self
}
}
impl TypeOptionBuilder for NumberTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::Number
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
// Number
#[derive(Clone, Debug, Serialize, Deserialize, ProtoBuf)]
pub struct NumberTypeOption {
#[pb(index = 1)]
pub format: NumberFormat,
#[pb(index = 2)]
pub scale: u32,
#[pb(index = 3)]
pub symbol: String,
#[pb(index = 4)]
pub sign_positive: bool,
#[pb(index = 5)]
pub name: String,
}
impl_type_option!(NumberTypeOption, FieldType::Number);
impl NumberTypeOption {
pub fn new() -> Self {
Self::default()
}
pub(crate) fn format_cell_data(&self, s: &str) -> FlowyResult<NumberCellData> {
match self.format {
NumberFormat::Num | NumberFormat::Percent => match Decimal::from_str(s) {
Ok(value, ..) => Ok(NumberCellData::from_decimal(value)),
Err(_) => Ok(NumberCellData::new()),
},
_ => NumberCellData::from_format_str(s, self.sign_positive, &self.format),
}
}
pub fn set_format(&mut self, format: NumberFormat) {
self.format = format;
self.symbol = format.symbol();
}
}
pub(crate) fn strip_currency_symbol<T: ToString>(s: T) -> String {
let mut s = s.to_string();
for symbol in CURRENCY_SYMBOL.iter() {
if s.starts_with(symbol) {
s = s.strip_prefix(symbol).unwrap_or("").to_string();
break;
}
}
s
}
impl CellDataOperation<String, String> for NumberTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<String>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if decoded_field_type.is_date() {
return Ok(CellBytes::default());
}
let cell_data: String = cell_data.try_into_inner()?;
match self.format_cell_data(&cell_data) {
Ok(num) => Ok(CellBytes::new(num.to_string())),
Err(_) => Ok(CellBytes::default()),
}
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let data = changeset.trim().to_string();
let _ = self.format_cell_data(&data)?;
Ok(data)
}
}
impl std::default::Default for NumberTypeOption {
fn default() -> Self {
let format = NumberFormat::default();
let symbol = format.symbol();
NumberTypeOption {
format,
scale: 0,
symbol,
sign_positive: true,
name: "Number".to_string(),
}
}
}
#[derive(Default)]
pub struct NumberCellData {
decimal: Option<Decimal>,
money: Option<String>,
}
impl NumberCellData {
pub fn new() -> Self {
Self {
decimal: Default::default(),
money: None,
}
}
pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult<Self> {
let mut num_str = strip_currency_symbol(s);
let currency = format.currency();
if num_str.is_empty() {
return Ok(Self::default());
}
match Decimal::from_str(&num_str) {
Ok(mut decimal) => {
decimal.set_sign_positive(sign_positive);
let money = Money::from_decimal(decimal, currency);
Ok(Self::from_money(money))
}
Err(_) => match Money::from_str(&num_str, currency) {
Ok(money) => Ok(NumberCellData::from_money(money)),
Err(_) => {
num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
if num_str.chars().all(char::is_numeric) {
Self::from_format_str(&num_str, sign_positive, format)
} else {
Err(FlowyError::invalid_data().context("Should only contain numbers"))
}
}
},
}
}
pub fn from_decimal(decimal: Decimal) -> Self {
Self {
decimal: Some(decimal),
money: None,
}
}
pub fn from_money(money: Money<Currency>) -> Self {
Self {
decimal: Some(*money.amount()),
money: Some(money.to_string()),
}
}
pub fn decimal(&self) -> &Option<Decimal> {
&self.decimal
}
pub fn is_empty(&self) -> bool {
self.decimal.is_none()
}
}
impl FromStr for NumberCellData {
type Err = rust_decimal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Ok(Self::default());
}
let decimal = Decimal::from_str(s)?;
Ok(Self::from_decimal(decimal))
}
}
impl ToString for NumberCellData {
fn to_string(&self) -> String {
match &self.money {
None => match self.decimal {
None => String::default(),
Some(decimal) => decimal.to_string(),
},
Some(money) => money.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::FieldBuilder;
use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption};
use flowy_grid_data_model::revision::FieldRevision;
use strum::IntoEnumIterator;
#[test]
fn number_type_option_invalid_input_test() {
let type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_equal(&type_option, "", "", &field_type, &field_rev);
assert_equal(&type_option, "abc", "", &field_type, &field_rev);
}
#[test]
fn number_type_option_strip_symbol_test() {
let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::USD;
assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned());
type_option.format = NumberFormat::Yuan;
assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned());
}
#[test]
fn number_type_option_format_number_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_rev);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev);
}
_ => {}
}
}
}
#[test]
fn number_type_option_format_str_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
assert_equal(&type_option, "0.2", "0.2", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_rev);
assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_rev);
assert_equal(&type_option, "", "", &field_type, &field_rev);
assert_equal(&type_option, "abc", "", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev);
assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_rev);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_rev);
assert_equal(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_rev);
assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_rev);
assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_rev);
}
_ => {}
}
}
}
#[test]
fn number_description_sign_test() {
let mut type_option = NumberTypeOption {
sign_positive: false,
..Default::default()
};
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev);
}
_ => {}
}
}
}
fn assert_equal(
type_option: &NumberTypeOption,
cell_data: &str,
expected_str: &str,
field_type: &FieldType,
field_rev: &FieldRevision,
) {
assert_eq!(
type_option
.decode_cell_data(cell_data.to_owned().into(), field_type, field_rev)
.unwrap()
.to_string(),
expected_str.to_owned()
);
}
}

View File

@ -0,0 +1,139 @@
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::FieldBuilder;
use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption};
use flowy_grid_data_model::revision::FieldRevision;
use strum::IntoEnumIterator;
#[test]
fn number_type_option_invalid_input_test() {
let type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_equal(&type_option, "", "", &field_type, &field_rev);
assert_equal(&type_option, "abc", "", &field_type, &field_rev);
}
#[test]
fn number_type_option_strip_symbol_test() {
let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::USD;
assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned());
type_option.format = NumberFormat::Yuan;
assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned());
}
#[test]
fn number_type_option_format_number_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_rev);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev);
}
_ => {}
}
}
}
#[test]
fn number_type_option_format_str_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
assert_equal(&type_option, "0.2", "0.2", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_rev);
assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_rev);
assert_equal(&type_option, "", "", &field_type, &field_rev);
assert_equal(&type_option, "abc", "", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev);
assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_rev);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_rev);
assert_equal(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_rev);
assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_rev);
assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_rev);
}
_ => {}
}
}
}
#[test]
fn number_description_sign_test() {
let mut type_option = NumberTypeOption {
sign_positive: false,
..Default::default()
};
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev);
}
_ => {}
}
}
}
fn assert_equal(
type_option: &NumberTypeOption,
cell_data: &str,
expected_str: &str,
field_type: &FieldType,
field_rev: &FieldRevision,
) {
assert_eq!(
type_option
.decode_cell_data(cell_data.to_owned().into(), field_type, field_rev)
.unwrap()
.to_string(),
expected_str.to_owned()
);
}
}

View File

@ -0,0 +1,7 @@
mod multi_select_type_option;
mod select_option;
mod single_select_type_option;
pub use multi_select_type_option::*;
pub use select_option::*;
pub use single_select_type_option::*;

View File

@ -1,19 +1,15 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
use crate::services::field::select_option::{
make_selected_select_options, SelectOption, SelectOptionCellChangeset, 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::field::{
make_selected_select_options, BoxTypeOptionBuilder, SelectOption, SelectOptionCellChangeset, SelectOptionCellData,
SelectOptionIds, SelectOptionOperation, TypeOptionBuilder, SELECTION_IDS_SEPARATOR,
};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use serde::{Deserialize, Serialize};
// Multiple select
@ -56,8 +52,7 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for MultiSele
return Ok(CellBytes::default());
}
let cell_data = self.display_data(cell_data, decoded_field_type, field_rev)?;
CellBytes::from(cell_data)
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
@ -121,7 +116,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::select_option::*;
use crate::services::field::type_options::selection_type_option::*;
use crate::services::field::FieldBuilder;
use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder};
use flowy_grid_data_model::revision::FieldRevision;

View File

@ -1,5 +1,5 @@
use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType};
use crate::services::cell::{AnyCellData, CellData, CellDisplayable, FromCellChangeset, FromCellString};
use crate::services::cell::{AnyCellData, CellBytes, CellData, CellDisplayable, FromCellChangeset, FromCellString};
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
@ -106,7 +106,7 @@ pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
}
impl<T> CellDisplayable<SelectOptionIds, SelectOptionCellData> for T
impl<T> CellDisplayable<SelectOptionIds> for T
where
T: SelectOptionOperation,
{
@ -115,8 +115,8 @@ where
cell_data: CellData<SelectOptionIds>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<SelectOptionCellData> {
Ok(self.selected_select_option(cell_data))
) -> FlowyResult<CellBytes> {
CellBytes::from(self.selected_select_option(cell_data))
}
}

View File

@ -1,7 +1,7 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
use crate::services::field::select_option::{
use crate::services::field::{
make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds,
SelectOptionOperation,
};
@ -54,8 +54,7 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for SingleSel
return Ok(CellBytes::default());
}
let cell_data = self.display_data(cell_data, decoded_field_type, field_rev)?;
CellBytes::from(cell_data)
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
@ -103,7 +102,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::select_option::*;
use crate::services::field::type_options::*;
use crate::services::field::FieldBuilder;
use flowy_grid_data_model::revision::FieldRevision;

View File

@ -0,0 +1,2 @@
mod text_option;
pub use text_option::*;

View File

@ -32,15 +32,15 @@ pub struct RichTextTypeOption {
}
impl_type_option!(RichTextTypeOption, FieldType::RichText);
impl CellDisplayable<String, String> for RichTextTypeOption {
impl CellDisplayable<String> for RichTextTypeOption {
fn display_data(
&self,
cell_data: CellData<String>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<String> {
) -> FlowyResult<CellBytes> {
let cell_str: String = cell_data.try_into_inner()?;
Ok(cell_str)
Ok(CellBytes::new(cell_str))
}
}
@ -58,8 +58,7 @@ impl CellDataOperation<String, String> for RichTextTypeOption {
{
try_decode_cell_data(cell_data, field_rev, decoded_field_type, decoded_field_type)
} else {
let content = self.display_data(cell_data, decoded_field_type, field_rev)?;
Ok(CellBytes::new(content))
self.display_data(cell_data, decoded_field_type, field_rev)
}
}
@ -96,7 +95,7 @@ impl std::convert::TryFrom<AnyCellData> for TextCellData {
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::select_option::*;
use crate::services::field::FieldBuilder;
use crate::services::field::*;

View File

@ -1,206 +0,0 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{
AnyCellData, CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, FromCellString,
};
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes;
use fancy_regex::Regex;
use flowy_derive::ProtoBuf;
use flowy_error::{internal_error, FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct URLTypeOptionBuilder(URLTypeOption);
impl_into_box_type_option_builder!(URLTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption);
impl TypeOptionBuilder for URLTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::URL
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
pub struct URLTypeOption {
#[pb(index = 1)]
data: String, //It's not used yet.
}
impl_type_option!(URLTypeOption, FieldType::URL);
impl CellDisplayable<URLCellData, URLCellData> for URLTypeOption {
fn display_data(
&self,
cell_data: CellData<URLCellData>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<URLCellData> {
let cell_data: URLCellData = cell_data.try_into_inner()?;
Ok(cell_data)
}
}
impl CellDataOperation<URLCellData, String> for URLTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<URLCellData>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if !decoded_field_type.is_url() {
return Ok(CellBytes::default());
}
let cell_data = self.display_data(cell_data, decoded_field_type, field_rev)?;
CellBytes::from(cell_data)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let mut url = "".to_string();
if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
url = auto_append_scheme(m.as_str());
}
URLCellData {
url,
content: changeset,
}
.to_json()
}
}
fn auto_append_scheme(s: &str) -> String {
// Only support https scheme by now
match url::Url::parse(s) {
Ok(url) => {
if url.scheme() == "https" {
url.into()
} else {
format!("https://{}", s)
}
}
Err(_) => {
format!("https://{}", s)
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
pub struct URLCellData {
#[pb(index = 1)]
pub url: String,
#[pb(index = 2)]
pub content: String,
}
impl URLCellData {
pub fn new(s: &str) -> Self {
Self {
url: "".to_string(),
content: s.to_string(),
}
}
fn to_json(&self) -> FlowyResult<String> {
serde_json::to_string(self).map_err(internal_error)
}
}
impl FromCellString for URLCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self> {
serde_json::from_str::<URLCellData>(s).map_err(internal_error)
}
}
impl std::convert::TryFrom<AnyCellData> for URLCellData {
type Error = FlowyError;
fn try_from(data: AnyCellData) -> Result<Self, Self::Error> {
serde_json::from_str::<URLCellData>(&data.data).map_err(internal_error)
}
}
lazy_static! {
static ref URL_REGEX: Regex = Regex::new(
"[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)"
)
.unwrap();
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellData, CellDataOperation};
use crate::services::field::FieldBuilder;
use crate::services::field::{URLCellData, URLTypeOption};
use flowy_grid_data_model::revision::FieldRevision;
#[test]
fn url_type_option_test_no_url() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_changeset(&type_option, "123", &field_type, &field_rev, "123", "");
}
#[test]
fn url_type_option_test_contains_url() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_changeset(
&type_option,
"AppFlowy website - https://www.appflowy.io",
&field_type,
&field_rev,
"AppFlowy website - https://www.appflowy.io",
"https://www.appflowy.io/",
);
assert_changeset(
&type_option,
"AppFlowy website appflowy.io",
&field_type,
&field_rev,
"AppFlowy website appflowy.io",
"https://appflowy.io",
);
}
fn assert_changeset(
type_option: &URLTypeOption,
cell_data: &str,
field_type: &FieldType,
field_rev: &FieldRevision,
expected: &str,
expected_url: &str,
) {
let encoded_data = type_option.apply_changeset(cell_data.to_owned().into(), None).unwrap();
let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type);
assert_eq!(expected.to_owned(), decode_cell_data.content);
assert_eq!(expected_url.to_owned(), decode_cell_data.url);
}
fn decode_cell_data<T: Into<CellData<URLCellData>>>(
encoded_data: T,
type_option: &URLTypeOption,
field_rev: &FieldRevision,
field_type: &FieldType,
) -> URLCellData {
type_option
.decode_cell_data(encoded_data.into(), field_type, field_rev)
.unwrap()
.parse::<URLCellData>()
.unwrap()
}
}

View File

@ -0,0 +1,6 @@
mod tests;
mod url_option;
mod url_option_entities;
pub use url_option::*;
pub use url_option_entities::*;

View File

@ -0,0 +1,67 @@
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellData, CellDataOperation};
use crate::services::field::FieldBuilder;
use crate::services::field::{URLCellData, URLTypeOption};
use flowy_grid_data_model::revision::FieldRevision;
#[test]
fn url_type_option_test_no_url() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_changeset(&type_option, "123", &field_type, &field_rev, "123", "");
}
#[test]
fn url_type_option_test_contains_url() {
let type_option = URLTypeOption::default();
let field_type = FieldType::URL;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_changeset(
&type_option,
"AppFlowy website - https://www.appflowy.io",
&field_type,
&field_rev,
"AppFlowy website - https://www.appflowy.io",
"https://www.appflowy.io/",
);
assert_changeset(
&type_option,
"AppFlowy website appflowy.io",
&field_type,
&field_rev,
"AppFlowy website appflowy.io",
"https://appflowy.io",
);
}
fn assert_changeset(
type_option: &URLTypeOption,
cell_data: &str,
field_type: &FieldType,
field_rev: &FieldRevision,
expected: &str,
expected_url: &str,
) {
let encoded_data = type_option.apply_changeset(cell_data.to_owned().into(), None).unwrap();
let decode_cell_data = decode_cell_data(encoded_data, type_option, field_rev, field_type);
assert_eq!(expected.to_owned(), decode_cell_data.content);
assert_eq!(expected_url.to_owned(), decode_cell_data.url);
}
fn decode_cell_data<T: Into<CellData<URLCellData>>>(
encoded_data: T,
type_option: &URLTypeOption,
field_rev: &FieldRevision,
field_type: &FieldType,
) -> URLCellData {
type_option
.decode_cell_data(encoded_data.into(), field_type, field_rev)
.unwrap()
.parse::<URLCellData>()
.unwrap()
}
}

View File

@ -0,0 +1,101 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{
AnyCellData, CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable, FromCellString,
};
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder, URLCellData};
use bytes::Bytes;
use fancy_regex::Regex;
use flowy_derive::ProtoBuf;
use flowy_error::{internal_error, FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct URLTypeOptionBuilder(URLTypeOption);
impl_into_box_type_option_builder!(URLTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(URLTypeOptionBuilder, URLTypeOption);
impl TypeOptionBuilder for URLTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::URL
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
pub struct URLTypeOption {
#[pb(index = 1)]
data: String, //It's not used yet.
}
impl_type_option!(URLTypeOption, FieldType::URL);
impl CellDisplayable<URLCellData> for URLTypeOption {
fn display_data(
&self,
cell_data: CellData<URLCellData>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
let cell_data: URLCellData = cell_data.try_into_inner()?;
CellBytes::from(cell_data)
}
}
impl CellDataOperation<URLCellData, String> for URLTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<URLCellData>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if !decoded_field_type.is_url() {
return Ok(CellBytes::default());
}
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let mut url = "".to_string();
if let Ok(Some(m)) = URL_REGEX.find(&changeset) {
url = auto_append_scheme(m.as_str());
}
URLCellData {
url,
content: changeset,
}
.to_json()
}
}
fn auto_append_scheme(s: &str) -> String {
// Only support https scheme by now
match url::Url::parse(s) {
Ok(url) => {
if url.scheme() == "https" {
url.into()
} else {
format!("https://{}", s)
}
}
Err(_) => {
format!("https://{}", s)
}
}
}
lazy_static! {
static ref URL_REGEX: Regex = Regex::new(
"[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)"
)
.unwrap();
}

View File

@ -0,0 +1,40 @@
use crate::services::cell::{AnyCellData, FromCellString};
use flowy_derive::ProtoBuf;
use flowy_error::{internal_error, FlowyError, FlowyResult};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize, ProtoBuf)]
pub struct URLCellData {
#[pb(index = 1)]
pub url: String,
#[pb(index = 2)]
pub content: String,
}
impl URLCellData {
pub fn new(s: &str) -> Self {
Self {
url: "".to_string(),
content: s.to_string(),
}
}
pub(crate) fn to_json(&self) -> FlowyResult<String> {
serde_json::to_string(self).map_err(internal_error)
}
}
impl FromCellString for URLCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self> {
serde_json::from_str::<URLCellData>(s).map_err(internal_error)
}
}
impl std::convert::TryFrom<AnyCellData> for URLCellData {
type Error = FlowyError;
fn try_from(data: AnyCellData) -> Result<Self, Self::Error> {
serde_json::from_str::<URLCellData>(&data.data).map_err(internal_error)
}
}

View File

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

View File

@ -2,8 +2,8 @@
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::field::select_option::{SelectOptionOperation, SelectedSelectOptions};
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
use crate::services::field::{SelectOptionOperation, SelectedSelectOptions};
use flowy_error::FlowyResult;
impl GridSelectOptionFilter {
@ -64,7 +64,7 @@ impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
mod tests {
#![allow(clippy::all)]
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
use crate::services::field::select_option::{SelectOption, SelectedSelectOptions};
use crate::services::field::selection_type_option::{SelectOption, SelectedSelectOptions};
#[test]
fn select_option_filter_is_test() {

View File

@ -1,5 +1,5 @@
use crate::services::cell::apply_cell_data_changeset;
use crate::services::field::select_option::SelectOptionCellChangeset;
use crate::services::field::SelectOptionCellChangeset;
use flowy_error::{FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
use indexmap::IndexMap;

View File

@ -1,5 +1,5 @@
use flowy_grid::entities::FieldType;
use flowy_grid::services::field::select_option::{SelectOption, SELECTION_IDS_SEPARATOR};
use flowy_grid::services::field::selection_type_option::{SelectOption, SELECTION_IDS_SEPARATOR};
use flowy_grid::services::field::{DateCellChangeset, MultiSelectTypeOption, SingleSelectTypeOption};
use flowy_grid::services::row::RowRevisionBuilder;
use flowy_grid_data_model::revision::{FieldRevision, RowRevision};

View File

@ -2,7 +2,7 @@ use crate::grid::cell_test::script::CellScript::*;
use crate::grid::cell_test::script::GridCellTest;
use crate::grid::field_test::util::make_date_cell_string;
use flowy_grid::entities::{CellChangeset, FieldType};
use flowy_grid::services::field::select_option::SelectOptionCellChangeset;
use flowy_grid::services::field::selection_type_option::SelectOptionCellChangeset;
use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
#[tokio::test]

View File

@ -1,7 +1,7 @@
use crate::grid::field_test::script::FieldScript::*;
use crate::grid::field_test::script::GridFieldTest;
use crate::grid::field_test::util::*;
use flowy_grid::services::field::select_option::SelectOption;
use flowy_grid::services::field::selection_type_option::SelectOption;
use flowy_grid::services::field::SingleSelectTypeOption;
use flowy_grid_data_model::revision::TypeOptionDataEntry;
use flowy_sync::entities::grid::FieldChangesetParams;

View File

@ -1,5 +1,5 @@
use flowy_grid::entities::*;
use flowy_grid::services::field::select_option::SelectOption;
use flowy_grid::services::field::selection_type_option::SelectOption;
use flowy_grid::services::field::*;
use flowy_grid_data_model::revision::*;

View File

@ -3,7 +3,7 @@
#![allow(unused_imports)]
use bytes::Bytes;
use flowy_grid::entities::*;
use flowy_grid::services::field::select_option::SelectOption;
use flowy_grid::services::field::SelectOption;
use flowy_grid::services::field::*;
use flowy_grid::services::grid_editor::{GridPadBuilder, GridRevisionEditor};
use flowy_grid::services::row::{CreateRowRevisionPayload, RowRevisionBuilder};