feat: timer field (#5349)

* feat: wip timer field

* feat: timer field fixing errors

* feat: wip timer field frontend

* fix: parsing TimerCellDataPB

* feat: parse time string to minutes

* fix: don't allow none number input

* fix: timer filter

* style: cargo fmt

* fix: clippy errors

* refactor: rename field type timer to time

* refactor: missed some variable and files to rename

* style: cargo fmt fix

* feat: format time field type data in frontend

* style: fix cargo fmt

* fix: fixes after merge

---------

Co-authored-by: Mathias Mogensen <mathiasrieckm@gmail.com>
This commit is contained in:
Mohammad Zolfaghari
2024-06-13 10:22:13 +03:30
committed by GitHub
parent 2d4300e931
commit aa621a8d84
57 changed files with 1579 additions and 26 deletions

View File

@ -450,6 +450,7 @@ pub enum FieldType {
Relation = 10,
Summary = 11,
Translate = 12,
Time = 13,
}
impl Display for FieldType {
@ -491,6 +492,7 @@ impl FieldType {
FieldType::Relation => "Relation",
FieldType::Summary => "Summarize",
FieldType::Translate => "Translate",
FieldType::Time => "Time",
};
s.to_string()
}
@ -547,6 +549,10 @@ impl FieldType {
matches!(self, FieldType::Relation)
}
pub fn is_time(&self) -> bool {
matches!(self, FieldType::Time)
}
pub fn can_be_group(&self) -> bool {
self.is_select_option() || self.is_checkbox() || self.is_url()
}

View File

@ -6,6 +6,7 @@ mod number_filter;
mod relation_filter;
mod select_option_filter;
mod text_filter;
mod time_filter;
mod util;
pub use checkbox_filter::*;
@ -16,4 +17,5 @@ pub use number_filter::*;
pub use relation_filter::*;
pub use select_option_filter::*;
pub use text_filter::*;
pub use time_filter::*;
pub use util::*;

View File

@ -0,0 +1,23 @@
use flowy_derive::ProtoBuf;
use crate::entities::NumberFilterConditionPB;
use crate::services::filter::ParseFilterData;
#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
pub struct TimeFilterPB {
#[pb(index = 1)]
pub condition: NumberFilterConditionPB,
#[pb(index = 2)]
pub content: String,
}
impl ParseFilterData for TimeFilterPB {
fn parse(condition: u8, content: String) -> Self {
TimeFilterPB {
condition: NumberFilterConditionPB::try_from(condition)
.unwrap_or(NumberFilterConditionPB::Equal),
content,
}
}
}

View File

@ -10,7 +10,7 @@ use validator::Validate;
use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, RelationFilterPB,
SelectOptionFilterPB, TextFilterPB,
SelectOptionFilterPB, TextFilterPB, TimeFilterPB,
};
use crate::services::filter::{Filter, FilterChangeset, FilterInner};
@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB {
.cloned::<TextFilterPB>()
.unwrap()
.try_into(),
FieldType::Time => condition_and_content
.cloned::<TimeFilterPB>()
.unwrap()
.try_into(),
FieldType::Translate => condition_and_content
.cloned::<TextFilterPB>()
.unwrap()
@ -160,6 +164,9 @@ impl TryFrom<FilterDataPB> for FilterInner {
FieldType::Summary => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Time => {
BoxAny::new(TimeFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},
FieldType::Translate => {
BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?)
},

View File

@ -17,6 +17,7 @@ macro_rules! impl_into_field_type {
10 => FieldType::Relation,
11 => FieldType::Summary,
12 => FieldType::Translate,
13 => FieldType::Time,
_ => {
tracing::error!("🔴Can't parse FieldType from value: {}", ty);
FieldType::RichText

View File

@ -6,6 +6,7 @@ mod relation_entities;
mod select_option_entities;
mod summary_entities;
mod text_entities;
mod time_entities;
mod timestamp_entities;
mod translate_entities;
mod url_entities;
@ -18,6 +19,7 @@ pub use relation_entities::*;
pub use select_option_entities::*;
pub use summary_entities::*;
pub use text_entities::*;
pub use time_entities::*;
pub use timestamp_entities::*;
pub use translate_entities::*;
pub use url_entities::*;

View File

@ -0,0 +1,28 @@
use crate::services::field::TimeTypeOption;
use flowy_derive::ProtoBuf;
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct TimeTypeOptionPB {
#[pb(index = 1)]
pub dummy: String,
}
impl From<TimeTypeOption> for TimeTypeOptionPB {
fn from(_data: TimeTypeOption) -> Self {
Self {
dummy: "".to_string(),
}
}
}
impl From<TimeTypeOptionPB> for TimeTypeOption {
fn from(_data: TimeTypeOptionPB) -> Self {
Self
}
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct TimeCellDataPB {
#[pb(index = 2)]
pub time: i64,
}

View File

@ -222,7 +222,7 @@ impl<'a> CellBuilder<'a> {
FieldType::RichText => {
cells.insert(field_id, insert_text_cell(cell_str, field));
},
FieldType::Number => {
FieldType::Number | FieldType::Time => {
if let Ok(num) = cell_str.parse::<i64>() {
cells.insert(field_id, insert_number_cell(num, field));
}

View File

@ -6,6 +6,7 @@ pub mod relation_type_option;
pub mod selection_type_option;
pub mod summary_type_option;
pub mod text_type_option;
pub mod time_type_option;
pub mod timestamp_type_option;
pub mod translate_type_option;
mod type_option;
@ -20,6 +21,7 @@ pub use number_type_option::*;
pub use relation_type_option::*;
pub use selection_type_option::*;
pub use text_type_option::*;
pub use time_type_option::*;
pub use timestamp_type_option::*;
pub use type_option::*;
pub use type_option_cell::*;

View File

@ -79,13 +79,14 @@ impl CellDataDecoder for RichTextTypeOption {
| FieldType::SingleSelect
| FieldType::MultiSelect
| FieldType::Checkbox
| FieldType::URL => Some(StringCellData::from(stringify_cell(cell, field))),
| FieldType::URL
| FieldType::Summary
| FieldType::Translate
| FieldType::Time => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Checklist
| FieldType::LastEditedTime
| FieldType::CreatedTime
| FieldType::Relation => None,
FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))),
FieldType::Translate => Some(StringCellData::from(stringify_cell(cell, field))),
}
}

View File

@ -0,0 +1,6 @@
mod time;
mod time_entities;
mod time_filter;
pub use time::*;
pub use time_entities::*;

View File

@ -0,0 +1,115 @@
use crate::entities::{TimeCellDataPB, TimeFilterPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
TimeCellData, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform,
};
use crate::services::sort::SortCondition;
use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder};
use collab_database::rows::Cell;
use flowy_error::FlowyResult;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct TimeTypeOption;
impl TypeOption for TimeTypeOption {
type CellData = TimeCellData;
type CellChangeset = TimeCellChangeset;
type CellProtobufType = TimeCellDataPB;
type CellFilter = TimeFilterPB;
}
impl From<TypeOptionData> for TimeTypeOption {
fn from(_data: TypeOptionData) -> Self {
Self
}
}
impl From<TimeTypeOption> for TypeOptionData {
fn from(_data: TimeTypeOption) -> Self {
TypeOptionDataBuilder::new().build()
}
}
impl TypeOptionCellDataSerde for TimeTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
if let Some(time) = cell_data.0 {
return TimeCellDataPB { time };
}
TimeCellDataPB {
time: i64::default(),
}
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(TimeCellData::from(cell))
}
}
impl TimeTypeOption {
pub fn new() -> Self {
Self
}
}
impl TypeOptionTransform for TimeTypeOption {}
impl CellDataDecoder for TimeTypeOption {
fn decode_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
self.parse_cell(cell)
}
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
if let Some(time) = cell_data.0 {
return time.to_string();
}
"".to_string()
}
fn numeric_cell(&self, cell: &Cell) -> Option<f64> {
let time_cell_data = self.parse_cell(cell).ok()?;
Some(time_cell_data.0.unwrap() as f64)
}
}
pub type TimeCellChangeset = String;
impl CellDataChangeset for TimeTypeOption {
fn apply_changeset(
&self,
changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
let str = changeset.trim().to_string();
let cell_data = TimeCellData(str.parse::<i64>().ok());
Ok((Cell::from(&cell_data), cell_data))
}
}
impl TypeOptionCellDataFilter for TimeTypeOption {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
filter.is_visible(cell_data.0)
}
}
impl TypeOptionCellDataCompare for TimeTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
let order = cell_data.0.cmp(&other_cell_data.0);
sort_condition.evaluate_order(order)
}
}

