Merge branch 'main' into refactor/add_pb_suffix

This commit is contained in:
appflowy 2022-07-17 13:19:29 +08:00
commit 3a0b8bbd74
60 changed files with 2235 additions and 1698 deletions

View File

@ -8,10 +8,10 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/cell_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:app_flowy/workspace/application/grid/cell/cell_listener.dart';

View File

@ -5,6 +5,7 @@ import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:table_calendar/table_calendar.dart';

View File

@ -1,4 +1,4 @@
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

View File

@ -1,4 +1,4 @@
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';

View File

@ -1,4 +1,4 @@
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';

View File

@ -1,5 +1,6 @@
import 'package:app_flowy/workspace/application/grid/field/type_option/type_option_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';

View File

@ -9,7 +9,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option_entities.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'builder.dart';

View File

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

View File

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

View File

@ -1,10 +1,13 @@
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};
use flowy_grid_data_model::revision::FieldRevision;
use flowy_sync::entities::grid::{FieldChangesetParams, GridSettingChangesetParams};
@ -229,10 +232,12 @@ pub(crate) async fn get_row_handler(
) -> DataResult<OptionalRow, FlowyError> {
let params: GridRowId = data.into_inner().try_into()?;
let editor = manager.get_grid_editor(&params.grid_id)?;
let row = OptionalRow {
row: editor.get_row(&params.row_id).await?,
};
data_result(row)
let row = editor
.get_row_rev(&params.row_id)
.await?
.and_then(make_row_from_row_rev);
data_result(OptionalRow { row })
}
#[tracing::instrument(level = "debug", skip(data, manager), err)]
@ -373,7 +378,7 @@ pub(crate) async fn get_select_option_handler(
},
Some(cell_rev) => cell_rev.try_into()?,
};
let option_context = type_option.selected_select_option(any_cell_data);
let option_context = type_option.selected_select_option(any_cell_data.into());
data_result(option_context)
}
}

View File

