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 = [
|
||||
"src/event_map.rs",
|
||||
"src/services/field/type_options",
|
||||
"src/services/field/select_option.rs",
|
||||
"src/entities",
|
||||
"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_error::ErrorCode;
|
||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||
|
@ -1,9 +1,11 @@
|
||||
use crate::entities::*;
|
||||
use crate::manager::GridManager;
|
||||
use crate::services::cell::AnyCellData;
|
||||
use crate::services::field::select_option::*;
|
||||
use crate::services::field::{
|
||||
default_type_option_builder_from_type, type_option_builder_from_json_str, DateChangesetParams, DateChangesetPayload,
|
||||
default_type_option_builder_from_type, select_option_operation, type_option_builder_from_json_str,
|
||||
DateChangesetParams, DateChangesetPayload, SelectOption, SelectOptionCellChangeset,
|
||||
SelectOptionCellChangesetParams, SelectOptionCellChangesetPayload, SelectOptionCellData, SelectOptionChangeset,
|
||||
SelectOptionChangesetPayload,
|
||||
};
|
||||
use crate::services::row::make_row_from_row_rev;
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
|
@ -12,13 +12,13 @@ pub trait CellFilterOperation<T> {
|
||||
}
|
||||
|
||||
/// Return object that describes the cell.
|
||||
pub trait CellDisplayable<CD, DC> {
|
||||
pub trait CellDisplayable<CD> {
|
||||
fn display_data(
|
||||
&self,
|
||||
cell_data: CellData<CD>,
|
||||
decoded_field_type: &FieldType,
|
||||
field_rev: &FieldRevision,
|
||||
) -> FlowyResult<DC>;
|
||||
) -> FlowyResult<CellBytes>;
|
||||
}
|
||||
|
||||
// CD: Short for CellData. This type is the type return by apply_changeset function.
|
||||
|
@ -1,5 +1,4 @@
|
||||
mod field_builder;
|
||||
pub mod select_option;
|
||||
pub(crate) mod type_options;
|
||||
|
||||
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;
|
||||
mod date_type_option;
|
||||
mod multi_select_type_option;
|
||||
mod number_type_option;
|
||||
mod single_select_type_option;
|
||||
mod text_type_option;
|
||||
mod url_type_option;
|
||||
pub mod checkbox_type_option;
|
||||
pub mod date_type_option;
|
||||
pub mod number_type_option;
|
||||
pub mod selection_type_option;
|
||||
pub mod text_type_option;
|
||||
pub mod url_type_option;
|
||||
mod util;
|
||||
|
||||
pub use checkbox_type_option::*;
|
||||
pub use date_type_option::*;
|
||||
pub use multi_select_type_option::*;
|
||||
pub use multi_select_type_option::*;
|
||||
pub use number_type_option::*;
|
||||
pub use single_select_type_option::*;
|
||||
pub use selection_type_option::*;
|
||||
pub use text_type_option::*;
|
||||
pub use url_type_option::*;
|
||||
|
@ -1,6 +1,9 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
mod format;
|
||||
mod number_type_option;
|
||||
mod number_option;
|
||||
mod number_option_entities;
|
||||
mod tests;
|
||||
|
||||
pub use format::*;
|
||||
pub use number_type_option::*;
|
||||
pub use number_option::*;
|
||||
pub use number_option_entities::*;
|
||||
|
@ -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::impl_type_option;
|
||||
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
|
||||
use crate::services::field::select_option::{
|
||||
make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds,
|
||||
SelectOptionOperation, SELECTION_IDS_SEPARATOR,
|
||||
};
|
||||
use crate::services::field::type_options::util::get_cell_data;
|
||||
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
|
||||
use crate::services::field::{
|
||||
make_selected_select_options, BoxTypeOptionBuilder, SelectOption, SelectOptionCellChangeset, SelectOptionCellData,
|
||||
SelectOptionIds, SelectOptionOperation, TypeOptionBuilder, SELECTION_IDS_SEPARATOR,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
|
||||
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, TypeOptionDataDeserializer, TypeOptionDataEntry};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Multiple select
|
||||
@ -56,8 +52,7 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for MultiSele
|
||||
return Ok(CellBytes::default());
|
||||
}
|
||||
|
||||
let cell_data = self.display_data(cell_data, decoded_field_type, field_rev)?;
|
||||
CellBytes::from(cell_data)
|
||||
self.display_data(cell_data, decoded_field_type, field_rev)
|
||||
}
|
||||
|
||||
fn apply_changeset(
|
||||
@ -121,7 +116,7 @@ impl TypeOptionBuilder for MultiSelectTypeOptionBuilder {
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::CellDataOperation;
|
||||
use crate::services::field::select_option::*;
|
||||
use crate::services::field::type_options::selection_type_option::*;
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::{MultiSelectTypeOption, MultiSelectTypeOptionBuilder};
|
||||
use flowy_grid_data_model::revision::FieldRevision;
|
@ -1,5 +1,5 @@
|
||||
use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType};
|
||||
use crate::services::cell::{AnyCellData, CellData, CellDisplayable, FromCellChangeset, FromCellString};
|
||||
use crate::services::cell::{AnyCellData, CellBytes, CellData, CellDisplayable, FromCellChangeset, FromCellString};
|
||||
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
|
||||
@ -106,7 +106,7 @@ pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
|
||||
}
|
||||
|
||||
impl<T> CellDisplayable<SelectOptionIds, SelectOptionCellData> for T
|
||||
impl<T> CellDisplayable<SelectOptionIds> for T
|
||||
where
|
||||
T: SelectOptionOperation,
|
||||
{
|
||||
@ -115,8 +115,8 @@ where
|
||||
cell_data: CellData<SelectOptionIds>,
|
||||
_decoded_field_type: &FieldType,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<SelectOptionCellData> {
|
||||
Ok(self.selected_select_option(cell_data))
|
||||
) -> FlowyResult<CellBytes> {
|
||||
CellBytes::from(self.selected_select_option(cell_data))
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::entities::FieldType;
|
||||
use crate::impl_type_option;
|
||||
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
|
||||
use crate::services::field::select_option::{
|
||||
use crate::services::field::{
|
||||
make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds,
|
||||
SelectOptionOperation,
|
||||
};
|
||||
@ -54,8 +54,7 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for SingleSel
|
||||
return Ok(CellBytes::default());
|
||||
}
|
||||
|
||||
let cell_data = self.display_data(cell_data, decoded_field_type, field_rev)?;
|
||||
CellBytes::from(cell_data)
|
||||
self.display_data(cell_data, decoded_field_type, field_rev)
|
||||
}
|
||||
|
||||
fn apply_changeset(
|
||||
@ -103,7 +102,7 @@ impl TypeOptionBuilder for SingleSelectTypeOptionBuilder {
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::CellDataOperation;
|
||||
use crate::services::field::select_option::*;
|
||||
|
||||
use crate::services::field::type_options::*;
|
||||
use crate::services::field::FieldBuilder;
|
||||
use flowy_grid_data_model::revision::FieldRevision;
|
@ -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 CellDisplayable<String, String> for RichTextTypeOption {
|
||||
impl CellDisplayable<String> for RichTextTypeOption {
|
||||
fn display_data(
|
||||
&self,
|
||||
cell_data: CellData<String>,
|
||||
_decoded_field_type: &FieldType,
|
||||
_field_rev: &FieldRevision,
|
||||
) -> FlowyResult<String> {
|
||||
) -> FlowyResult<CellBytes> {
|
||||
let cell_str: String = cell_data.try_into_inner()?;
|
||||
Ok(cell_str)
|
||||
Ok(CellBytes::new(cell_str))
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,8 +58,7 @@ impl CellDataOperation<String, String> for RichTextTypeOption {
|
||||
{
|
||||
try_decode_cell_data(cell_data, field_rev, decoded_field_type, decoded_field_type)
|
||||
} else {
|
||||
let content = self.display_data(cell_data, decoded_field_type, field_rev)?;
|
||||
Ok(CellBytes::new(content))
|
||||
self.display_data(cell_data, decoded_field_type, field_rev)
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,7 +95,7 @@ impl std::convert::TryFrom<AnyCellData> for TextCellData {
|
||||
mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::CellDataOperation;
|
||||
use crate::services::field::select_option::*;
|
||||
|
||||
use crate::services::field::FieldBuilder;
|
||||
use crate::services::field::*;
|
||||
|
@ -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;
|
||||
|
||||
pub use crate::services::field::select_option::*;
|
||||
pub use cell_data_util::*;
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
|
||||
use crate::services::cell::{AnyCellData, CellFilterOperation};
|
||||
use crate::services::field::select_option::{SelectOptionOperation, SelectedSelectOptions};
|
||||
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
use crate::services::field::{SelectOptionOperation, SelectedSelectOptions};
|
||||
use flowy_error::FlowyResult;
|
||||
|
||||
impl GridSelectOptionFilter {
|
||||
@ -64,7 +64,7 @@ impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
|
||||
mod tests {
|
||||
#![allow(clippy::all)]
|
||||
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
|
||||
use crate::services::field::select_option::{SelectOption, SelectedSelectOptions};
|
||||
use crate::services::field::selection_type_option::{SelectOption, SelectedSelectOptions};
|
||||
|
||||
#[test]
|
||||
fn select_option_filter_is_test() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::services::cell::apply_cell_data_changeset;
|
||||
use crate::services::field::select_option::SelectOptionCellChangeset;
|
||||
use crate::services::field::SelectOptionCellChangeset;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_grid_data_model::revision::{gen_row_id, CellRevision, FieldRevision, RowRevision, DEFAULT_ROW_HEIGHT};
|
||||
use indexmap::IndexMap;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use flowy_grid::entities::FieldType;
|
||||
use flowy_grid::services::field::select_option::{SelectOption, SELECTION_IDS_SEPARATOR};
|
||||
use flowy_grid::services::field::selection_type_option::{SelectOption, SELECTION_IDS_SEPARATOR};
|
||||
use flowy_grid::services::field::{DateCellChangeset, MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
use flowy_grid::services::row::RowRevisionBuilder;
|
||||
use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
|
||||
|
@ -2,7 +2,7 @@ use crate::grid::cell_test::script::CellScript::*;
|
||||
use crate::grid::cell_test::script::GridCellTest;
|
||||
use crate::grid::field_test::util::make_date_cell_string;
|
||||
use flowy_grid::entities::{CellChangeset, FieldType};
|
||||
use flowy_grid::services::field::select_option::SelectOptionCellChangeset;
|
||||
use flowy_grid::services::field::selection_type_option::SelectOptionCellChangeset;
|
||||
use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::grid::field_test::script::FieldScript::*;
|
||||
use crate::grid::field_test::script::GridFieldTest;
|
||||
use crate::grid::field_test::util::*;
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_grid::services::field::selection_type_option::SelectOption;
|
||||
use flowy_grid::services::field::SingleSelectTypeOption;
|
||||
use flowy_grid_data_model::revision::TypeOptionDataEntry;
|
||||
use flowy_sync::entities::grid::FieldChangesetParams;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use flowy_grid::entities::*;
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_grid::services::field::selection_type_option::SelectOption;
|
||||
use flowy_grid::services::field::*;
|
||||
use flowy_grid_data_model::revision::*;
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
#![allow(unused_imports)]
|
||||
use bytes::Bytes;
|
||||
use flowy_grid::entities::*;
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_grid::services::field::SelectOption;
|
||||
use flowy_grid::services::field::*;
|
||||
use flowy_grid::services::grid_editor::{GridPadBuilder, GridRevisionEditor};
|
||||
use flowy_grid::services::row::{CreateRowRevisionPayload, RowRevisionBuilder};
|
||||
|
Loading…
Reference in New Issue
Block a user