View File

@ -0,0 +1,47 @@
use crate::entities::FieldType;
use crate::services::field::{TypeOptionCellData, CELL_DATA};
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
#[derive(Clone, Debug, Default)]
pub struct TimeCellData(pub Option<i64>);
impl TypeOptionCellData for TimeCellData {
fn is_cell_empty(&self) -> bool {
self.0.is_none()
}
}
impl From<&Cell> for TimeCellData {
fn from(cell: &Cell) -> Self {
Self(
cell
.get_str_value(CELL_DATA)
.and_then(|data| data.parse::<i64>().ok()),
)
}
}
impl std::convert::From<String> for TimeCellData {
fn from(s: String) -> Self {
Self(s.trim().to_string().parse::<i64>().ok())
}
}
impl ToString for TimeCellData {
fn to_string(&self) -> String {
if let Some(time) = self.0 {
time.to_string()
} else {
"".to_string()
}
}
}
impl From<&TimeCellData> for Cell {
fn from(data: &TimeCellData) -> Self {
new_cell_builder(FieldType::Time)
.insert_str_value(CELL_DATA, data.to_string())
.build()
}
}

View File

@ -0,0 +1,72 @@
use collab_database::fields::Field;
use collab_database::rows::Cell;
use crate::entities::{NumberFilterConditionPB, TimeFilterPB};
use crate::services::cell::insert_text_cell;
use crate::services::filter::PreFillCellsWithFilter;
impl TimeFilterPB {
pub fn is_visible(&self, cell_time: Option<i64>) -> bool {
if self.content.is_empty() {
match self.condition {
NumberFilterConditionPB::NumberIsEmpty => {
return cell_time.is_none();
},
NumberFilterConditionPB::NumberIsNotEmpty => {
return cell_time.is_some();
},
_ => {},
}
}
if cell_time.is_none() {
return false;
}
let time = cell_time.unwrap();
let content_time = self.content.parse::<i64>().unwrap_or_default();
match self.condition {
NumberFilterConditionPB::Equal => time == content_time,
NumberFilterConditionPB::NotEqual => time != content_time,
NumberFilterConditionPB::GreaterThan => time > content_time,
NumberFilterConditionPB::LessThan => time < content_time,
NumberFilterConditionPB::GreaterThanOrEqualTo => time >= content_time,
NumberFilterConditionPB::LessThanOrEqualTo => time <= content_time,
_ => true,
}
}
}
impl PreFillCellsWithFilter for TimeFilterPB {
fn get_compliant_cell(&self, field: &Field) -> (Option<Cell>, bool) {
let expected_decimal = || self.content.parse::<i64>().ok();
let text = match self.condition {
NumberFilterConditionPB::Equal
| NumberFilterConditionPB::GreaterThanOrEqualTo
| NumberFilterConditionPB::LessThanOrEqualTo
if !self.content.is_empty() =>
{
Some(self.content.clone())
},
NumberFilterConditionPB::GreaterThan if !self.content.is_empty() => {
expected_decimal().map(|value| {
let answer = value + 1;
answer.to_string()
})
},
NumberFilterConditionPB::LessThan if !self.content.is_empty() => {
expected_decimal().map(|value| {
let answer = value - 1;
answer.to_string()
})
},
_ => None,
};
let open_after_create = matches!(self.condition, NumberFilterConditionPB::NumberIsNotEmpty);
// use `insert_text_cell` because self.content might not be a parsable i64.
(text.map(|s| insert_text_cell(s, field)), open_after_create)
}
}