@ -1,9 +1,11 @@
use crate::entities::FieldType;
use crate::services::cell::{CellData, FromCellString};
use bytes::Bytes;
use flowy_error::{internal_error, FlowyError, FlowyResult};
use flowy_grid_data_model::revision::CellRevision;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
/// AnyCellData is a generic CellData, you can parse the cell_data according to the field_type.
/// When the type of field is changed, it's different from the field_type of AnyCellData.
/// So it will return an empty data. You could check the CellDataOperation trait for more information.
@ -46,6 +48,15 @@ impl std::convert::TryFrom<CellRevision> for AnyCellData {
}
}
impl<T> std::convert::From<AnyCellData> for CellData<T>
where
T: FromCellString,
{
fn from(any_call_data: AnyCellData) -> Self {
CellData::from(any_call_data.data)
}
}
impl AnyCellData {
pub fn new(content: String, field_type: FieldType) -> Self {
AnyCellData {
@ -100,36 +111,45 @@ impl AnyCellData {
/// * Use String to parse the data when the FieldType is RichText, Number, or Checkbox.
/// * Check out the implementation of CellDataOperation trait for more information.
#[derive(Default)]
pub struct DecodedCellData {
pub data: Vec<u8>,
pub struct CellBytes(pub Bytes);
pub trait CellBytesParser {
type Object;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object>;
}
impl DecodedCellData {
impl CellBytes {
pub fn new<T: AsRef<[u8]>>(data: T) -> Self {
Self {
data: data.as_ref().to_vec(),
}
let bytes = Bytes::from(data.as_ref().to_vec());
Self(bytes)
}
pub fn try_from_bytes<T: TryInto<Bytes>>(bytes: T) -> FlowyResult<Self>
pub fn from<T: TryInto<Bytes>>(bytes: T) -> FlowyResult<Self>
where
<T as TryInto<Bytes>>::Error: std::fmt::Debug,
{
let bytes = bytes.try_into().map_err(internal_error)?;
Ok(Self { data: bytes.to_vec() })
Ok(Self(bytes))
}
pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult<T>
pub fn with_parser<P>(&self, parser: P) -> FlowyResult<P::Object>
where
<T as TryFrom<&'a [u8]>>::Error: std::fmt::Debug,
P: CellBytesParser,
{
T::try_from(self.data.as_ref()).map_err(internal_error)
parser.parse(&self.0)
}
// pub fn parse<'a, T: TryFrom<&'a [u8]>>(&'a self) -> FlowyResult<T>
// where
// <T as TryFrom<&'a [u8]>>::Error: std::fmt::Debug,
// {
// T::try_from(self.0.as_ref()).map_err(internal_error)
// }
}
impl ToString for DecodedCellData {
impl ToString for CellBytes {
fn to_string(&self) -> String {
match String::from_utf8(self.data.clone()) {
match String::from_utf8(self.0.to_vec()) {
Ok(s) => s,
Err(e) => {
tracing::error!("DecodedCellData to string failed: {:?}", e);
@ -138,3 +158,11 @@ impl ToString for DecodedCellData {
}
}
}
impl std::ops::Deref for CellBytes {
type Target = Bytes;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -1,33 +1,51 @@
use crate::entities::FieldType;
use crate::services::cell::{AnyCellData, CellBytes};
use crate::services::field::*;
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use flowy_grid_data_model::revision::{CellRevision, FieldRevision, FieldTypeRevision};
use crate::entities::FieldType;
use crate::services::cell::{AnyCellData, DecodedCellData};
use crate::services::field::*;
/// This trait is used when doing filter/search on the grid.
pub trait CellFilterOperation<T> {
/// Return true if any_cell_data match the filter condition.
fn apply_filter(&self, any_cell_data: AnyCellData, filter: &T) -> FlowyResult<bool>;
}
pub trait CellDataOperation<D, C> {
/// The cell_data is able to parse into the specific data that was impl the FromCellData trait.
/// Return object that describes the cell.
pub trait CellDisplayable<CD> {
fn display_data(
&self,
cell_data: CellData<CD>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes>;
}
// CD: Short for CellData. This type is the type return by apply_changeset function.
// CS: Short for Changeset. Parse the string into specific Changeset type.
pub trait CellDataOperation<CD, CS> {
/// The cell_data is able to parse into the specific data if CD impl the FromCellData trait.
/// For example:
/// URLCellData, DateCellData. etc.
fn decode_cell_data(
&self,
cell_data: CellData<D>,
cell_data: CellData<CD>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData>;
) -> FlowyResult<CellBytes>;
/// The changeset is able to parse into the specific data that was impl the FromCellChangeset trait.
/// The changeset is able to parse into the specific data if CS impl the FromCellChangeset trait.
/// For example:
/// SelectOptionCellChangeset,DateCellChangeset. etc.
fn apply_changeset(&self, changeset: CellDataChangeset<C>, cell_rev: Option<CellRevision>) -> FlowyResult<String>;
fn apply_changeset(&self, changeset: CellDataChangeset<CS>, cell_rev: Option<CellRevision>) -> FlowyResult<String>;
}
/// The changeset will be deserialized into specific data base on the FieldType.
/// For example, it's String on FieldType::RichText, and SelectOptionChangeset on FieldType::SingleSelect
/// changeset: It will be deserialized into specific data base on the FieldType.
/// For example,
/// FieldType::RichText => String
/// FieldType::SingleSelect => SelectOptionChangeset
///
/// cell_rev: It will be None if the cell does not contain any data.
pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
changeset: C,
cell_rev: Option<CellRevision>,
@ -49,23 +67,20 @@ pub fn apply_cell_data_changeset<C: ToString, T: AsRef<FieldRevision>>(
Ok(AnyCellData::new(s, field_type).json())
}
pub fn decode_any_cell_data<T: TryInto<AnyCellData>>(data: T, field_rev: &FieldRevision) -> DecodedCellData {
pub fn decode_any_cell_data<T: TryInto<AnyCellData>>(data: T, field_rev: &FieldRevision) -> CellBytes {
if let Ok(any_cell_data) = data.try_into() {
let AnyCellData {
data: cell_data,
field_type,
} = any_cell_data;
let AnyCellData { data, field_type } = any_cell_data;
let to_field_type = field_rev.field_type_rev.into();
match try_decode_cell_data(CellData(Some(cell_data)), field_rev, &field_type, &to_field_type) {
Ok(cell_data) => cell_data,
match try_decode_cell_data(data.into(), field_rev, &field_type, &to_field_type) {
Ok(cell_bytes) => cell_bytes,
Err(e) => {
tracing::error!("Decode cell data failed, {:?}", e);
DecodedCellData::default()
CellBytes::default()
}
}
} else {
tracing::error!("Decode type option data failed");
DecodedCellData::default()
CellBytes::default()
}
}
@ -74,7 +89,7 @@ pub fn try_decode_cell_data(
field_rev: &FieldRevision,
s_field_type: &FieldType,
t_field_type: &FieldType,
) -> FlowyResult<DecodedCellData> {
) -> FlowyResult<CellBytes> {
let cell_data = cell_data.try_into_inner()?;
let get_cell_data = || {
let field_type: FieldTypeRevision = t_field_type.into();
@ -108,20 +123,22 @@ pub fn try_decode_cell_data(
Some(Ok(data)) => Ok(data),
Some(Err(err)) => {
tracing::error!("{:?}", err);
Ok(DecodedCellData::default())
Ok(CellBytes::default())
}
None => Ok(DecodedCellData::default()),
None => Ok(CellBytes::default()),
}
}
/// If the cell data is not String type, it should impl this trait.
/// Deserialize the String into cell specific data type.
pub trait FromCellString {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized;
}
/// CellData is a helper struct. String will be parser into Option<T> only if the T impl the FromCellString trait.
pub struct CellData<T>(pub Option<T>);
impl<T> CellData<T> {
pub fn try_into_inner(self) -> FlowyResult<T> {
match self.0 {
@ -146,9 +163,9 @@ where
}
}
impl std::convert::From<String> for CellData<String> {
fn from(s: String) -> Self {
CellData(Some(s))
impl<T> std::convert::From<T> for CellData<T> {
fn from(val: T) -> Self {
CellData(Some(val))
}
}
@ -158,7 +175,8 @@ impl std::convert::From<CellData<String>> for String {
}
}
// CellChangeset
/// If the changeset applying to the cell is not String type, it should impl this trait.
/// Deserialize the string into cell specific changeset.
pub trait FromCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where

View File

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

View File

@ -1,133 +0,0 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData};
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 CellDataOperation<String, String> for CheckboxTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<String>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData> {
if !decoded_field_type.is_checkbox() {
return Ok(DecodedCellData::default());
}
let s: String = cell_data.try_into_inner()?;
if s == YES || s == NO {
return Ok(DecodedCellData::new(s));
}
Ok(DecodedCellData::default())
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let s = match string_to_bool(&changeset) {
true => YES,
false => NO,
};
Ok(s.to_string())
}
}
fn string_to_bool(bool_str: &str) -> bool {
let lower_case_str: &str = &bool_str.to_lowercase();
match lower_case_str {
"1" => true,
"true" => true,
"yes" => true,
"0" => false,
"false" => false,
"no" => false,
_ => false,
}
}
pub struct CheckboxCellData(pub String);
impl CheckboxCellData {
pub fn is_check(&self) -> bool {
string_to_bool(&self.0)
}
}
impl std::convert::TryFrom<AnyCellData> for CheckboxCellData {
type Error = FlowyError;
fn try_from(_value: AnyCellData) -> Result<Self, Self::Error> {
todo!()
}
}
#[cfg(test)]
mod tests {
use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data};
use crate::services::field::type_options::checkbox_type_option::{NO, YES};
use crate::services::field::FieldBuilder;
use crate::entities::FieldType;
#[test]
fn checkout_box_description_test() {
let field_rev = FieldBuilder::from_field_type(&FieldType::Checkbox).build();
let data = apply_cell_data_changeset("true", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev).to_string(), YES);
let data = apply_cell_data_changeset("1", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
let data = apply_cell_data_changeset("yes", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), YES);
let data = apply_cell_data_changeset("false", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
let data = apply_cell_data_changeset("no", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
let data = apply_cell_data_changeset("12", None, &field_rev).unwrap();
assert_eq!(decode_any_cell_data(data, &field_rev,).to_string(), NO);
}
}

View File

@ -0,0 +1,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(), "");
}
}

View File

@ -0,0 +1,76 @@
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};
use std::str::FromStr;
#[derive(Default)]
pub struct CheckboxTypeOptionBuilder(CheckboxTypeOption);
impl_into_box_type_option_builder!(CheckboxTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(CheckboxTypeOptionBuilder, CheckboxTypeOption);
impl CheckboxTypeOptionBuilder {
pub fn set_selected(mut self, is_selected: bool) -> Self {
self.0.is_selected = is_selected;
self
}
}
impl TypeOptionBuilder for CheckboxTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::Checkbox
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, ProtoBuf)]
pub struct CheckboxTypeOption {
#[pb(index = 1)]
pub is_selected: bool,
}
impl_type_option!(CheckboxTypeOption, FieldType::Checkbox);
impl CellDisplayable<CheckboxCellData> for CheckboxTypeOption {
fn display_data(
&self,
cell_data: CellData<CheckboxCellData>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
let cell_data = cell_data.try_into_inner()?;
Ok(CellBytes::new(cell_data))
}
}
impl CellDataOperation<CheckboxCellData, String> for CheckboxTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<CheckboxCellData>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if !decoded_field_type.is_checkbox() {
return Ok(CellBytes::default());
}
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<String>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let cell_data = CheckboxCellData::from_str(&changeset)?;
Ok(cell_data.to_string())
}
}

View File

@ -0,0 +1,69 @@
use crate::services::cell::{CellBytesParser, FromCellString};
use bytes::Bytes;
use flowy_error::{FlowyError, FlowyResult};
use std::str::FromStr;
pub const YES: &str = "Yes";
pub const NO: &str = "No";
pub struct CheckboxCellData(String);
impl CheckboxCellData {
pub fn is_check(&self) -> bool {
self.0 == YES
}
}
impl AsRef<[u8]> for CheckboxCellData {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl FromStr for CheckboxCellData {
type Err = FlowyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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) => Ok(Self(YES.to_string())),
Some(false) => Ok(Self(NO.to_string())),
None => Ok(Self("".to_string())),
}
}
}
impl FromCellString for CheckboxCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
Self::from_str(s)
}
}
impl ToString for CheckboxCellData {
fn to_string(&self) -> String {
self.0.clone()
}
}
pub struct CheckboxCellDataParser();
impl CellBytesParser for CheckboxCellDataParser {
type Object = CheckboxCellData;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => CheckboxCellData::from_str(&s),
Err(_) => Ok(CheckboxCellData("".to_string())),
}
}
}

View File

@ -0,0 +1,7 @@
#![allow(clippy::module_inception)]
mod checkbox_tests;
mod checkbox_type_option;
mod checkbox_type_option_entities;
pub use checkbox_type_option::*;
pub use checkbox_type_option_entities::*;

View File

@ -1,661 +0,0 @@
use crate::entities::{CellChangeset, FieldType};
use crate::entities::{CellIdentifier, CellIdentifierPayload};
use crate::impl_type_option;
use crate::services::cell::{
AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData, 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 CellDataOperation<DateTimestamp, DateCellChangeset> for DateTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<DateTimestamp>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData> {
// 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(DecodedCellData::default());
}
let timestamp = cell_data.try_into_inner()?;
let date = self.today_desc_from_timestamp(timestamp.0);
DecodedCellData::try_from_bytes(date)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<DateCellChangeset>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let cell_data = match changeset.date_timestamp() {
None => 0,
Some(date_timestamp) => match (self.include_time, changeset.time) {
(true, Some(time)) => {
let time = Some(time.trim().to_uppercase());
let utc = self.utc_date_time_from_timestamp(date_timestamp);
self.timestamp_from_utc_with_time(&utc, &time)?
}
_ => date_timestamp,
},
};
Ok(cell_data.to_string())
}
}
pub struct DateTimestamp(i64);
impl AsRef<i64> for DateTimestamp {
fn as_ref(&self) -> &i64 {
&self.0
}
}
impl std::convert::From<DateTimestamp> for i64 {
fn from(timestamp: DateTimestamp) -> Self {
timestamp.0
}
}
impl FromCellString for DateTimestamp {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
let num = s.parse::<i64>().unwrap_or(0);
Ok(DateTimestamp(num))
}
}
impl std::convert::From<AnyCellData> for DateTimestamp {
fn from(data: AnyCellData) -> Self {
let num = data.data.parse::<i64>().unwrap_or(0);
DateTimestamp(num)
}
}
#[derive(Default)]
pub struct DateTypeOptionBuilder(DateTypeOption);
impl_into_box_type_option_builder!(DateTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption);
impl DateTypeOptionBuilder {
pub fn date_format(mut self, date_format: DateFormat) -> Self {
self.0.date_format = date_format;
self
}
pub fn time_format(mut self, time_format: TimeFormat) -> Self {
self.0.time_format = time_format;
self
}
}
impl TypeOptionBuilder for DateTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::DateTime
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}
#[derive(Clone, Debug, Copy, EnumIter, Serialize, Deserialize, ProtoBuf_Enum)]
pub enum DateFormat {
Local = 0,
US = 1,
ISO = 2,
Friendly = 3,
}
impl std::default::Default for DateFormat {
fn default() -> Self {
DateFormat::Friendly
}
}
impl std::convert::From<i32> for DateFormat {
fn from(value: i32) -> Self {
match value {
0 => DateFormat::Local,
1 => DateFormat::US,
2 => DateFormat::ISO,
3 => DateFormat::Friendly,
_ => {
tracing::error!("Unsupported date format, fallback to friendly");
DateFormat::Friendly
}
}
}
}
impl DateFormat {
pub fn value(&self) -> i32 {
*self as i32
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
DateFormat::Local => "%Y/%m/%d",
DateFormat::US => "%Y/%m/%d",
DateFormat::ISO => "%Y-%m-%d",
DateFormat::Friendly => "%b %d,%Y",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, EnumIter, Debug, Hash, Serialize, Deserialize, ProtoBuf_Enum)]
pub enum TimeFormat {
TwelveHour = 0,
TwentyFourHour = 1,
}
impl std::convert::From<i32> for TimeFormat {
fn from(value: i32) -> Self {
match value {
0 => TimeFormat::TwelveHour,
1 => TimeFormat::TwentyFourHour,
_ => {
tracing::error!("Unsupported time format, fallback to TwentyFourHour");
TimeFormat::TwentyFourHour
}
}
}
}
impl TimeFormat {
pub fn value(&self) -> i32 {
*self as i32
}
// https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html
pub fn format_str(&self) -> &'static str {
match self {
TimeFormat::TwelveHour => "%I:%M %p",
TimeFormat::TwentyFourHour => "%R",
}
}
}
impl std::default::Default for TimeFormat {
fn default() -> Self {
TimeFormat::TwentyFourHour
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateCellData {
#[pb(index = 1)]
pub date: String,
#[pb(index = 2)]
pub time: String,
#[pb(index = 3)]
pub timestamp: i64,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct DateChangesetPayload {
#[pb(index = 1)]
pub cell_identifier: CellIdentifierPayload,
#[pb(index = 2, one_of)]
pub date: Option<String>,
#[pb(index = 3, one_of)]
pub time: Option<String>,
}
pub struct DateChangesetParams {
pub cell_identifier: CellIdentifier,
pub date: Option<String>,
pub time: Option<String>,
}
impl TryInto<DateChangesetParams> for DateChangesetPayload {
type Error = ErrorCode;
fn try_into(self) -> Result<DateChangesetParams, Self::Error> {
let cell_identifier: CellIdentifier = self.cell_identifier.try_into()?;
Ok(DateChangesetParams {
cell_identifier,
date: self.date,
time: self.time,
})
}
}
impl std::convert::From<DateChangesetParams> for CellChangeset {
fn from(params: DateChangesetParams) -> Self {
let changeset = DateCellChangeset {
date: params.date,
time: params.time,
};
let s = serde_json::to_string(&changeset).unwrap();
CellChangeset {
grid_id: params.cell_identifier.grid_id,
row_id: params.cell_identifier.row_id,
field_id: params.cell_identifier.field_id,
content: Some(s),
}
}
}
#[derive(Clone, Serialize, Deserialize)]
pub struct DateCellChangeset {
pub date: Option<String>,
pub time: Option<String>,
}
impl DateCellChangeset {
pub fn date_timestamp(&self) -> Option<i64> {
if let Some(date) = &self.date {
match date.parse::<i64>() {
Ok(date_timestamp) => Some(date_timestamp),
Err(_) => None,
}
} else {
None
}
}
}
impl FromCellChangeset for DateCellChangeset {
fn from_changeset(changeset: String) -> FlowyResult<Self>
where
Self: Sized,
{
serde_json::from_str::<DateCellChangeset>(&changeset).map_err(internal_error)
}
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellDataChangeset, CellDataOperation};
use crate::services::field::FieldBuilder;
use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOption, TimeFormat};
use flowy_grid_data_model::revision::FieldRevision;
use strum::IntoEnumIterator;
#[test]
fn date_type_option_invalid_input_test() {
let type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some("1e".to_string()),
time: Some("23:00".to_owned()),
},
&field_type,
&field_rev,
"",
);
}
#[test]
fn date_type_option_date_format_test() {
let mut type_option = DateTypeOption::default();
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
for date_format in DateFormat::iter() {
type_option.date_format = date_format;
match date_format {
DateFormat::Friendly => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "Mar 14,2022");
}
DateFormat::US => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
}
DateFormat::ISO => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022-03-14");
}
DateFormat::Local => {
assert_decode_timestamp(1647251762, &type_option, &field_rev, "2022/03/14");
}
}
}
}
#[test]
fn date_type_option_time_format_test() {
let mut type_option = DateTypeOption::default();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for time_format in TimeFormat::iter() {
type_option.time_format = time_format;
type_option.include_time = true;
match time_format {
TimeFormat::TwentyFourHour => {
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("23:00".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 23:00",
);
}
TimeFormat::TwelveHour => {
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
//
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(1653609600.to_string()),
time: Some("11:23 pm".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 11:23 PM",
);
}
}
}
}
#[test]
fn date_type_option_apply_changeset_test() {
let mut type_option = DateTypeOption::new();
let field_type = FieldType::DateTime;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
type_option.include_time = true;
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: None,
},
&field_type,
&field_rev,
"May 27,2022",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: Some("1:00".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 01:00",
);
type_option.time_format = TimeFormat::TwelveHour;
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00 am".to_owned()),
},
&field_type,
&field_rev,
"May 27,2022 01:00 AM",
);
}
#[test]
#[should_panic]
fn date_type_option_apply_changeset_error_test() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp.clone()),
time: Some("1:".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
}
#[test]
#[should_panic]
fn date_type_option_twelve_hours_to_twenty_four_hours() {
let mut type_option = DateTypeOption::new();
type_option.include_time = true;
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
let date_timestamp = "1653609600".to_owned();
assert_changeset_result(
&type_option,
DateCellChangeset {
date: Some(date_timestamp),
time: Some("1:00 am".to_owned()),
},
&FieldType::DateTime,
&field_rev,
"May 27,2022 01:00",
);
}
fn assert_changeset_result(
type_option: &DateTypeOption,
changeset: DateCellChangeset,
_field_type: &FieldType,
field_rev: &FieldRevision,
expected: &str,
) {
let changeset = CellDataChangeset(Some(changeset));
let encoded_data = type_option.apply_changeset(changeset, None).unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(encoded_data, type_option, field_rev)
);
}
fn assert_decode_timestamp(
timestamp: i64,
type_option: &DateTypeOption,
field_rev: &FieldRevision,
expected: &str,
) {
let s = serde_json::to_string(&DateCellChangeset {
date: Some(timestamp.to_string()),
time: None,
})
.unwrap();
let encoded_data = type_option.apply_changeset(s.into(), None).unwrap();
assert_eq!(
expected.to_owned(),
decode_cell_data(encoded_data, type_option, field_rev)
);
}
fn decode_cell_data(encoded_data: String, type_option: &DateTypeOption, field_rev: &FieldRevision) -> String {
let decoded_data = type_option
.decode_cell_data(encoded_data.into(), &FieldType::DateTime, field_rev)
.unwrap()
.parse::<DateCellData>()
.unwrap();
if type_option.include_time {
format!("{}{}", decoded_data.date, decoded_data.time)
} else {
decoded_data.date
}
}
}

View File

@ -0,0 +1,272 @@
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellDataChangeset, CellDataOperation};
use crate::services::field::*;
// 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()
.with_parser(DateCellDataParser())
.unwrap();
if type_option.include_time {
format!("{}{}", decoded_data.date, decoded_data.time)
} else {
decoded_data.date
}
}
}

