mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: type options directory
This commit is contained in:
parent
1e3640f8ac
commit
f10e324b73
@ -2,7 +2,6 @@
|
|||||||
proto_input = [
|
proto_input = [
|
||||||
"src/event_map.rs",
|
"src/event_map.rs",
|
||||||
"src/services/field/type_options",
|
"src/services/field/type_options",
|
||||||
"src/services/field/select_option.rs",
|
|
||||||
"src/entities",
|
"src/entities",
|
||||||
"src/dart_notification.rs"
|
"src/dart_notification.rs"
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use crate::services::field::select_option::SelectOptionIds;
|
use crate::services::field::SelectOptionIds;
|
||||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
use flowy_error::ErrorCode;
|
use flowy_error::ErrorCode;
|
||||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
use crate::entities::*;
|
use crate::entities::*;
|
||||||
use crate::manager::GridManager;
|
use crate::manager::GridManager;
|
||||||
use crate::services::cell::AnyCellData;
|
use crate::services::cell::AnyCellData;
|
||||||
use crate::services::field::select_option::*;
|
|
||||||
use crate::services::field::{
|
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 crate::services::row::make_row_from_row_rev;
|
||||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||||
|
@ -12,13 +12,13 @@ pub trait CellFilterOperation<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Return object that describes the cell.
|
/// Return object that describes the cell.
|
||||||
pub trait CellDisplayable<CD, DC> {
|
pub trait CellDisplayable<CD> {
|
||||||
fn display_data(
|
fn display_data(
|
||||||
&self,
|
&self,
|
||||||
cell_data: CellData<CD>,
|
cell_data: CellData<CD>,
|
||||||
decoded_field_type: &FieldType,
|
decoded_field_type: &FieldType,
|
||||||
field_rev: &FieldRevision,
|
field_rev: &FieldRevision,
|
||||||
) -> FlowyResult<DC>;
|
) -> FlowyResult<CellBytes>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CD: Short for CellData. This type is the type return by apply_changeset function.
|
// CD: Short for CellData. This type is the type return by apply_changeset function.
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
mod field_builder;
|
mod field_builder;
|
||||||
pub mod select_option;
|
|
||||||
pub(crate) mod type_options;
|
pub(crate) mod type_options;
|
||||||
|
|
||||||
pub use field_builder::*;
|
pub use field_builder::*;
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
mod checkbox_option;
|
||||||
|
mod checkbox_option_entities;
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub use checkbox_option::*;
|
||||||
|
pub use checkbox_option_entities::*;
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
mod date_option;
|
||||||
|
mod date_option_entities;
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub use date_option::*;
|
||||||
|
pub use date_option_entities::*;
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,14 @@
|
|||||||
mod checkbox_type_option;
|
pub mod checkbox_type_option;
|
||||||
mod date_type_option;
|
pub mod date_type_option;
|
||||||
mod multi_select_type_option;
|
pub mod number_type_option;
|
||||||
mod number_type_option;
|
pub mod selection_type_option;
|
||||||
mod single_select_type_option;
|
pub mod text_type_option;
|
||||||
mod text_type_option;
|
pub mod url_type_option;
|
||||||
mod url_type_option;
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
pub use checkbox_type_option::*;
|
pub use checkbox_type_option::*;
|
||||||
pub use date_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 number_type_option::*;
|
||||||
pub use single_select_type_option::*;
|
pub use selection_type_option::*;
|
||||||
pub use text_type_option::*;
|
pub use text_type_option::*;
|
||||||
pub use url_type_option::*;
|
pub use url_type_option::*;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
#![allow(clippy::module_inception)]
|
#![allow(clippy::module_inception)]
|
||||||
mod format;
|
mod format;
|
||||||
mod number_type_option;
|
mod number_option;
|
||||||
|
mod number_option_entities;
|
||||||
|
mod tests;
|
||||||
|
|
||||||
pub use format::*;
|
pub use format::*;
|
||||||
pub use number_type_option::*;
|
pub use number_option::*;
|
||||||
|
pub use number_option_entities::*;
|
||||||
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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::*;
|
@ -1,19 +1,15 @@
|
|||||||
use crate::entities::FieldType;
|
use crate::entities::FieldType;
|
||||||
|
|
||||||
use crate::impl_type_option;
|
use crate::impl_type_option;
|
||||||
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
|
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::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 bytes::Bytes;
|
||||||
use flowy_derive::ProtoBuf;
|
use flowy_derive::ProtoBuf;
|
||||||
use flowy_error::{FlowyError, FlowyResult};
|
use flowy_error::{FlowyError, FlowyResult};
|
||||||
|
|
||||||
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
|
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
// Multiple select
|
// Multiple select
|
||||||
@ -56,8 +52,7 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for MultiSele
|
|||||||
return Ok(CellBytes::default());
|
return Ok(CellBytes::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let cell_data = self.display_data(cell_data, decoded_field_type, field_rev)?;
|
self.display_data(cell_data, decoded_field_type, field_rev)
|
||||||
CellBytes::from(cell_data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_changeset(
|
fn apply_changeset(
|
||||||
@ -121,7 +116,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::entities::FieldType;
|
use crate::entities::FieldType;
|
||||||
use crate::services::cell::CellDataOperation;
|
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::FieldBuilder;
|
||||||
use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder};
|
use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder};
|
||||||
use flowy_grid_data_model::revision::FieldRevision;
|
use flowy_grid_data_model::revision::FieldRevision;
|
@ -1,5 +1,5 @@
|
|||||||
use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType};
|
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 crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||||
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
|
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>;
|
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> CellDisplayable<SelectOptionIds, SelectOptionCellData> for T
|
impl<T> CellDisplayable<SelectOptionIds> for T
|
||||||
where
|
where
|
||||||
T: SelectOptionOperation,
|
T: SelectOptionOperation,
|
||||||
{
|
{
|
||||||
@ -115,8 +115,8 @@ where
|
|||||||
cell_data: CellData<SelectOptionIds>,
|
cell_data: CellData<SelectOptionIds>,
|
||||||
_decoded_field_type: &FieldType,
|
_decoded_field_type: &FieldType,
|
||||||
_field_rev: &FieldRevision,
|
_field_rev: &FieldRevision,
|
||||||
) -> FlowyResult<SelectOptionCellData> {
|
) -> FlowyResult<CellBytes> {
|
||||||
Ok(self.selected_select_option(cell_data))
|
CellBytes::from(self.selected_select_option(cell_data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
use crate::entities::FieldType;
|
use crate::entities::FieldType;
|
||||||
use crate::impl_type_option;
|
use crate::impl_type_option;
|
||||||
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
|
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,
|
make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds,
|
||||||
SelectOptionOperation,
|
SelectOptionOperation,
|
||||||
};
|
};
|
||||||
@ -54,8 +54,7 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for SingleSel
|
|||||||
return Ok(CellBytes::default());
|
return Ok(CellBytes::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let cell_data = self.display_data(cell_data, decoded_field_type, field_rev)?;
|
self.display_data(cell_data, decoded_field_type, field_rev)
|
||||||
CellBytes::from(cell_data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_changeset(
|
fn apply_changeset(
|
||||||
@ -103,7 +102,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::entities::FieldType;
|
use crate::entities::FieldType;
|
||||||
use crate::services::cell::CellDataOperation;
|
use crate::services::cell::CellDataOperation;
|
||||||
use crate::services::field::select_option::*;
|
|
||||||
use crate::services::field::type_options::*;
|
use crate::services::field::type_options::*;
|
||||||
use crate::services::field::FieldBuilder;
|
use crate::services::field::FieldBuilder;
|
||||||
use flowy_grid_data_model::revision::FieldRevision;
|
use flowy_grid_data_model::revision::FieldRevision;
|
@ -0,0 +1,2 @@
|
|||||||
|
mod text_option;
|
||||||
|
pub use text_option::*;
|
@ -32,15 +32,15 @@ pub struct RichTextTypeOption {
|
|||||||
}
|
}
|
||||||
impl_type_option!(RichTextTypeOption, FieldType::RichText);
|
impl_type_option!(RichTextTypeOption, FieldType::RichText);
|
||||||
|
|
||||||
impl CellDisplayable<String, String> for RichTextTypeOption {
|
impl CellDisplayable<String> for RichTextTypeOption {
|
||||||
fn display_data(
|
fn display_data(
|
||||||
&self,
|
&self,
|
||||||
cell_data: CellData<String>,
|
cell_data: CellData<String>,
|
||||||
_decoded_field_type: &FieldType,
|
_decoded_field_type: &FieldType,
|
||||||
_field_rev: &FieldRevision,
|
_field_rev: &FieldRevision,
|
||||||
) -> FlowyResult<String> {
|
) -> FlowyResult<CellBytes> {
|
||||||
let cell_str: String = cell_data.try_into_inner()?;
|
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)
|
try_decode_cell_data(cell_data, field_rev, decoded_field_type, decoded_field_type)
|
||||||
} else {
|
} else {
|
||||||
let content = self.display_data(cell_data, decoded_field_type, field_rev)?;
|
self.display_data(cell_data, decoded_field_type, field_rev)
|
||||||
Ok(CellBytes::new(content))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +95,7 @@ impl std::convert::TryFrom<AnyCellData> for TextCellData {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use crate::entities::FieldType;
|
use crate::entities::FieldType;
|
||||||
use crate::services::cell::CellDataOperation;
|
use crate::services::cell::CellDataOperation;
|
||||||
use crate::services::field::select_option::*;
|
|
||||||
use crate::services::field::FieldBuilder;
|
use crate::services::field::FieldBuilder;
|
||||||
use crate::services::field::*;
|
use crate::services::field::*;
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,6 @@
|
|||||||
|
mod tests;
|
||||||
|
mod url_option;
|
||||||
|
mod url_option_entities;
|
||||||
|
|
||||||
|
pub use url_option::*;
|
||||||
|
pub use url_option_entities::*;
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
mod cell_data_util;
|
mod cell_data_util;
|
||||||
|
|
||||||
pub use crate::services::field::select_option::*;
|
|
||||||
pub use cell_data_util::*;
|
pub use cell_data_util::*;
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
|
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
|
||||||
use crate::services::cell::{AnyCellData, CellFilterOperation};
|
use crate::services::cell::{AnyCellData, CellFilterOperation};
|
||||||
use crate::services::field::select_option::{SelectOptionOperation, SelectedSelectOptions};
|
|
||||||
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||||
|
use crate::services::field::{SelectOptionOperation, SelectedSelectOptions};
|
||||||
use flowy_error::FlowyResult;
|
use flowy_error::FlowyResult;
|
||||||
|
|
||||||
impl GridSelectOptionFilter {
|
impl GridSelectOptionFilter {
|
||||||
@ -64,7 +64,7 @@ impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
|
|||||||
mod tests {
|
mod tests {
|
||||||
#![allow(clippy::all)]
|
#![allow(clippy::all)]
|
||||||
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
|
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
|
||||||
use crate::services::field::select_option::{SelectOption, SelectedSelectOptions};
|
use crate::services::field::selection_type_option::{SelectOption, SelectedSelectOptions};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn select_option_filter_is_test() {
|
fn select_option_filter_is_test() {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use crate::services::cell::apply_cell_data_changeset;
|
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_error::{FlowyError, FlowyResult};
|
||||||
use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
|
use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use flowy_grid::entities::FieldType;
|
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::field::{DateCellChangeset, MultiSelectTypeOption, SingleSelectTypeOption};
|
||||||
use flowy_grid::services::row::RowRevisionBuilder;
|
use flowy_grid::services::row::RowRevisionBuilder;
|
||||||
use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
|
use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
|
||||||
|
@ -2,7 +2,7 @@ use crate::grid::cell_test::script::CellScript::*;
|
|||||||
use crate::grid::cell_test::script::GridCellTest;
|
use crate::grid::cell_test::script::GridCellTest;
|
||||||
use crate::grid::field_test::util::make_date_cell_string;
|
use crate::grid::field_test::util::make_date_cell_string;
|
||||||
use flowy_grid::entities::{CellChangeset, FieldType};
|
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};
|
use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::grid::field_test::script::FieldScript::*;
|
use crate::grid::field_test::script::FieldScript::*;
|
||||||
use crate::grid::field_test::script::GridFieldTest;
|
use crate::grid::field_test::script::GridFieldTest;
|
||||||
use crate::grid::field_test::util::*;
|
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::services::field::SingleSelectTypeOption;
|
||||||
use flowy_grid_data_model::revision::TypeOptionDataEntry;
|
use flowy_grid_data_model::revision::TypeOptionDataEntry;
|
||||||
use flowy_sync::entities::grid::FieldChangesetParams;
|
use flowy_sync::entities::grid::FieldChangesetParams;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use flowy_grid::entities::*;
|
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::services::field::*;
|
||||||
use flowy_grid_data_model::revision::*;
|
use flowy_grid_data_model::revision::*;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
#![allow(unused_imports)]
|
#![allow(unused_imports)]
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flowy_grid::entities::*;
|
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::field::*;
|
||||||
use flowy_grid::services::grid_editor::{GridPadBuilder, GridRevisionEditor};
|
use flowy_grid::services::grid_editor::{GridPadBuilder, GridRevisionEditor};
|
||||||
use flowy_grid::services::row::{CreateRowRevisionPayload, RowRevisionBuilder};
|
use flowy_grid::services::row::{CreateRowRevisionPayload, RowRevisionBuilder};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user