View File

@ -11,7 +11,7 @@ use flowy_error::FlowyResult;
use crate::entities::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB,
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB,
SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimeTypeOptionPB, TimestampTypeOptionPB,
TranslateTypeOptionPB, URLTypeOptionPB,
};
use crate::services::cell::CellDataDecoder;
@ -20,7 +20,7 @@ use crate::services::field::summary_type_option::summary::SummarizationTypeOptio
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption, TimestampTypeOption, URLTypeOption,
};
use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter};
use crate::services::sort::SortCondition;
@ -187,6 +187,7 @@ pub fn type_option_data_from_pb<T: Into<Bytes>>(
FieldType::Summary => {
SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into())
},
FieldType::Time => TimeTypeOptionPB::try_from(bytes).map(|pb| TimeTypeOption::from(pb).into()),
FieldType::Translate => {
TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into())
},
@ -257,6 +258,10 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
.try_into()
.unwrap()
},
FieldType::Time => {
let time_type_option: TimeTypeOption = type_option.into();
TimeTypeOptionPB::from(time_type_option).try_into().unwrap()
},
FieldType::Translate => {
let translate_type_option: TranslateTypeOption = type_option.into();
TranslateTypeOptionPB::from(translate_type_option)
@ -284,5 +289,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa
FieldType::Relation => RelationTypeOption::default().into(),
FieldType::Summary => SummarizationTypeOption::default().into(),
FieldType::Translate => TranslateTypeOption::default().into(),
FieldType::Time => TimeTypeOption.into(),
}
}