View File

@ -0,0 +1,195 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
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()?;
let date_cell_data = self.today_desc_from_timestamp(timestamp);
CellBytes::from(date_cell_data)
}
}
impl CellDataOperation<DateTimestamp, DateCellChangeset> for DateTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<DateTimestamp>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
// Return default data if the type_option_cell_data is not FieldType::DateTime.
// It happens when switching from one field to another.
// For example:
// FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen.
if !decoded_field_type.is_date() {
return Ok(CellBytes::default());
}
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
&self,
changeset: CellDataChangeset<DateCellChangeset>,
_cell_rev: Option<CellRevision>,
) -> Result<String, FlowyError> {
let changeset = changeset.try_into_inner()?;
let cell_data = match changeset.date_timestamp() {
None => 0,
Some(date_timestamp) => match (self.include_time, changeset.time) {
(true, Some(time)) => {
let time = Some(time.trim().to_uppercase());
let utc = self.utc_date_time_from_timestamp(date_timestamp);
self.timestamp_from_utc_with_time(&utc, &time)?
}
_ => date_timestamp,
},
};
Ok(cell_data.to_string())
}
}
#[derive(Default)]
pub struct DateTypeOptionBuilder(DateTypeOption);
impl_into_box_type_option_builder!(DateTypeOptionBuilder);
impl_builder_from_json_str_and_from_bytes!(DateTypeOptionBuilder, DateTypeOption);
impl DateTypeOptionBuilder {
pub fn date_format(mut self, date_format: DateFormat) -> Self {
self.0.date_format = date_format;
self
}
pub fn time_format(mut self, time_format: TimeFormat) -> Self {
self.0.time_format = time_format;
self
}
}
impl TypeOptionBuilder for DateTypeOptionBuilder {
fn field_type(&self) -> FieldType {
FieldType::DateTime
}
fn entry(&self) -> &dyn TypeOptionDataEntry {
&self.0
}
}

View File

@ -0,0 +1,210 @@
use crate::entities::CellChangeset;
use crate::entities::{CellIdentifier, CellIdentifierPayload};
use crate::services::cell::{CellBytesParser, FromCellChangeset, FromCellString};
use bytes::Bytes;
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))
}
}
#[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
}
}
pub struct DateCellDataParser();
impl CellBytesParser for DateCellDataParser {
type Object = DateCellData;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
DateCellData::try_from(bytes.as_ref()).map_err(internal_error)
}
}

View File

@ -0,0 +1,7 @@
#![allow(clippy::module_inception)]
mod date_tests;
mod date_type_option;
mod date_type_option_entities;
pub use date_type_option::*;
pub use date_type_option_entities::*;

View File

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

View File

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

View File

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

View File

@ -1,17 +1,15 @@
use crate::impl_type_option;
use crate::entities::FieldType;
use crate::services::cell::{CellData, CellDataChangeset, CellDataOperation, DecodedCellData};
use crate::services::field::number_currency::Currency;
use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation};
use crate::services::field::type_options::number_type_option::format::*;
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
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;
@ -110,15 +108,15 @@ impl CellDataOperation<String, String> for NumberTypeOption {
cell_data: CellData<String>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData> {
) -> FlowyResult<CellBytes> {
if decoded_field_type.is_date() {
return Ok(DecodedCellData::default());
return Ok(CellBytes::default());
}
let cell_data: String = cell_data.try_into_inner()?;
match self.format_cell_data(&cell_data) {
Ok(num) => Ok(DecodedCellData::new(num.to_string())),
Err(_) => Ok(DecodedCellData::default()),
Ok(num) => Ok(CellBytes::new(num.to_string())),
Err(_) => Ok(CellBytes::default()),
}
}
@ -147,230 +145,3 @@ impl std::default::Default for NumberTypeOption {
}
}
}
#[derive(Default)]
pub struct NumberCellData {
decimal: Option<Decimal>,
money: Option<String>,
}
impl NumberCellData {
pub fn new() -> Self {
Self {
decimal: Default::default(),
money: None,
}
}
pub fn from_format_str(s: &str, sign_positive: bool, format: &NumberFormat) -> FlowyResult<Self> {
let mut num_str = strip_currency_symbol(s);
let currency = format.currency();
if num_str.is_empty() {
return Ok(Self::default());
}
match Decimal::from_str(&num_str) {
Ok(mut decimal) => {
decimal.set_sign_positive(sign_positive);
let money = Money::from_decimal(decimal, currency);
Ok(Self::from_money(money))
}
Err(_) => match Money::from_str(&num_str, currency) {
Ok(money) => Ok(NumberCellData::from_money(money)),
Err(_) => {
num_str.retain(|c| !STRIP_SYMBOL.contains(&c.to_string()));
if num_str.chars().all(char::is_numeric) {
Self::from_format_str(&num_str, sign_positive, format)
} else {
Err(FlowyError::invalid_data().context("Should only contain numbers"))
}
}
},
}
}
pub fn from_decimal(decimal: Decimal) -> Self {
Self {
decimal: Some(decimal),
money: None,
}
}
pub fn from_money(money: Money<Currency>) -> Self {
Self {
decimal: Some(*money.amount()),
money: Some(money.to_string()),
}
}
pub fn decimal(&self) -> &Option<Decimal> {
&self.decimal
}
pub fn is_empty(&self) -> bool {
self.decimal.is_none()
}
}
impl FromStr for NumberCellData {
type Err = rust_decimal::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Ok(Self::default());
}
let decimal = Decimal::from_str(s)?;
Ok(Self::from_decimal(decimal))
}
}
impl ToString for NumberCellData {
fn to_string(&self) -> String {
match &self.money {
None => match self.decimal {
None => String::default(),
Some(decimal) => decimal.to_string(),
},
Some(money) => money.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::CellDataOperation;
use crate::services::field::FieldBuilder;
use crate::services::field::{strip_currency_symbol, NumberFormat, NumberTypeOption};
use flowy_grid_data_model::revision::FieldRevision;
use strum::IntoEnumIterator;
#[test]
fn number_type_option_invalid_input_test() {
let type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
assert_equal(&type_option, "", "", &field_type, &field_rev);
assert_equal(&type_option, "abc", "", &field_type, &field_rev);
}
#[test]
fn number_type_option_strip_symbol_test() {
let mut type_option = NumberTypeOption::new();
type_option.format = NumberFormat::USD;
assert_eq!(strip_currency_symbol("$18,443"), "18,443".to_owned());
type_option.format = NumberFormat::Yuan;
assert_eq!(strip_currency_symbol("$0.2"), "0.2".to_owned());
}
#[test]
fn number_type_option_format_number_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "$18,443", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "¥18,443", &field_type, &field_rev);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "18443", "CN¥18,443", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "€18.443", &field_type, &field_rev);
}
_ => {}
}
}
}
#[test]
fn number_type_option_format_str_test() {
let mut type_option = NumberTypeOption::default();
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
assert_equal(&type_option, "0.2", "0.2", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "$18,44", "$1,844", &field_type, &field_rev);
assert_equal(&type_option, "$0.2", "$0.2", &field_type, &field_rev);
assert_equal(&type_option, "", "", &field_type, &field_rev);
assert_equal(&type_option, "abc", "", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "¥18,44", "¥1,844", &field_type, &field_rev);
assert_equal(&type_option, "¥1844", "¥1,844", &field_type, &field_rev);
}
NumberFormat::Yuan => {
assert_equal(&type_option, "CN¥18,44", "CN¥1,844", &field_type, &field_rev);
assert_equal(&type_option, "CN¥1844", "CN¥1,844", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "€18.44", "€18,44", &field_type, &field_rev);
assert_equal(&type_option, "€0.5", "€0,5", &field_type, &field_rev);
assert_equal(&type_option, "€1844", "€1.844", &field_type, &field_rev);
}
_ => {}
}
}
}
#[test]
fn number_description_sign_test() {
let mut type_option = NumberTypeOption {
sign_positive: false,
..Default::default()
};
let field_type = FieldType::Number;
let field_rev = FieldBuilder::from_field_type(&field_type).build();
for format in NumberFormat::iter() {
type_option.format = format;
match format {
NumberFormat::Num => {
assert_equal(&type_option, "18443", "18443", &field_type, &field_rev);
}
NumberFormat::USD => {
assert_equal(&type_option, "18443", "-$18,443", &field_type, &field_rev);
}
NumberFormat::Yen => {
assert_equal(&type_option, "18443", "-¥18,443", &field_type, &field_rev);
}
NumberFormat::EUR => {
assert_equal(&type_option, "18443", "-€18.443", &field_type, &field_rev);
}
_ => {}
}
}
}
fn assert_equal(
type_option: &NumberTypeOption,
cell_data: &str,
expected_str: &str,
field_type: &FieldType,
field_rev: &FieldRevision,
) {
assert_eq!(
type_option
.decode_cell_data(cell_data.to_owned().into(), field_type, field_rev)
.unwrap()
.to_string(),
expected_str.to_owned()
);
}
}

View File

@ -0,0 +1,105 @@
use crate::services::cell::CellBytesParser;
use crate::services::field::number_currency::Currency;
use crate::services::field::{strip_currency_symbol, NumberFormat, STRIP_SYMBOL};
use bytes::Bytes;
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 = FlowyError;
//
// fn from_str(s: &str) -> Result<Self, Self::Err> {
// if s.is_empty() {
// return Ok(Self::default());
// }
// let decimal = Decimal::from_str(s).map_err(internal_error)?;
// 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(),
}
}
}
pub struct NumberCellDataParser(pub NumberFormat);
impl CellBytesParser for NumberCellDataParser {
type Object = NumberCellData;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => NumberCellData::from_format_str(&s, true, &self.0),
Err(_) => Ok(NumberCellData::default()),
}
}
}

View File

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

View File

