mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'main' into refactor/add_pb_suffix
This commit is contained in:
commit
3a0b8bbd74
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -2,7 +2,6 @@
|
||||
proto_input = [
|
||||
"src/event_map.rs",
|
||||
"src/services/field/type_options",
|
||||
"src/services/field/select_option.rs",
|
||||
"src/entities",
|
||||
"src/dart_notification.rs"
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::services::field::select_option::SelectOptionIds;
|
||||
use crate::services::field::SelectOptionIds;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::ErrorCode;
|
||||
use flowy_grid_data_model::revision::GridFilterRevision;
|
||||
|
@ -1,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(¶ms.grid_id)?;
|
||||
let row = OptionalRow {
|
||||
row: editor.get_row(¶ms.row_id).await?,
|
||||
};
|
||||
data_result(row)
|
||||
let row = editor
|
||||
.get_row_rev(¶ms.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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,4 @@
|
||||
mod field_builder;
|
||||
pub mod select_option;
|
||||
pub(crate) mod type_options;
|
||||
|
||||
pub use field_builder::*;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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(), "");
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
@ -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())),
|
||||
}
|
||||
}
|
||||
}
|
@ -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::*;
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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::*;
|
@ -1,17 +1,14 @@
|
||||
mod checkbox_type_option;
|
||||
mod date_type_option;
|
||||
mod multi_select_type_option;
|
||||
mod number_type_option;
|
||||
mod single_select_type_option;
|
||||
mod text_type_option;
|
||||
mod url_type_option;
|
||||
pub mod checkbox_type_option;
|
||||
pub mod date_type_option;
|
||||
pub mod number_type_option;
|
||||
pub mod selection_type_option;
|
||||
pub mod text_type_option;
|
||||
pub mod url_type_option;
|
||||
mod util;
|
||||
|
||||
pub use checkbox_type_option::*;
|
||||
pub use date_type_option::*;
|
||||
pub use multi_select_type_option::*;
|
||||
pub use multi_select_type_option::*;
|
||||
pub use number_type_option::*;
|
||||
pub use single_select_type_option::*;
|
||||
pub use selection_type_option::*;
|
||||
pub use text_type_option::*;
|
||||
pub use url_type_option::*;
|
||||
|
@ -1,6 +1,9 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
mod format;
|
||||
mod number_tests;
|
||||
mod number_type_option;
|
||||
mod number_type_option_entities;
|
||||
|
||||
pub use format::*;
|
||||
pub use number_type_option::*;
|
||||
pub use number_type_option_entities::*;
|
||||
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
mod multi_select_type_option;
|
||||
mod select_option;
|
||||
mod single_select_type_option;
|
||||
|
||||
pub use multi_select_type_option::*;
|
||||
pub use select_option::*;
|
||||
pub use single_select_type_option::*;
|
@ -1,19 +1,15 @@
|
||||
use crate::entities::FieldType;
|
||||
|
||||
use crate::impl_type_option;
|
||||
use crate::services::cell::{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,
|
||||
);
|
@ -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 {
|
@ -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,
|
||||
);
|
@ -0,0 +1,3 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
mod text_type_option;
|
||||
pub use text_type_option::*;
|
@ -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]
|
@ -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()
|
||||
}
|
||||
}
|
@ -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::*;
|
@ -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()
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
mod cell_data_util;
|
||||
|
||||
pub use crate::services::field::select_option::*;
|
||||
pub use cell_data_util::*;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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(¶ms.field_id, cell_bytes.to_vec()))
|
||||
}
|
||||
|
||||
pub async fn get_cell_bytes(&self, params: &CellIdentifier) -> Option<CellBytes> {
|
||||
let field_rev = self.get_field_rev(¶ms.field_id).await?;
|
||||
let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??;
|
||||
|
||||
let cell_rev = row_rev.cells.get(¶ms.field_id)?.clone();
|
||||
let data = decode_any_cell_data(cell_rev.data, &field_rev).data;
|
||||
Some(Cell::new(¶ms.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,
|
||||
})
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<_>>()
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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(×tamp.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;
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use crate::grid::cell_test::script::CellScript::*;
|
||||
use crate::grid::cell_test::script::GridCellTest;
|
||||
use crate::grid::field_test::util::make_date_cell_string;
|
||||
use flowy_grid::entities::{CellChangeset, FieldType};
|
||||
use flowy_grid::services::field::select_option::SelectOptionCellChangeset;
|
||||
use flowy_grid::services::field::selection_type_option::SelectOptionCellChangeset;
|
||||
use flowy_grid::services::field::{MultiSelectTypeOption, SingleSelectTypeOption};
|
||||
|
||||
#[tokio::test]
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::grid::field_test::script::FieldScript::*;
|
||||
use crate::grid::field_test::script::GridFieldTest;
|
||||
use crate::grid::field_test::util::*;
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_grid::services::field::selection_type_option::SelectOption;
|
||||
use flowy_grid::services::field::SingleSelectTypeOption;
|
||||
use flowy_grid_data_model::revision::TypeOptionDataEntry;
|
||||
use flowy_sync::entities::grid::FieldChangesetParams;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use flowy_grid::entities::*;
|
||||
use flowy_grid::services::field::select_option::SelectOption;
|
||||
use flowy_grid::services::field::selection_type_option::SelectOption;
|
||||
use flowy_grid::services::field::*;
|
||||
use flowy_grid_data_model::revision::*;
|
||||
|
||||
|
@ -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 },
|
||||
])
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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>,
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user