View File

@ -14,9 +14,9 @@ use crate::services::field::summary_type_option::summary::SummarizationTypeOptio
use crate::services::field::translate_type_option::translate::TranslateTypeOption;
use crate::services::field::{
CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption,
TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
TypeOptionTransform, URLTypeOption,
RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption,
TimestampTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
};
use crate::services::sort::SortCondition;
@ -450,6 +450,16 @@ impl<'a> TypeOptionCellExt<'a> {
self.cell_data_cache.clone(),
)
}),
FieldType::Time => self
.field
.get_type_option::<TimeTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
field_type,
self.cell_data_cache.clone(),
)
}),
FieldType::Translate => self
.field
.get_type_option::<TranslateTypeOption>(field_type)
@ -563,6 +573,9 @@ fn get_type_option_transform_handler(
},
FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data))
as Box<dyn TypeOptionTransformHandler>,
FieldType::Time => {
Box::new(TimeTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::Translate => {
Box::new(TranslateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},

View File

@ -303,6 +303,10 @@ impl FilterController {
let filter = condition_and_content.cloned::<ChecklistFilterPB>().unwrap();
filter.get_compliant_cell(field)
},
FieldType::Time => {
let filter = condition_and_content.cloned::<TimeFilterPB>().unwrap();
filter.get_compliant_cell(field)
},
_ => (None, false),
};

View File

@ -12,6 +12,7 @@ use lib_infra::box_any::BoxAny;
use crate::entities::{
CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType,
InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB,
TimeFilterPB,
};
use crate::services::field::SelectOptionIds;
@ -282,6 +283,7 @@ impl FilterInner {
FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)),
FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)),
FieldType::Time => BoxAny::new(TimeFilterPB::parse(condition as u8, content)),
};
FilterInner::Data {
@ -368,6 +370,10 @@ impl<'a> From<&'a Filter> for FilterMap {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)
},
FieldType::Time => {
let filter = condition_and_content.cloned::<TimeFilterPB>()?;
(filter.condition as u8, filter.content)
},
FieldType::Translate => {
let filter = condition_and_content.cloned::<TextFilterPB>()?;
(filter.condition as u8, filter.content)

View File

@ -4,7 +4,7 @@ use flowy_database2::entities::FieldType;
use flowy_database2::services::field::{
ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption,
RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StringCellData,
URLCellData,
TimeCellData, URLCellData,
};
use lib_infra::box_any::BoxAny;
@ -200,3 +200,20 @@ async fn update_updated_at_field_on_other_cell_update() {
}
}
}
#[tokio::test]
async fn time_cell_data_test() {
let test = DatabaseCellTest::new().await;
let time_field = test.get_first_field(FieldType::Time);
let cells = test
.editor
.get_cells_for_field(&test.view_id, &time_field.id)
.await;
if let Some(cell) = cells[0].cell.as_ref() {
let cell = TimeCellData::from(cell);
assert!(cell.0.is_some());
assert_eq!(cell.0.unwrap_or_default(), 75);
}
}

View File

@ -40,6 +40,26 @@ async fn grid_create_field() {
},
];
test.run_scripts(scripts).await;
let (params, field) = create_time_field(&test.view_id());
let scripts = vec![
CreateField { params },
AssertFieldTypeOptionEqual {
field_index: test.field_count(),
expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(),
},
];
test.run_scripts(scripts).await;
let (params, field) = create_time_field(&test.view_id());
let scripts = vec![
CreateField { params },
AssertFieldTypeOptionEqual {
field_index: test.field_count(),
expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(),
},
];
test.run_scripts(scripts).await;
}
#[tokio::test]

View File

@ -4,7 +4,7 @@ use collab_database::views::OrderObjectPosition;
use flowy_database2::entities::{CreateFieldParams, FieldType};
use flowy_database2::services::field::{
type_option_to_pb, DateFormat, DateTypeOption, FieldBuilder, RichTextTypeOption, SelectOption,
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption,
};
pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) {
@ -98,3 +98,21 @@ pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFi
};
(params, field)
}
pub fn create_time_field(grid_id: &str) -> (CreateFieldParams, Field) {
let field_type = FieldType::Time;
let type_option = TimeTypeOption;
let text_field = FieldBuilder::new(field_type, type_option.clone())
.name("Time field")
.build();
let type_option_data = type_option_to_pb(type_option.into(), &field_type).to_vec();
let params = CreateFieldParams {
view_id: grid_id.to_owned(),
field_type,
type_option_data: Some(type_option_data),
field_name: None,
position: OrderObjectPosition::default(),
};
(params, text_field)
}