@ -1,19 +1,15 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData};
use crate::services::field::select_option::{
make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds,
SelectOptionOperation, SELECTION_IDS_SEPARATOR,
};
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
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
@ -28,8 +24,8 @@ pub struct MultiSelectTypeOption {
impl_type_option!(MultiSelectTypeOption, FieldType::MultiSelect);
impl SelectOptionOperation for MultiSelectTypeOption {
fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData {
let select_options = make_selected_select_options(any_cell_data, &self.options);
fn selected_select_option(&self, cell_data: CellData<SelectOptionIds>) -> SelectOptionCellData {
let select_options = make_selected_select_options(cell_data, &self.options);
SelectOptionCellData {
options: self.options.clone(),
select_options,
@ -50,24 +46,13 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for MultiSele
&self,
cell_data: CellData<SelectOptionIds>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData> {
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if !decoded_field_type.is_select_option() {
return Ok(DecodedCellData::default());
return Ok(CellBytes::default());
}
let ids: SelectOptionIds = cell_data.try_into_inner()?;
let select_options = ids
.iter()
.flat_map(|option_id| self.options.iter().find(|option| &option.id == option_id).cloned())
.collect::<Vec<SelectOption>>();
let cell_data = SelectOptionCellData {
options: self.options.clone(),
select_options,
};
DecodedCellData::try_from_bytes(cell_data)
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
@ -131,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;
@ -195,7 +180,7 @@ mod tests {
type_option
.decode_cell_data(cell_data.into(), &field_type, field_rev)
.unwrap()
.parse::<SelectOptionCellData>()
.with_parser(SelectOptionCellDataParser())
.unwrap()
.select_options,
);

View File

@ -1,8 +1,9 @@
use crate::entities::{CellChangeset, CellIdentifier, CellIdentifierPayload, FieldType};
use crate::services::cell::{AnyCellData, FromCellChangeset, FromCellString};
use crate::services::cell::{CellBytes, CellBytesParser, CellData, CellDisplayable, FromCellChangeset, FromCellString};
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
use bytes::Bytes;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
use flowy_error::{internal_error, ErrorCode, FlowyResult};
use flowy_grid_data_model::parser::NotEmptyStr;
use flowy_grid_data_model::revision::{FieldRevision, TypeOptionDataEntry};
use nanoid::nanoid;
@ -60,12 +61,11 @@ impl std::default::Default for SelectOptionColor {
}
}
pub fn make_selected_select_options<T: TryInto<AnyCellData>>(
any_cell_data: T,
pub fn make_selected_select_options(
cell_data: CellData<SelectOptionIds>,
options: &[SelectOption],
) -> Vec<SelectOption> {
if let Ok(type_option_cell_data) = any_cell_data.try_into() {
let ids = SelectOptionIds::from(type_option_cell_data.data);
if let Ok(ids) = cell_data.try_into_inner() {
ids.iter()
.flat_map(|option_id| options.iter().find(|option| &option.id == option_id).cloned())
.collect()
@ -100,13 +100,27 @@ pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
SelectOption::with_color(name, color)
}
fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData;
fn selected_select_option(&self, cell_data: CellData<SelectOptionIds>) -> SelectOptionCellData;
fn options(&self) -> &Vec<SelectOption>;
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
}
impl<T> CellDisplayable<SelectOptionIds> for T
where
T: SelectOptionOperation,
{
fn display_data(
&self,
cell_data: CellData<SelectOptionIds>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
CellBytes::from(self.selected_select_option(cell_data))
}
}
pub fn select_option_operation(field_rev: &FieldRevision) -> FlowyResult<Box<dyn SelectOptionOperation>> {
let field_type: FieldType = field_rev.field_type_rev.into();
match &field_type {
@ -147,14 +161,6 @@ impl SelectOptionIds {
}
}
impl std::convert::TryFrom<AnyCellData> for SelectOptionIds {
type Error = FlowyError;
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
Ok(Self::from(value.data))
}
}
impl FromCellString for SelectOptionIds {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
@ -196,6 +202,25 @@ impl std::ops::DerefMut for SelectOptionIds {
&mut self.0
}
}
pub struct SelectOptionIdsParser();
impl CellBytesParser for SelectOptionIdsParser {
type Object = SelectOptionIds;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => Ok(SelectOptionIds::from(s)),
Err(_) => Ok(SelectOptionIds::from("".to_owned())),
}
}
}
pub struct SelectOptionCellDataParser();
impl CellBytesParser for SelectOptionCellDataParser {
type Object = SelectOptionCellData;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
SelectOptionCellData::try_from(bytes.as_ref()).map_err(internal_error)
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct SelectOptionCellChangesetPayload {

View File

@ -1,7 +1,7 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData};
use crate::services::field::select_option::{
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
use crate::services::field::{
make_selected_select_options, SelectOption, SelectOptionCellChangeset, SelectOptionCellData, SelectOptionIds,
SelectOptionOperation,
};
@ -24,8 +24,10 @@ pub struct SingleSelectTypeOption {
impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect);
impl SelectOptionOperation for SingleSelectTypeOption {
fn selected_select_option(&self, any_cell_data: AnyCellData) -> SelectOptionCellData {
let select_options = make_selected_select_options(any_cell_data, &self.options);
fn selected_select_option(&self, cell_data: CellData<SelectOptionIds>) -> SelectOptionCellData {
let mut select_options = make_selected_select_options(cell_data, &self.options);
// only keep option in single select
select_options.truncate(1);
SelectOptionCellData {
options: self.options.clone(),
select_options,
@ -46,24 +48,13 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for SingleSel
&self,
cell_data: CellData<SelectOptionIds>,
decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData> {
field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
if !decoded_field_type.is_select_option() {
return Ok(DecodedCellData::default());
return Ok(CellBytes::default());
}
let ids: SelectOptionIds = cell_data.try_into_inner()?;
let mut cell_data = SelectOptionCellData {
options: self.options.clone(),
select_options: vec![],
};
if let Some(option_id) = ids.first() {
if let Some(option) = self.options.iter().find(|option| &option.id == option_id) {
cell_data.select_options.push(option.clone());
}
}
DecodedCellData::try_from_bytes(cell_data)
self.display_data(cell_data, decoded_field_type, field_rev)
}
fn apply_changeset(
@ -111,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;
@ -171,7 +162,7 @@ mod tests {
type_option
.decode_cell_data(cell_data.into(), &field_type, field_rev)
.unwrap()
.parse::<SelectOptionCellData>()
.with_parser(SelectOptionCellDataParser())
.unwrap()
.select_options,
);

View File

@ -0,0 +1,3 @@
#![allow(clippy::module_inception)]
mod text_type_option;
pub use text_type_option::*;

View File

@ -1,7 +1,8 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{
try_decode_cell_data, AnyCellData, CellData, CellDataChangeset, CellDataOperation, DecodedCellData,
try_decode_cell_data, CellBytes, CellBytesParser, CellData, CellDataChangeset, CellDataOperation, CellDisplayable,
FromCellString,
};
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder};
use bytes::Bytes;
@ -32,13 +33,25 @@ pub struct RichTextTypeOption {
}
impl_type_option!(RichTextTypeOption, FieldType::RichText);
impl CellDisplayable<String> for RichTextTypeOption {
fn display_data(
&self,
cell_data: CellData<String>,
_decoded_field_type: &FieldType,
_field_rev: &FieldRevision,
) -> FlowyResult<CellBytes> {
let cell_str: String = cell_data.try_into_inner()?;
Ok(CellBytes::new(cell_str))
}
}
impl CellDataOperation<String, String> for RichTextTypeOption {
fn decode_cell_data(
&self,
cell_data: CellData<String>,
decoded_field_type: &FieldType,
field_rev: &FieldRevision,
) -> FlowyResult<DecodedCellData> {
) -> FlowyResult<CellBytes> {
if decoded_field_type.is_date()
|| decoded_field_type.is_single_select()
|| decoded_field_type.is_multi_select()
@ -46,8 +59,7 @@ impl CellDataOperation<String, String> for RichTextTypeOption {
{
try_decode_cell_data(cell_data, field_rev, decoded_field_type, decoded_field_type)
} else {
let cell_data: String = cell_data.try_into_inner()?;
Ok(DecodedCellData::new(cell_data))
self.display_data(cell_data, decoded_field_type, field_rev)
}
}
@ -72,11 +84,23 @@ impl AsRef<str> for TextCellData {
}
}
impl std::convert::TryFrom<AnyCellData> for TextCellData {
type Error = FlowyError;
impl FromCellString for TextCellData {
fn from_cell_str(s: &str) -> FlowyResult<Self>
where
Self: Sized,
{
Ok(TextCellData(s.to_owned()))
}
}
fn try_from(value: AnyCellData) -> Result<Self, Self::Error> {
Ok(TextCellData(value.data))
pub struct TextCellDataParser();
impl CellBytesParser for TextCellDataParser {
type Object = TextCellData;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
match String::from_utf8(bytes.to_vec()) {
Ok(s) => Ok(TextCellData(s)),
Err(_) => Ok(TextCellData("".to_owned())),
}
}
}
@ -84,7 +108,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::*;
@ -100,7 +124,7 @@ mod tests {
type_option
.decode_cell_data(1647251762.to_string().into(), &field_type, &date_time_field_rev)
.unwrap()
.parse::<DateCellData>()
.with_parser(DateCellDataParser())
.unwrap()
.date,
"Mar 14,2022".to_owned()
@ -120,7 +144,7 @@ mod tests {
&single_select_field_rev
)
.unwrap()
.parse::<SelectOptionCellData>()
.with_parser(SelectOptionCellDataParser())
.unwrap()
.select_options,
vec![done_option],
@ -143,7 +167,7 @@ mod tests {
type_option
.decode_cell_data(cell_data.into(), &FieldType::MultiSelect, &multi_select_field_rev)
.unwrap()
.parse::<SelectOptionCellData>()
.with_parser(SelectOptionCellDataParser())
.unwrap()
.select_options,
vec![google_option, facebook_option]

View File

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

View File

@ -0,0 +1,7 @@
#![allow(clippy::module_inception)]
mod url_tests;
mod url_type_option;
mod url_type_option_entities;
pub use url_type_option::*;
pub use url_type_option_entities::*;

View File

@ -0,0 +1,67 @@
#[cfg(test)]
mod tests {
use crate::entities::FieldType;
use crate::services::cell::{CellData, CellDataOperation};
use crate::services::field::{FieldBuilder, URLCellDataParser};
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()
.with_parser(URLCellDataParser())
.unwrap()
}
}

View File

@ -0,0 +1,95 @@
use crate::entities::FieldType;
use crate::impl_type_option;
use crate::services::cell::{CellBytes, CellData, CellDataChangeset, CellDataOperation, CellDisplayable};
use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder, URLCellData};
use bytes::Bytes;
use fancy_regex::Regex;
use flowy_derive::ProtoBuf;
use flowy_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 content = changeset.try_into_inner()?;
let mut url = "".to_string();
if let Ok(Some(m)) = URL_REGEX.find(&content) {
url = auto_append_scheme(m.as_str());
}
URLCellData { url, content }.to_json()
}
}
fn auto_append_scheme(s: &str) -> String {
// Only support https scheme by now
match url::Url::parse(s) {
Ok(url) => {
if url.scheme() == "https" {
url.into()
} else {
format!("https://{}", s)
}
}
Err(_) => {
format!("https://{}", s)
}
}
}
lazy_static! {
static ref URL_REGEX: Regex = Regex::new(
"[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)"
)
.unwrap();
}

View File

@ -0,0 +1,42 @@
use crate::services::cell::{CellBytesParser, FromCellString};
use bytes::Bytes;
use flowy_derive::ProtoBuf;
use flowy_error::{internal_error, 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)
}
}
pub struct URLCellDataParser();
impl CellBytesParser for URLCellDataParser {
type Object = URLCellData;
fn parse(&self, bytes: &Bytes) -> FlowyResult<Self::Object> {
URLCellData::try_from(bytes.as_ref()).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)
}
}

View File

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

View File

@ -1,5 +1,5 @@
use crate::entities::{CheckboxCondition, GridCheckboxFilter};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::field::{CheckboxCellData, CheckboxTypeOption};
use flowy_error::FlowyResult;
@ -18,7 +18,8 @@ impl CellFilterOperation<GridCheckboxFilter> for CheckboxTypeOption {
if !any_cell_data.is_checkbox() {
return Ok(true);
}
let checkbox_cell_data: CheckboxCellData = any_cell_data.try_into()?;
let cell_data: CellData<CheckboxCellData> = any_cell_data.into();
let checkbox_cell_data = cell_data.try_into_inner()?;
Ok(filter.is_visible(&checkbox_cell_data))
}
}
@ -27,6 +28,7 @@ impl CellFilterOperation<GridCheckboxFilter> for CheckboxTypeOption {
mod tests {
use crate::entities::{CheckboxCondition, GridCheckboxFilter};
use crate::services::field::CheckboxCellData;
use std::str::FromStr;
#[test]
fn checkbox_filter_is_check_test() {
@ -34,7 +36,7 @@ mod tests {
condition: CheckboxCondition::IsChecked,
};
for (value, visible) in [("true", true), ("yes", true), ("false", false), ("no", false)] {
let data = CheckboxCellData(value.to_owned());
let data = CheckboxCellData::from_str(value).unwrap();
assert_eq!(checkbox_filter.is_visible(&data), visible);
}
}
@ -45,7 +47,7 @@ mod tests {
condition: CheckboxCondition::IsUnChecked,
};
for (value, visible) in [("false", true), ("no", true), ("true", false), ("yes", false)] {
let data = CheckboxCellData(value.to_owned());
let data = CheckboxCellData::from_str(value).unwrap();
assert_eq!(checkbox_filter.is_visible(&data), visible);
}
}

View File

@ -1,5 +1,5 @@
use crate::entities::{DateFilterCondition, GridDateFilter};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::field::{DateTimestamp, DateTypeOption};
use flowy_error::FlowyResult;
@ -34,7 +34,8 @@ impl CellFilterOperation<GridDateFilter> for DateTypeOption {
if !any_cell_data.is_date() {
return Ok(true);
}
let timestamp: DateTimestamp = any_cell_data.into();
let cell_data: CellData<DateTimestamp> = any_cell_data.into();
let timestamp = cell_data.try_into_inner()?;
Ok(filter.is_visible(timestamp))
}
}

View File

@ -47,9 +47,7 @@ impl CellFilterOperation<GridNumberFilter> for NumberTypeOption {
#[cfg(test)]
mod tests {
use crate::entities::{GridNumberFilter, NumberFilterCondition};
use crate::services::field::{NumberCellData, NumberFormat};
use std::str::FromStr;
#[test]
fn number_filter_equal_test() {
let number_filter = GridNumberFilter {
@ -58,7 +56,7 @@ mod tests {
};
for (num_str, visible) in [("123", true), ("1234", false), ("", false)] {
let data = NumberCellData::from_str(num_str).unwrap();
let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap();
assert_eq!(number_filter.is_visible(&data), visible);
}
@ -75,7 +73,7 @@ mod tests {
content: Some("12".to_owned()),
};
for (num_str, visible) in [("123", true), ("10", false), ("30", true), ("", false)] {
let data = NumberCellData::from_str(num_str).unwrap();
let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap();
assert_eq!(number_filter.is_visible(&data), visible);
}
}
@ -87,7 +85,7 @@ mod tests {
content: Some("100".to_owned()),
};
for (num_str, visible) in [("12", true), ("1234", false), ("30", true), ("", true)] {
let data = NumberCellData::from_str(num_str).unwrap();
let data = NumberCellData::from_format_str(num_str, true, &NumberFormat::Num).unwrap();
assert_eq!(number_filter.is_visible(&data), visible);
}
}

View File

@ -2,8 +2,8 @@
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::field::select_option::{SelectOptionOperation, SelectedSelectOptions};
use crate::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
use crate::services::field::{SelectOptionOperation, SelectedSelectOptions};
use flowy_error::FlowyResult;
impl GridSelectOptionFilter {
@ -45,7 +45,7 @@ impl CellFilterOperation<GridSelectOptionFilter> for MultiSelectTypeOption {
return Ok(true);
}
let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data));
let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data.into()));
Ok(filter.is_visible(&selected_options))
}
}
@ -55,7 +55,7 @@ impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
if !any_cell_data.is_single_select() {
return Ok(true);
}
let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data));
let selected_options = SelectedSelectOptions::from(self.selected_select_option(any_cell_data.into()));
Ok(filter.is_visible(&selected_options))
}
}
@ -64,7 +64,7 @@ impl CellFilterOperation<GridSelectOptionFilter> for SingleSelectTypeOption {
mod tests {
#![allow(clippy::all)]
use crate::entities::{GridSelectOptionFilter, SelectOptionCondition};
use crate::services::field::select_option::{SelectOption, SelectedSelectOptions};
use crate::services::field::selection_type_option::{SelectOption, SelectedSelectOptions};
#[test]
fn select_option_filter_is_test() {

View File

@ -1,5 +1,5 @@
use crate::entities::{GridTextFilter, TextFilterCondition};
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::field::{RichTextTypeOption, TextCellData};
use flowy_error::FlowyResult;
@ -30,7 +30,8 @@ impl CellFilterOperation<GridTextFilter> for RichTextTypeOption {
return Ok(true);
}
let text_cell_data: TextCellData = any_cell_data.try_into()?;
let cell_data: CellData<TextCellData> = any_cell_data.into();
let text_cell_data = cell_data.try_into_inner()?;
Ok(filter.is_visible(text_cell_data))
}
}

View File

@ -1,5 +1,5 @@
use crate::entities::GridTextFilter;
use crate::services::cell::{AnyCellData, CellFilterOperation};
use crate::services::cell::{AnyCellData, CellData, CellFilterOperation};
use crate::services::field::{TextCellData, URLTypeOption};
use flowy_error::FlowyResult;
@ -9,7 +9,8 @@ impl CellFilterOperation<GridTextFilter> for URLTypeOption {
return Ok(true);
}
let text_cell_data: TextCellData = any_cell_data.try_into()?;
let cell_data: CellData<TextCellData> = any_cell_data.into();
let text_cell_data = cell_data.try_into_inner()?;
Ok(filter.is_visible(&text_cell_data))
}
}

View File

@ -3,13 +3,12 @@ use crate::entities::CellIdentifier;
use crate::entities::*;
use crate::manager::{GridTaskSchedulerRwLock, GridUser};
use crate::services::block_manager::GridBlockManager;
use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data};
use crate::services::cell::{apply_cell_data_changeset, decode_any_cell_data, CellBytes};
use crate::services::field::{default_type_option_builder_from_type, type_option_builder_from_bytes, FieldBuilder};
use crate::services::filter::{GridFilterChangeset, GridFilterService};
use crate::services::persistence::block_index::BlockIndexCache;
use crate::services::row::{
make_grid_blocks, make_row_from_row_rev, make_row_rev_from_context, make_rows_from_row_revs,
CreateRowRevisionBuilder, CreateRowRevisionPayload, GridBlockSnapshot,
make_grid_blocks, make_row_from_row_rev, make_rows_from_row_revs, GridBlockSnapshot, RowRevisionBuilder,
};
use crate::services::setting::make_grid_setting;
use bytes::Bytes;
@ -274,8 +273,7 @@ impl GridRevisionEditor {
let block_id = self.block_id().await?;
// insert empty row below the row whose id is upper_row_id
let row_rev_ctx = CreateRowRevisionBuilder::new(&field_revs).build();
let row_rev = make_row_rev_from_context(&block_id, row_rev_ctx);
let row_rev = RowRevisionBuilder::new(&field_revs).build(&block_id);
let row_order = Row::from(&row_rev);
// insert the row
@ -287,12 +285,11 @@ impl GridRevisionEditor {
Ok(row_order)
}
pub async fn insert_rows(&self, contexts: Vec<CreateRowRevisionPayload>) -> FlowyResult<Vec<Row>> {
pub async fn insert_rows(&self, row_revs: Vec<RowRevision>) -> FlowyResult<Vec<Row>> {
let block_id = self.block_id().await?;
let mut rows_by_block_id: HashMap<String, Vec<RowRevision>> = HashMap::new();
let mut row_orders = vec![];
for ctx in contexts {
let row_rev = make_row_rev_from_context(&block_id, ctx);
for row_rev in row_revs {
row_orders.push(Row::from(&row_rev));
rows_by_block_id
.entry(block_id.clone())
@ -307,10 +304,7 @@ impl GridRevisionEditor {
}
pub async fn update_row(&self, changeset: RowMetaChangeset) -> FlowyResult<()> {
let field_revs = self.get_field_revs(None).await?;
self.block_manager
.update_row(changeset, |row_rev| make_row_from_row_rev(&field_revs, row_rev))
.await
self.block_manager.update_row(changeset, make_row_from_row_rev).await
}
pub async fn get_rows(&self, block_id: &str) -> FlowyResult<RepeatedRow> {
@ -322,26 +316,20 @@ impl GridRevisionEditor {
debug_assert_eq!(grid_block_snapshot.len(), 1);
if grid_block_snapshot.len() == 1 {
let snapshot = grid_block_snapshot.pop().unwrap();
let field_revs = self.get_field_revs(None).await?;
let rows = make_rows_from_row_revs(&field_revs, &snapshot.row_revs);
let rows = make_rows_from_row_revs(&snapshot.row_revs);
Ok(rows.into())
} else {
Ok(vec![].into())
}
}
pub async fn get_row(&self, row_id: &str) -> FlowyResult<Option<Row>> {
pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult<Option<Arc<RowRevision>>> {
match self.block_manager.get_row_rev(row_id).await? {
None => Ok(None),
Some(row_rev) => {
let field_revs = self.get_field_revs(None).await?;
let row_revs = vec![row_rev];
let mut rows = make_rows_from_row_revs(&field_revs, &row_revs);
debug_assert!(rows.len() == 1);
Ok(rows.pop())
}
Some(row_rev) => Ok(Some(row_rev)),
}
}
pub async fn delete_row(&self, row_id: &str) -> FlowyResult<()> {
let _ = self.block_manager.delete_row(row_id).await?;
Ok(())
@ -352,12 +340,16 @@ impl GridRevisionEditor {
}
pub async fn get_cell(&self, params: &CellIdentifier) -> Option<Cell> {
let cell_bytes = self.get_cell_bytes(params).await?;
Some(Cell::new(&params.field_id, cell_bytes.to_vec()))
}
pub async fn get_cell_bytes(&self, params: &CellIdentifier) -> Option<CellBytes> {
let field_rev = self.get_field_rev(&params.field_id).await?;
let row_rev = self.block_manager.get_row_rev(&params.row_id).await.ok()??;
let cell_rev = row_rev.cells.get(&params.field_id)?.clone();
let data = decode_any_cell_data(cell_rev.data, &field_rev).data;
Some(Cell::new(&params.field_id, data))
Some(decode_any_cell_data(cell_rev.data, &field_rev))
}
pub async fn get_cell_rev(&self, row_id: &str, field_id: &str) -> FlowyResult<Option<CellRevision>> {
@ -395,7 +387,6 @@ impl GridRevisionEditor {
let cell_rev = self.get_cell_rev(&row_id, &field_id).await?;
// Update the changeset.data property with the return value.
content = Some(apply_cell_data_changeset(content.unwrap(), cell_rev, field_rev)?);
let field_revs = self.get_field_revs(None).await?;
let cell_changeset = CellChangeset {
grid_id,
row_id,
@ -404,7 +395,7 @@ impl GridRevisionEditor {
};
let _ = self
.block_manager
.update_cell(cell_changeset, |row_rev| make_row_from_row_rev(&field_revs, row_rev))
.update_cell(cell_changeset, make_row_from_row_rev)
.await?;
Ok(())
}
@ -561,7 +552,7 @@ impl GridRevisionEditor {
drop(grid_pad);
Ok(BuildGridContext {
field_revs: duplicated_fields,
field_revs: duplicated_fields.into_iter().map(Arc::new).collect(),
blocks: duplicated_blocks,
blocks_meta_data,
})

View File

@ -1,22 +1,22 @@
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;
use std::collections::HashMap;
use std::sync::Arc;
pub struct CreateRowRevisionBuilder<'a> {
field_rev_map: HashMap<&'a String, &'a Arc<FieldRevision>>,
pub struct RowRevisionBuilder<'a> {
field_rev_map: HashMap<&'a String, Arc<FieldRevision>>,
payload: CreateRowRevisionPayload,
}
impl<'a> CreateRowRevisionBuilder<'a> {
impl<'a> RowRevisionBuilder<'a> {
pub fn new(fields: &'a [Arc<FieldRevision>]) -> Self {
let field_rev_map = fields
.iter()
.map(|field| (&field.id, field))
.collect::<HashMap<&String, &Arc<FieldRevision>>>();
.map(|field| (&field.id, field.clone()))
.collect::<HashMap<&String, Arc<FieldRevision>>>();
let payload = CreateRowRevisionPayload {
row_id: gen_row_id(),
@ -28,10 +28,10 @@ impl<'a> CreateRowRevisionBuilder<'a> {
Self { field_rev_map, payload }
}
pub fn add_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> {
pub fn insert_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> {
match self.field_rev_map.get(&field_id.to_owned()) {
None => {
let msg = format!("Invalid field_id: {}", field_id);
let msg = format!("Can't find the field with id: {}", field_id);
Err(FlowyError::internal().context(msg))
}
Some(field_rev) => {
@ -43,7 +43,7 @@ impl<'a> CreateRowRevisionBuilder<'a> {
}
}
pub fn add_select_option_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> {
pub fn insert_select_option_cell(&mut self, field_id: &str, data: String) -> FlowyResult<()> {
match self.field_rev_map.get(&field_id.to_owned()) {
None => {
let msg = format!("Invalid field_id: {}", field_id);
@ -71,18 +71,14 @@ impl<'a> CreateRowRevisionBuilder<'a> {
self
}
pub fn build(self) -> CreateRowRevisionPayload {
self.payload
}
}
pub fn make_row_rev_from_context(block_id: &str, payload: CreateRowRevisionPayload) -> RowRevision {
RowRevision {
id: payload.row_id,
block_id: block_id.to_owned(),
cells: payload.cell_by_field_id,
height: payload.height,
visibility: payload.visibility,
pub fn build(self, block_id: &str) -> RowRevision {
RowRevision {
id: self.payload.row_id,
block_id: block_id.to_owned(),
cells: self.payload.cell_by_field_id,
height: self.payload.height,
visibility: self.payload.visibility,
}
}
}

View File

@ -1,6 +1,6 @@
use crate::entities::{GridBlock, RepeatedGridBlock, Row};
use flowy_error::FlowyResult;
use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
use flowy_grid_data_model::revision::RowRevision;
use std::collections::HashMap;
use std::sync::Arc;
@ -39,29 +39,15 @@ pub(crate) fn make_row_orders_from_row_revs(row_revs: &[Arc<RowRevision>]) -> Ve
row_revs.iter().map(Row::from).collect::<Vec<_>>()
}
pub(crate) fn make_row_from_row_rev(fields: &[Arc<FieldRevision>], row_rev: Arc<RowRevision>) -> Option<Row> {
make_rows_from_row_revs(fields, &[row_rev]).pop()
pub(crate) fn make_row_from_row_rev(row_rev: Arc<RowRevision>) -> Option<Row> {
make_rows_from_row_revs(&[row_rev]).pop()
}
pub(crate) fn make_rows_from_row_revs(_fields: &[Arc<FieldRevision>], row_revs: &[Arc<RowRevision>]) -> Vec<Row> {
// let field_rev_map = fields
// .iter()
// .map(|field_rev| (&field_rev.id, field_rev))
// .collect::<HashMap<&String, &FieldRevision>>();
let make_row = |row_rev: &Arc<RowRevision>| {
// let cell_by_field_id = row_rev
// .cells
// .clone()
// .into_iter()
// .flat_map(|(field_id, cell_rev)| make_cell_by_field_id(&field_rev_map, field_id, cell_rev))
// .collect::<HashMap<String, Cell>>();
Row {
block_id: row_rev.block_id.clone(),
id: row_rev.id.clone(),
height: row_rev.height,
}
pub(crate) fn make_rows_from_row_revs(row_revs: &[Arc<RowRevision>]) -> Vec<Row> {
let make_row = |row_rev: &Arc<RowRevision>| Row {
block_id: row_rev.block_id.clone(),
id: row_rev.id.clone(),
height: row_rev.height,
};
row_revs.iter().map(make_row).collect::<Vec<_>>()

View File

@ -4,29 +4,29 @@ use flowy_grid_data_model::revision::BuildGridContext;
use flowy_sync::client_grid::GridBuilder;
pub fn make_default_grid() -> BuildGridContext {
let mut grid_builder = GridBuilder::new();
// text
let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
.name("Name")
.visibility(true)
.primary(true)
.build();
grid_builder.add_field(text_field);
// single select
let single_select = SingleSelectTypeOptionBuilder::default();
let single_select_field = FieldBuilder::new(single_select).name("Type").visibility(true).build();
grid_builder.add_field(single_select_field);
// checkbox
let checkbox_field = FieldBuilder::from_field_type(&FieldType::Checkbox)
.name("Done")
.visibility(true)
.build();
grid_builder.add_field(checkbox_field);
GridBuilder::default()
.add_field(text_field)
.add_field(single_select_field)
.add_field(checkbox_field)
.add_empty_row()
.add_empty_row()
.add_empty_row()
.build()
grid_builder.add_empty_row();
grid_builder.add_empty_row();
grid_builder.add_empty_row();
grid_builder.build()
}

View File

@ -1,26 +1,21 @@
use crate::grid::block_test::script::GridRowTest;
use crate::grid::block_test::script::RowScript::*;
use crate::grid::block_test::util::GridRowTestBuilder;
use chrono::NaiveDateTime;
use crate::grid::block_test::script::{CreateRowScriptBuilder, GridRowTest};
use crate::grid::grid_editor::{COMPLETED, FACEBOOK, GOOGLE, PAUSED, TWITTER};
use flowy_grid::entities::FieldType;
use flowy_grid::services::cell::decode_any_cell_data;
use flowy_grid::services::field::select_option::SELECTION_IDS_SEPARATOR;
use flowy_grid::services::field::{DateCellData, MultiSelectTypeOption, SingleSelectTypeOption};
use crate::grid::field_test::util::make_date_cell_string;
use flowy_grid::services::field::{NO, SELECTION_IDS_SEPARATOR};
use flowy_grid_data_model::revision::RowMetaChangeset;
#[tokio::test]
async fn grid_create_row_count_test() {
let mut test = GridRowTest::new().await;
let scripts = vec![
AssertRowCount(3),
AssertRowCount(5),
CreateEmptyRow,
CreateEmptyRow,
CreateRow {
payload: GridRowTestBuilder::new(&test).build(),
row_rev: test.row_builder().build(),
},
AssertRowCount(6),
AssertRowCount(8),
];
test.run_scripts(scripts).await;
}
@ -28,42 +23,42 @@ async fn grid_create_row_count_test() {
#[tokio::test]
async fn grid_update_row() {
let mut test = GridRowTest::new().await;
let payload = GridRowTestBuilder::new(&test).build();
let row_rev = test.row_builder().build();
let changeset = RowMetaChangeset {
row_id: payload.row_id.clone(),
row_id: row_rev.id.clone(),
height: None,
visibility: None,
cell_by_field_id: Default::default(),
};
let scripts = vec![AssertRowCount(3), CreateRow { payload }, UpdateRow { changeset }];
let scripts = vec![AssertRowCount(5), CreateRow { row_rev }, UpdateRow { changeset }];
test.run_scripts(scripts).await;
let expected_row = test.last_row().unwrap();
let scripts = vec![AssertRow { expected_row }, AssertRowCount(4)];
let scripts = vec![AssertRow { expected_row }, AssertRowCount(6)];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_delete_row() {
let mut test = GridRowTest::new().await;
let payload1 = GridRowTestBuilder::new(&test).build();
let payload2 = GridRowTestBuilder::new(&test).build();
let row_ids = vec![payload1.row_id.clone(), payload2.row_id.clone()];
let row_1 = test.row_builder().build();
let row_2 = test.row_builder().build();
let row_ids = vec![row_1.id.clone(), row_2.id.clone()];
let scripts = vec![
AssertRowCount(3),
CreateRow { payload: payload1 },
CreateRow { payload: payload2 },
AssertRowCount(5),
CreateRow { row_rev: row_1 },
CreateRow { row_rev: row_2 },
AssertBlockCount(1),
AssertBlock {
block_index: 0,
row_count: 5,
row_count: 7,
start_row_index: 0,
},
DeleteRows { row_ids },
AssertBlock {
block_index: 0,
row_count: 3,
row_count: 5,
start_row_index: 0,
},
];
@ -73,78 +68,68 @@ async fn grid_delete_row() {
#[tokio::test]
async fn grid_row_add_cells_test() {
let mut test = GridRowTest::new().await;
let mut builder = test.builder();
for field in test.field_revs() {
let field_type: FieldType = field.field_type_rev.into();
match field_type {
FieldType::RichText => {
builder.add_cell(&field.id, "hello world".to_owned()).unwrap();
}
FieldType::Number => {
builder.add_cell(&field.id, "18,443".to_owned()).unwrap();
}
FieldType::DateTime => {
builder
.add_cell(&field.id, make_date_cell_string("1647251762"))
.unwrap();
}
FieldType::SingleSelect => {
let type_option = SingleSelectTypeOption::from(field);
let option = type_option.options.first().unwrap();
builder.add_select_option_cell(&field.id, option.id.clone()).unwrap();
}
FieldType::MultiSelect => {
let type_option = MultiSelectTypeOption::from(field);
let ops_ids = type_option
.options
.iter()
.map(|option| option.id.clone())
.collect::<Vec<_>>()
.join(SELECTION_IDS_SEPARATOR);
builder.add_select_option_cell(&field.id, ops_ids).unwrap();
}
FieldType::Checkbox => {
builder.add_cell(&field.id, "false".to_string()).unwrap();
}
FieldType::URL => {
builder.add_cell(&field.id, "1".to_string()).unwrap();
}
}
}
let context = builder.build();
let scripts = vec![CreateRow { payload: context }];
let mut builder = CreateRowScriptBuilder::new(&test);
builder.insert(FieldType::RichText, "hello world", "hello world");
builder.insert(FieldType::DateTime, "1647251762", "2022/03/14");
builder.insert(FieldType::Number, "18,443", "$18,443.00");
builder.insert(FieldType::Checkbox, "false", NO);
builder.insert(FieldType::URL, "https://appflowy.io", "https://appflowy.io");
builder.insert_single_select_cell(|mut options| options.remove(0), COMPLETED);
builder.insert_multi_select_cell(
|options| options,
&vec![GOOGLE, FACEBOOK, TWITTER].join(SELECTION_IDS_SEPARATOR),
);
let scripts = builder.build();
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_row_add_date_cell_test() {
async fn grid_row_insert_number_test() {
let mut test = GridRowTest::new().await;
let mut builder = test.builder();
let mut date_field = None;
let timestamp = 1647390674;
for field in test.field_revs() {
let field_type: FieldType = field.field_type_rev.into();
if field_type == FieldType::DateTime {
date_field = Some(field.clone());
NaiveDateTime::from_timestamp(123, 0);
// The data should not be empty
assert!(builder.add_cell(&field.id, "".to_string()).is_err());
assert!(builder.add_cell(&field.id, make_date_cell_string("123")).is_ok());
assert!(builder
.add_cell(&field.id, make_date_cell_string(&timestamp.to_string()))
.is_ok());
}
for (val, expected) in &[("1647251762", "2022/03/14"), ("2022/03/14", ""), ("", "")] {
let mut builder = CreateRowScriptBuilder::new(&test);
builder.insert(FieldType::DateTime, val, expected);
let scripts = builder.build();
test.run_scripts(scripts).await;
}
let context = builder.build();
let date_field = date_field.unwrap();
let cell_rev = context.cell_by_field_id.get(&date_field.id).unwrap();
assert_eq!(
decode_any_cell_data(cell_rev, &date_field)
.parse::<DateCellData>()
.unwrap()
.date,
"2022/03/16",
);
let scripts = vec![CreateRow { payload: context }];
}
#[tokio::test]
async fn grid_row_insert_date_test() {
let mut test = GridRowTest::new().await;
for (val, expected) in &[
("18,443", "$18,443.00"),
("0", "$0.00"),
("100000", "$100,000.00"),
("$100,000.00", "$100,000.00"),
("", ""),
] {
let mut builder = CreateRowScriptBuilder::new(&test);
builder.insert(FieldType::Number, val, expected);
let scripts = builder.build();
test.run_scripts(scripts).await;
}
}
#[tokio::test]
async fn grid_row_insert_single_select_test() {
let mut test = GridRowTest::new().await;
let mut builder = CreateRowScriptBuilder::new(&test);
builder.insert_single_select_cell(|mut options| options.pop().unwrap(), PAUSED);
let scripts = builder.build();
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_row_insert_multi_select_test() {
let mut test = GridRowTest::new().await;
let mut builder = CreateRowScriptBuilder::new(&test);
builder.insert_multi_select_cell(
|mut options| {
options.remove(0);
options
},
&vec![FACEBOOK, TWITTER].join(SELECTION_IDS_SEPARATOR),
);
let scripts = builder.build();
test.run_scripts(scripts).await;
}

View File

@ -1,15 +1,20 @@
use crate::grid::block_test::script::RowScript::{AssertCell, CreateRow};
use crate::grid::block_test::util::GridRowTestBuilder;
use crate::grid::grid_editor::GridEditorTest;
use flowy_grid::entities::Row;
use flowy_grid::services::row::{CreateRowRevisionBuilder, CreateRowRevisionPayload};
use flowy_grid::entities::{CellIdentifier, FieldType, Row};
use flowy_grid::services::field::*;
use flowy_grid_data_model::revision::{
FieldRevision, GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision,
GridBlockMetaRevision, GridBlockMetaRevisionChangeset, RowMetaChangeset, RowRevision,
};
use std::collections::HashMap;
use std::sync::Arc;
use strum::IntoEnumIterator;
pub enum RowScript {
CreateEmptyRow,
CreateRow {
payload: CreateRowRevisionPayload,
row_rev: RowRevision,
},
UpdateRow {
changeset: RowMetaChangeset,
@ -20,6 +25,12 @@ pub enum RowScript {
DeleteRows {
row_ids: Vec<String>,
},
AssertCell {
row_id: String,
field_id: String,
field_type: FieldType,
expected: String,
},
AssertRowCount(usize),
CreateBlock {
block: GridBlockMetaRevision,
@ -49,10 +60,6 @@ impl GridRowTest {
Self { inner: editor_test }
}
pub fn field_revs(&self) -> &Vec<Arc<FieldRevision>> {
&self.field_revs
}
pub fn last_row(&self) -> Option<RowRevision> {
self.row_revs.last().map(|a| a.clone().as_ref().clone())
}
@ -63,8 +70,8 @@ impl GridRowTest {
}
}
pub fn builder(&self) -> CreateRowRevisionBuilder {
CreateRowRevisionBuilder::new(&self.field_revs)
pub fn row_builder(&self) -> GridRowTestBuilder {
GridRowTestBuilder::new(self.block_id(), &self.field_revs)
}
pub async fn run_script(&mut self, script: RowScript) {
@ -76,8 +83,8 @@ impl GridRowTest {
self.row_revs = self.get_row_revs().await;
self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap();
}
RowScript::CreateRow { payload: context } => {
let row_orders = self.editor.insert_rows(vec![context]).await.unwrap();
RowScript::CreateRow { row_rev } => {
let row_orders = self.editor.insert_rows(vec![row_rev]).await.unwrap();
for row_order in row_orders {
self.row_order_by_row_id
.insert(row_order.row_id().to_owned(), row_order);
@ -96,6 +103,19 @@ impl GridRowTest {
self.row_revs = self.get_row_revs().await;
self.block_meta_revs = self.editor.get_block_meta_revs().await.unwrap();
}
RowScript::AssertCell {
row_id,
field_id,
field_type,
expected,
} => {
let id = CellIdentifier {
grid_id: self.grid_id.clone(),
field_id,
row_id,
};
self.compare_cell_content(id, field_type, expected).await;
}
RowScript::AssertRow { expected_row } => {
let row = &*self
.row_revs
@ -133,6 +153,99 @@ impl GridRowTest {
}
}
}
async fn compare_cell_content(&self, cell_id: CellIdentifier, field_type: FieldType, expected: String) {
match field_type {
FieldType::RichText => {
let cell_data = self
.editor
.get_cell_bytes(&cell_id)
.await
.unwrap()
.with_parser(TextCellDataParser())
.unwrap();
assert_eq!(cell_data.as_ref(), &expected);
}
FieldType::Number => {
let field_rev = self.editor.get_field_rev(&cell_id.field_id).await.unwrap();
let number_type_option = field_rev
.get_type_option_entry::<NumberTypeOption>(FieldType::Number.into())
.unwrap();
let cell_data = self
.editor
.get_cell_bytes(&cell_id)
.await
.unwrap()
.with_parser(NumberCellDataParser(number_type_option.format))
.unwrap();
assert_eq!(cell_data.to_string(), expected);
}
FieldType::DateTime => {
let cell_data = self
.editor
.get_cell_bytes(&cell_id)
.await
.unwrap()
.with_parser(DateCellDataParser())
.unwrap();
assert_eq!(cell_data.date, expected);
}
FieldType::SingleSelect => {
let cell_data = self
.editor
.get_cell_bytes(&cell_id)
.await
.unwrap()
.with_parser(SelectOptionCellDataParser())
.unwrap();
let select_option = cell_data.select_options.first().unwrap();
assert_eq!(select_option.name, expected);
}
FieldType::MultiSelect => {
let cell_data = self
.editor
.get_cell_bytes(&cell_id)
.await
.unwrap()
.with_parser(SelectOptionCellDataParser())
.unwrap();
let s = cell_data
.select_options
.into_iter()
.map(|option| option.name)
.collect::<Vec<String>>()
.join(SELECTION_IDS_SEPARATOR);
assert_eq!(s, expected);
}
FieldType::Checkbox => {
let cell_data = self
.editor
.get_cell_bytes(&cell_id)
.await
.unwrap()
.with_parser(CheckboxCellDataParser())
.unwrap();
assert_eq!(cell_data.to_string(), expected);
}
FieldType::URL => {
let cell_data = self
.editor
.get_cell_bytes(&cell_id)
.await
.unwrap()
.with_parser(URLCellDataParser())
.unwrap();
assert_eq!(cell_data.content, expected);
// assert_eq!(cell_data.url, expected);
}
}
}
}
impl std::ops::Deref for GridRowTest {
@ -148,3 +261,113 @@ impl std::ops::DerefMut for GridRowTest {
&mut self.inner
}
}
pub struct CreateRowScriptBuilder<'a> {
builder: GridRowTestBuilder<'a>,
data_by_field_type: HashMap<FieldType, CellTestData>,
output_by_field_type: HashMap<FieldType, CellTestOutput>,
}
impl<'a> CreateRowScriptBuilder<'a> {
pub fn new(test: &'a GridRowTest) -> Self {
Self {
builder: test.row_builder(),
data_by_field_type: HashMap::new(),
output_by_field_type: HashMap::new(),
}
}
pub fn insert(&mut self, field_type: FieldType, input: &str, expected: &str) {
self.data_by_field_type.insert(
field_type,
CellTestData {
input: input.to_string(),
expected: expected.to_owned(),
},
);
}
pub fn insert_single_select_cell<F>(&mut self, f: F, expected: &str)
where
F: Fn(Vec<SelectOption>) -> SelectOption,
{
let field_id = self.builder.insert_single_select_cell(f);
self.output_by_field_type.insert(
FieldType::SingleSelect,
CellTestOutput {
field_id,
expected: expected.to_owned(),
},
);
}
pub fn insert_multi_select_cell<F>(&mut self, f: F, expected: &str)
where
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
{
let field_id = self.builder.insert_multi_select_cell(f);
self.output_by_field_type.insert(
FieldType::MultiSelect,
CellTestOutput {
field_id,
expected: expected.to_owned(),
},
);
}
pub fn build(mut self) -> Vec<RowScript> {
let mut scripts = vec![];
let output_by_field_type = &mut self.output_by_field_type;
for field_type in FieldType::iter() {
let field_type: FieldType = field_type;
if let Some(data) = self.data_by_field_type.get(&field_type) {
let field_id = match field_type {
FieldType::RichText => self.builder.insert_text_cell(&data.input),
FieldType::Number => self.builder.insert_number_cell(&data.input),
FieldType::DateTime => self.builder.insert_date_cell(&data.input),
FieldType::Checkbox => self.builder.insert_checkbox_cell(&data.input),
FieldType::URL => self.builder.insert_url_cell(&data.input),
_ => "".to_owned(),
};
if !field_id.is_empty() {
output_by_field_type.insert(
field_type,
CellTestOutput {
field_id,
expected: data.expected.clone(),
},
);
}
}
}
let row_rev = self.builder.build();
let row_id = row_rev.id.clone();
scripts.push(CreateRow { row_rev });
for field_type in FieldType::iter() {
if let Some(data) = output_by_field_type.get(&field_type) {
let script = AssertCell {
row_id: row_id.clone(),
field_id: data.field_id.clone(),
field_type,
expected: data.expected.clone(),
};
scripts.push(script);
}
}
scripts
}
}
pub struct CellTestData {
pub input: String,
pub expected: String,
}
struct CellTestOutput {
field_id: String,
expected: String,
}

View File

@ -1,66 +1,109 @@
use crate::grid::block_test::script::GridRowTest;
use flowy_grid::entities::FieldType;
use flowy_grid::services::field::DateCellChangeset;
use flowy_grid::services::row::{CreateRowRevisionBuilder, CreateRowRevisionPayload};
use flowy_grid_data_model::revision::FieldRevision;
use std::sync::Arc;
use flowy_grid::services::field::{
DateCellChangeset, MultiSelectTypeOption, SelectOption, SingleSelectTypeOption, SELECTION_IDS_SEPARATOR,
};
use flowy_grid::services::row::RowRevisionBuilder;
use flowy_grid_data_model::revision::{FieldRevision, RowRevision};
use strum::EnumCount;
pub struct GridRowTestBuilder<'a> {
test: &'a GridRowTest,
inner_builder: CreateRowRevisionBuilder<'a>,
block_id: String,
field_revs: &'a [Arc<FieldRevision>],
inner_builder: RowRevisionBuilder<'a>,
}
impl<'a> GridRowTestBuilder<'a> {
pub fn new(test: &'a GridRowTest) -> Self {
assert_eq!(test.field_revs().len(), FieldType::COUNT);
let inner_builder = CreateRowRevisionBuilder::new(test.field_revs());
Self { test, inner_builder }
}
#[allow(dead_code)]
pub fn update_text_cell(mut self, data: String) -> Self {
let text_field = self.field_rev_with_type(&FieldType::DateTime);
self.inner_builder.add_cell(&text_field.id, data).unwrap();
self
pub fn new(block_id: &str, field_revs: &'a [Arc<FieldRevision>]) -> Self {
assert_eq!(field_revs.len(), FieldType::COUNT);
let inner_builder = RowRevisionBuilder::new(field_revs);
Self {
block_id: block_id.to_owned(),
field_revs,
inner_builder,
}
}
#[allow(dead_code)]
pub fn update_number_cell(mut self, data: String) -> Self {
let number_field = self.field_rev_with_type(&FieldType::DateTime);
self.inner_builder.add_cell(&number_field.id, data).unwrap();
self
pub fn insert_text_cell(&mut self, data: &str) -> String {
let text_field = self.field_rev_with_type(&FieldType::RichText);
self.inner_builder
.insert_cell(&text_field.id, data.to_string())
.unwrap();
text_field.id.clone()
}
#[allow(dead_code)]
pub fn update_date_cell(mut self, value: i64) -> Self {
pub fn insert_number_cell(&mut self, data: &str) -> String {
let number_field = self.field_rev_with_type(&FieldType::Number);
self.inner_builder
.insert_cell(&number_field.id, data.to_string())
.unwrap();
number_field.id.clone()
}
pub fn insert_date_cell(&mut self, data: &str) -> String {
let value = serde_json::to_string(&DateCellChangeset {
date: Some(value.to_string()),
date: Some(data.to_string()),
time: None,
})
.unwrap();
let date_field = self.field_rev_with_type(&FieldType::DateTime);
self.inner_builder.add_cell(&date_field.id, value).unwrap();
self
self.inner_builder.insert_cell(&date_field.id, value).unwrap();
date_field.id.clone()
}
#[allow(dead_code)]
pub fn update_checkbox_cell(mut self, data: bool) -> Self {
let number_field = self.field_rev_with_type(&FieldType::Checkbox);
self.inner_builder.add_cell(&number_field.id, data.to_string()).unwrap();
self
pub fn insert_checkbox_cell(&mut self, data: &str) -> String {
let checkbox_field = self.field_rev_with_type(&FieldType::Checkbox);
self.inner_builder
.insert_cell(&checkbox_field.id, data.to_string())
.unwrap();
checkbox_field.id.clone()
}
#[allow(dead_code)]
pub fn update_url_cell(mut self, data: String) -> Self {
let number_field = self.field_rev_with_type(&FieldType::Checkbox);
self.inner_builder.add_cell(&number_field.id, data).unwrap();
self
pub fn insert_url_cell(&mut self, data: &str) -> String {
let url_field = self.field_rev_with_type(&FieldType::URL);
self.inner_builder.insert_cell(&url_field.id, data.to_string()).unwrap();
url_field.id.clone()
}
pub fn insert_single_select_cell<F>(&mut self, f: F) -> String
where
F: Fn(Vec<SelectOption>) -> SelectOption,
{
let single_select_field = self.field_rev_with_type(&FieldType::SingleSelect);
let type_option = SingleSelectTypeOption::from(&single_select_field);
let option = f(type_option.options);
self.inner_builder
.insert_select_option_cell(&single_select_field.id, option.id)
.unwrap();
single_select_field.id.clone()
}
pub fn insert_multi_select_cell<F>(&mut self, f: F) -> String
where
F: Fn(Vec<SelectOption>) -> Vec<SelectOption>,
{
let multi_select_field = self.field_rev_with_type(&FieldType::MultiSelect);
let type_option = MultiSelectTypeOption::from(&multi_select_field);
let options = f(type_option.options);
let ops_ids = options
.iter()
.map(|option| option.id.clone())
.collect::<Vec<_>>()
.join(SELECTION_IDS_SEPARATOR);
self.inner_builder
.insert_select_option_cell(&multi_select_field.id, ops_ids)
.unwrap();
multi_select_field.id.clone()
}
pub fn field_rev_with_type(&self, field_type: &FieldType) -> FieldRevision {
self.test
.field_revs()
self.field_revs
.iter()
.find(|field_rev| {
let t_field_type: FieldType = field_rev.field_type_rev.into();
@ -71,7 +114,21 @@ impl<'a> GridRowTestBuilder<'a> {
.clone()
}
pub fn build(self) -> CreateRowRevisionPayload {
self.inner_builder.build()
pub fn build(self) -> RowRevision {
self.inner_builder.build(&self.block_id)
}
}
impl<'a> std::ops::Deref for GridRowTestBuilder<'a> {
type Target = RowRevisionBuilder<'a>;
fn deref(&self) -> &Self::Target {
&self.inner_builder
}
}
impl<'a> std::ops::DerefMut for GridRowTestBuilder<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner_builder
}
}

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
use crate::grid::filter_test::script::FilterScript::*;
use crate::grid::filter_test::script::*;
use flowy_grid::entities::{CreateGridFilterPayload, TextFilterCondition};
use flowy_grid::entities::{CreateGridFilterPayload, FieldType, TextFilterCondition};
use flowy_grid_data_model::revision::FieldRevision;
#[tokio::test]
async fn grid_filter_create_test() {
let mut test = GridFilterTest::new().await;
let field_rev = test.text_field();
let field_rev = test.get_field_rev(FieldType::RichText);
let payload = CreateGridFilterPayload::new(field_rev, TextFilterCondition::TextIsEmpty, Some("abc".to_owned()));
let scripts = vec![InsertGridTableFilter { payload }, AssertTableFilterCount { count: 1 }];
test.run_scripts(scripts).await;
@ -16,7 +16,7 @@ async fn grid_filter_create_test() {
#[should_panic]
async fn grid_filter_invalid_condition_panic_test() {
let mut test = GridFilterTest::new().await;
let field_rev = test.text_field().clone();
let field_rev = test.get_field_rev(FieldType::RichText).clone();
// 100 is not a valid condition, so this test should be panic.
let payload = CreateGridFilterPayload::new(&field_rev, 100, Some("".to_owned()));
@ -27,7 +27,7 @@ async fn grid_filter_invalid_condition_panic_test() {
#[tokio::test]
async fn grid_filter_delete_test() {
let mut test = GridFilterTest::new().await;
let field_rev = test.text_field().clone();
let field_rev = test.get_field_rev(FieldType::RichText).clone();
let payload = create_filter(&field_rev, TextFilterCondition::TextIsEmpty, "abc");
let scripts = vec![InsertGridTableFilter { payload }, AssertTableFilterCount { count: 1 }];
test.run_scripts(scripts).await;
@ -36,7 +36,7 @@ async fn grid_filter_delete_test() {
test.run_scripts(vec![
DeleteGridTableFilter {
filter_id: filter.id,
field_rev,
field_rev: field_rev.as_ref().clone(),
},
AssertTableFilterCount { count: 0 },
])

View File

@ -1,12 +1,13 @@
#![allow(clippy::all)]
#![allow(dead_code)]
#![allow(unused_imports)]
use crate::grid::block_test::util::GridRowTestBuilder;
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;
use flowy_grid::services::row::{CreateRowRevisionPayload, RowRevisionBuilder};
use flowy_grid::services::setting::GridSettingChangesetBuilder;
use flowy_grid_data_model::revision::*;
use flowy_revision::REVISION_WRITE_INTERVAL_IN_MILLIS;
@ -20,6 +21,7 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use strum::EnumCount;
use strum::IntoEnumIterator;
use tokio::time::sleep;
pub struct GridEditorTest {
@ -37,14 +39,13 @@ impl GridEditorTest {
pub async fn new() -> Self {
let sdk = FlowySDKTest::default();
let _ = sdk.init_user().await;
let build_context = make_all_field_test_grid();
let build_context = make_test_grid();
let view_data: Bytes = build_context.into();
let test = ViewTest::new_grid_view(&sdk, view_data.to_vec()).await;
let editor = sdk.grid_manager.open_grid(&test.view.id).await.unwrap();
let field_revs = editor.get_field_revs(None).await.unwrap();
let block_meta_revs = editor.get_block_meta_revs().await.unwrap();
let row_revs = editor.grid_block_snapshots(None).await.unwrap().pop().unwrap().row_revs;
assert_eq!(row_revs.len(), 3);
assert_eq!(block_meta_revs.len(), 1);
// It seems like you should add the field in the make_test_grid() function.
@ -64,7 +65,7 @@ impl GridEditorTest {
}
}
pub(crate) async fn get_row_revs(&self) -> Vec<Arc<RowRevision>> {
pub async fn get_row_revs(&self) -> Vec<Arc<RowRevision>> {
self.editor
.grid_block_snapshots(None)
.await
@ -79,86 +80,179 @@ impl GridEditorTest {
self.editor.get_grid_filter(&layout_type).await.unwrap()
}
pub fn text_field(&self) -> &FieldRevision {
pub fn get_field_rev(&self, field_type: FieldType) -> &Arc<FieldRevision> {
self.field_revs
.iter()
.filter(|field_rev| {
let t_field_type: FieldType = field_rev.field_type_rev.into();
t_field_type == FieldType::RichText
t_field_type == field_type
})
.collect::<Vec<_>>()
.pop()
.unwrap()
}
pub fn block_id(&self) -> &str {
&self.block_meta_revs.last().unwrap().block_id
}
}
fn make_all_field_test_grid() -> BuildGridContext {
let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
.name("Name")
.visibility(true)
.build();
pub const GOOGLE: &str = "Google";
pub const FACEBOOK: &str = "Facebook";
pub const TWITTER: &str = "Twitter";
// Single Select
let single_select = SingleSelectTypeOptionBuilder::default()
.option(SelectOption::new("Live"))
.option(SelectOption::new("Completed"))
.option(SelectOption::new("Planned"))
.option(SelectOption::new("Paused"));
let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build();
pub const COMPLETED: &str = "Completed";
pub const PLANNED: &str = "Planned";
pub const PAUSED: &str = "Paused";
// MultiSelect
let multi_select = MultiSelectTypeOptionBuilder::default()
.option(SelectOption::new("Google"))
.option(SelectOption::new("Facebook"))
.option(SelectOption::new("Twitter"));
let multi_select_field = FieldBuilder::new(multi_select)
.name("Platform")
.visibility(true)
.build();
// This grid is assumed to contain all the Fields.
fn make_test_grid() -> BuildGridContext {
let mut grid_builder = GridBuilder::new();
// Iterate through the FieldType to create the corresponding Field.
for field_type in FieldType::iter() {
let field_type: FieldType = field_type;
// Number
let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
let number_field = FieldBuilder::new(number).name("Price").visibility(true).build();
// The
match field_type {
FieldType::RichText => {
let text_field = FieldBuilder::new(RichTextTypeOptionBuilder::default())
.name("Name")
.visibility(true)
.build();
grid_builder.add_field(text_field);
}
FieldType::Number => {
// Number
let number = NumberTypeOptionBuilder::default().set_format(NumberFormat::USD);
let number_field = FieldBuilder::new(number).name("Price").visibility(true).build();
grid_builder.add_field(number_field);
}
FieldType::DateTime => {
// Date
let date = DateTypeOptionBuilder::default()
.date_format(DateFormat::US)
.time_format(TimeFormat::TwentyFourHour);
let date_field = FieldBuilder::new(date).name("Time").visibility(true).build();
grid_builder.add_field(date_field);
}
FieldType::SingleSelect => {
// Single Select
let single_select = SingleSelectTypeOptionBuilder::default()
.option(SelectOption::new(COMPLETED))
.option(SelectOption::new(PLANNED))
.option(SelectOption::new(PAUSED));
let single_select_field = FieldBuilder::new(single_select).name("Status").visibility(true).build();
grid_builder.add_field(single_select_field);
}
FieldType::MultiSelect => {
// MultiSelect
let multi_select = MultiSelectTypeOptionBuilder::default()
.option(SelectOption::new(GOOGLE))
.option(SelectOption::new(FACEBOOK))
.option(SelectOption::new(TWITTER));
let multi_select_field = FieldBuilder::new(multi_select)
.name("Platform")
.visibility(true)
.build();
grid_builder.add_field(multi_select_field);
}
FieldType::Checkbox => {
// Checkbox
let checkbox = CheckboxTypeOptionBuilder::default();
let checkbox_field = FieldBuilder::new(checkbox).name("is urgent").visibility(true).build();
grid_builder.add_field(checkbox_field);
}
FieldType::URL => {
// URL
let url = URLTypeOptionBuilder::default();
let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
grid_builder.add_field(url_field);
}
}
}
// Date
let date = DateTypeOptionBuilder::default()
.date_format(DateFormat::US)
.time_format(TimeFormat::TwentyFourHour);
let date_field = FieldBuilder::new(date).name("Time").visibility(true).build();
// We have many assumptions base on the number of the rows, so do not change the number of the loop.
for i in 0..5 {
let block_id = grid_builder.block_id().to_owned();
let field_revs = grid_builder.field_revs();
let mut row_builder = GridRowTestBuilder::new(&block_id, field_revs);
match i {
0 => {
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("A"),
FieldType::Number => row_builder.insert_number_cell("1"),
FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(0))
}
FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
_ => "".to_owned(),
};
}
}
1 => {
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("B"),
FieldType::Number => row_builder.insert_number_cell("2"),
FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(0))
}
FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
_ => "".to_owned(),
};
}
}
2 => {
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("C"),
FieldType::Number => row_builder.insert_number_cell("3"),
FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(1))
}
FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
_ => "".to_owned(),
};
}
}
3 => {
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("D"),
FieldType::Number => row_builder.insert_number_cell("4"),
FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(1))
}
FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
_ => "".to_owned(),
};
}
}
4 => {
for field_type in FieldType::iter() {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("E"),
FieldType::Number => row_builder.insert_number_cell("5"),
FieldType::DateTime => row_builder.insert_date_cell("1647251762"),
FieldType::SingleSelect => {
row_builder.insert_single_select_cell(|mut options| options.remove(2))
}
// Checkbox
let checkbox = CheckboxTypeOptionBuilder::default();
let checkbox_field = FieldBuilder::new(checkbox).name("is done").visibility(true).build();
FieldType::Checkbox => row_builder.insert_checkbox_cell("false"),
_ => "".to_owned(),
};
}
}
_ => {}
}
// URL
let url = URLTypeOptionBuilder::default();
let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
// for i in 0..3 {
// for field_type in FieldType::iter() {
// let field_type: FieldType = field_type;
// match field_type {
// FieldType::RichText => {}
// FieldType::Number => {}
// FieldType::DateTime => {}
// FieldType::SingleSelect => {}
// FieldType::MultiSelect => {}
// FieldType::Checkbox => {}
// FieldType::URL => {}
// }
// }
// }
GridBuilder::default()
.add_field(text_field)
.add_field(single_select_field)
.add_field(multi_select_field)
.add_field(number_field)
.add_field(date_field)
.add_field(checkbox_field)
.add_field(url_field)
.add_empty_row()
.add_empty_row()
.add_empty_row()
.build()
let row_rev = row_builder.build();
grid_builder.add_row(row_rev);
}
grid_builder.build()
}

View File

@ -53,7 +53,7 @@ impl GridRevision {
pub fn from_build_context(grid_id: &str, context: BuildGridContext) -> Self {
Self {
grid_id: grid_id.to_owned(),
fields: context.field_revs.into_iter().map(Arc::new).collect(),
fields: context.field_revs,
blocks: context.blocks.into_iter().map(Arc::new).collect(),
setting: Default::default(),
}
@ -245,7 +245,7 @@ impl CellRevision {
#[derive(Clone, Default, Deserialize, Serialize)]
pub struct BuildGridContext {
pub field_revs: Vec<FieldRevision>,
pub field_revs: Vec<Arc<FieldRevision>>,
pub blocks: Vec<GridBlockMetaRevision>,
pub blocks_meta_data: Vec<GridBlockRevision>,
}

View File

@ -26,18 +26,31 @@ impl std::default::Default for GridBuilder {
}
impl GridBuilder {
pub fn add_field(mut self, field: FieldRevision) -> Self {
self.build_context.field_revs.push(field);
self
pub fn new() -> Self {
Self::default()
}
pub fn add_field(&mut self, field: FieldRevision) {
self.build_context.field_revs.push(Arc::new(field));
}
pub fn add_empty_row(mut self) -> Self {
let row = RowRevision::new(&self.build_context.blocks.first().unwrap().block_id);
pub fn add_row(&mut self, row_rev: RowRevision) {
let block_meta_rev = self.build_context.blocks.first_mut().unwrap();
let block_rev = self.build_context.blocks_meta_data.first_mut().unwrap();
block_rev.rows.push(Arc::new(row));
block_rev.rows.push(Arc::new(row_rev));
block_meta_rev.row_count += 1;
self
}
pub fn add_empty_row(&mut self) {
let row = RowRevision::new(self.block_id());
self.add_row(row);
}
pub fn field_revs(&self) -> &Vec<Arc<FieldRevision>> {
&self.build_context.field_revs
}
pub fn block_id(&self) -> &str {
&self.build_context.blocks.first().unwrap().block_id
}
pub fn build(self) -> BuildGridContext {