View File

@ -6,3 +6,4 @@ mod number_filter_test;
mod script;
mod select_option_filter_test;
mod text_filter_test;
mod time_filter_test;

View File

@ -0,0 +1,121 @@
use flowy_database2::entities::{FieldType, NumberFilterConditionPB, TimeFilterPB};
use lib_infra::box_any::BoxAny;
use crate::database::filter_test::script::FilterScript::*;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged};
#[tokio::test]
async fn grid_filter_time_is_equal_test() {
let mut test = DatabaseFilterTest::new().await;
let row_count = test.row_details.len();
let expected = 1;
let scripts = vec![
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Time,
data: BoxAny::new(TimeFilterPB {
condition: NumberFilterConditionPB::Equal,
content: "75".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
}),
},
AssertNumberOfVisibleRows { expected },
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_filter_time_is_less_than_test() {
let mut test = DatabaseFilterTest::new().await;
let row_count = test.row_details.len();
let expected = 1;
let scripts = vec![
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Time,
data: BoxAny::new(TimeFilterPB {
condition: NumberFilterConditionPB::LessThan,
content: "80".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
}),
},
AssertNumberOfVisibleRows { expected },
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_filter_time_is_less_than_or_equal_test() {
let mut test = DatabaseFilterTest::new().await;
let row_count = test.row_details.len();
let expected = 1;
let scripts = vec![
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Time,
data: BoxAny::new(TimeFilterPB {
condition: NumberFilterConditionPB::LessThanOrEqualTo,
content: "75".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
}),
},
AssertNumberOfVisibleRows { expected },
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_filter_time_is_empty_test() {
let mut test = DatabaseFilterTest::new().await;
let row_count = test.row_details.len();
let expected = 6;
let scripts = vec![
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Time,
data: BoxAny::new(TimeFilterPB {
condition: NumberFilterConditionPB::NumberIsEmpty,
content: "".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
}),
},
AssertNumberOfVisibleRows { expected },
];
test.run_scripts(scripts).await;
}
#[tokio::test]
async fn grid_filter_time_is_not_empty_test() {
let mut test = DatabaseFilterTest::new().await;
let row_count = test.row_details.len();
let expected = 1;
let scripts = vec![
CreateDataFilter {
parent_filter_id: None,
field_type: FieldType::Time,
data: BoxAny::new(TimeFilterPB {
condition: NumberFilterConditionPB::NumberIsNotEmpty,
content: "".to_string(),
}),
changed: Some(FilterRowChanged {
showing_num_of_rows: 0,
hiding_num_of_rows: row_count - expected,
}),
},
AssertNumberOfVisibleRows { expected },
];
test.run_scripts(scripts).await;
}

View File

@ -134,6 +134,12 @@ pub fn make_test_board() -> DatabaseData {
.build();
fields.push(relation_field);
},
FieldType::Time => {
let time_field = FieldBuilder::from_field_type(field_type)
.name("Estimated time")
.build();
fields.push(time_field);
},
FieldType::Translate => {},
}
}

View File

@ -10,7 +10,7 @@ use flowy_database2::services::field::translate_type_option::translate::Translat
use flowy_database2::services::field::{
ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption,
NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor,
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption,
};
use flowy_database2::services::field_settings::default_field_settings_for_fields;
@ -133,6 +133,13 @@ pub fn make_test_grid() -> DatabaseData {
.build();
fields.push(relation_field);
},
FieldType::Time => {
let type_option = TimeTypeOption;
let time_field = FieldBuilder::new(field_type, type_option)
.name("Estimated time")
.build();
fields.push(time_field);
},
FieldType::Translate => {
let type_option = TranslateTypeOption {
auto_fill: false,
@ -168,6 +175,7 @@ pub fn make_test_grid() -> DatabaseData {
FieldType::Checklist => {
row_builder.insert_checklist_cell(vec![("First thing".to_string(), false)])
},
FieldType::Time => row_builder.insert_time_cell(75),
_ => "".to_owned(),
};
}

View File

@ -83,6 +83,7 @@ async fn export_and_then_import_meta_csv_test() {
FieldType::CreatedTime => {},
FieldType::Relation => {},
FieldType::Summary => {},
FieldType::Time => {},
FieldType::Translate => {},
}
} else {
@ -167,6 +168,7 @@ async fn history_database_import_test() {
FieldType::CreatedTime => {},
FieldType::Relation => {},
FieldType::Summary => {},
FieldType::Time => {},
FieldType::Translate => {},
}
} else {