diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_info.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_info.dart index 54d79a24f8..380a6ff2d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/field_info.dart @@ -32,6 +32,7 @@ class FieldInfo with _$FieldInfo { case FieldType.Checkbox: case FieldType.MultiSelect: case FieldType.SingleSelect: + case FieldType.DateTime: return true; default: return false; diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs index 879afea6a1..a27e67ff1a 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs @@ -2,7 +2,7 @@ use crate::services::group::Group; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct UrlGroupConfigurationPB { +pub struct URLGroupConfigurationPB { #[pb(index = 1)] hide_empty: bool, } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs index 94d145839c..2ce02bdbc5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -1,3 +1,15 @@ +use std::cmp::Ordering; +use std::str::FromStr; + +use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone}; +use chrono_tz::Tz; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use serde::{Deserialize, Serialize}; + +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; + use crate::entities::{DateCellDataPB, DateFilterPB, FieldType}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::field::{ @@ -6,16 +18,6 @@ use crate::services::field::{ TypeOptionTransform, }; use crate::services::sort::SortCondition; -use chrono::format::strftime::StrftimeItems; -use chrono::{DateTime, FixedOffset, Local, NaiveDateTime, NaiveTime, Offset, TimeZone}; -use chrono_tz::Tz; -use collab::core::any_map::AnyMapExtension; -use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; -use collab_database::rows::Cell; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; -use std::str::FromStr; /// The [DateTypeOption] is used by [FieldType::Date], [FieldType::LastEditedTime], and [FieldType::CreatedTime]. /// So, storing the field type is necessary to distinguish the field type. @@ -121,9 +123,9 @@ impl DateTypeOption { let date_time = DateTime::::from_utc(naive, offset); let fmt = self.date_format.format_str(); - let date = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt))); + let date = format!("{}", date_time.format(fmt)); let fmt = self.time_format.format_str(); - let time = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt))); + let time = format!("{}", date_time.format(fmt)); (date, time) }, diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs index 1ca75b5457..27738949e0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -1,12 +1,11 @@ #![allow(clippy::upper_case_acronyms)] -use std::fmt; - use bytes::Bytes; use collab::core::any_map::AnyMapExtension; use collab_database::rows::{new_cell_builder, Cell}; use serde::de::Visitor; use serde::{Deserialize, Serialize}; +use std::fmt; use strum_macros::EnumIter; use flowy_error::{internal_error, FlowyResult}; @@ -81,6 +80,15 @@ impl From<&Cell> for DateCellData { } } +impl From<&DateCellDataPB> for DateCellData { + fn from(data: &DateCellDataPB) -> Self { + Self { + timestamp: Some(data.timestamp), + include_time: data.include_time, + } + } +} + /// Wrapper for DateCellData that also contains the field type. /// Handy struct to use when you need to convert a DateCellData to a Cell. pub struct DateCellDataWrapper { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index a2a86f05b8..2fefedd37d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -379,6 +379,10 @@ where .await } + pub fn get_setting_content(&self) -> String { + self.setting.content.clone() + } + /// # Arguments /// /// * `mut_configuration_fn`: mutate the [GroupSetting] and return whether the [GroupSetting] is diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs new file mode 100644 index 0000000000..c7eba56add --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -0,0 +1,598 @@ +use crate::entities::{ + DateCellDataPB, FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, + RowMetaPB, +}; +use crate::services::cell::insert_date_cell; +use crate::services::field::{DateCellData, DateCellDataParser, DateTypeOption}; +use crate::services::group::action::GroupCustomize; +use crate::services::group::configuration::GroupContext; +use crate::services::group::controller::{ + BaseGroupController, GroupController, GroupsBuilder, MoveGroupRowContext, +}; +use crate::services::group::{ + make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, +}; +use chrono::{ + DateTime, Datelike, Days, Duration, Local, NaiveDate, NaiveDateTime, Offset, TimeZone, +}; +use chrono_tz::Tz; +use collab_database::database::timestamp; +use collab_database::fields::Field; +use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; +use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::format; +use std::str::FromStr; +use std::sync::Arc; + +pub trait GroupConfigurationContentSerde: Sized + Send + Sync { + fn from_json(s: &str) -> Result; + fn to_json(&self) -> Result; +} + +#[derive(Default, Serialize, Deserialize)] +pub struct DateGroupConfiguration { + pub hide_empty: bool, + pub condition: DateCondition, +} + +impl GroupConfigurationContentSerde for DateGroupConfiguration { + fn from_json(s: &str) -> Result { + serde_json::from_str(s) + } + fn to_json(&self) -> Result { + serde_json::to_string(self) + } +} + +#[derive(Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum DateCondition { + Relative = 0, + Day = 1, + Week = 2, + Month = 3, + Year = 4, +} + +impl std::default::Default for DateCondition { + fn default() -> Self { + DateCondition::Relative + } +} + +pub type DateGroupController = BaseGroupController< + DateGroupConfiguration, + DateTypeOption, + DateGroupGenerator, + DateCellDataParser, +>; + +pub type DateGroupContext = GroupContext; + +impl GroupCustomize for DateGroupController { + type CellData = DateCellDataPB; + + fn placeholder_cell(&self) -> Option { + Some( + new_cell_builder(FieldType::DateTime) + .insert_str_value("data", "") + .build(), + ) + } + + fn can_group(&self, content: &str, cell_data: &Self::CellData) -> bool { + content + == group_id( + &cell_data.into(), + self.type_option.as_ref(), + &self.context.get_setting_content(), + ) + } + + fn create_or_delete_group_when_cell_changed( + &mut self, + row_detail: &RowDetail, + old_cell_data: Option<&Self::CellData>, + cell_data: &Self::CellData, + ) -> FlowyResult<(Option, Option)> { + let setting_content = self.context.get_setting_content(); + let mut inserted_group = None; + if self + .context + .get_group(&group_id( + &cell_data.into(), + self.type_option.as_ref(), + &setting_content, + )) + .is_none() + { + let group = make_group_from_date_cell( + &cell_data.into(), + self.type_option.as_ref(), + &setting_content, + ); + let mut new_group = self.context.add_new_group(group)?; + new_group.group.rows.push(RowMetaPB::from(&row_detail.meta)); + inserted_group = Some(new_group); + } + + // Delete the old group if there are no rows in that group + let deleted_group = match old_cell_data.and_then(|old_cell_data| { + self.context.get_group(&group_id( + &old_cell_data.into(), + self.type_option.as_ref(), + &setting_content, + )) + }) { + None => None, + Some((_, group)) => { + if group.rows.len() == 1 { + Some(group.clone()) + } else { + None + } + }, + }; + + let deleted_group = match deleted_group { + None => None, + Some(group) => { + self.context.delete_group(&group.id)?; + Some(GroupPB::from(group.clone())) + }, + }; + + Ok((inserted_group, deleted_group)) + } + + fn add_or_remove_row_when_cell_changed( + &mut self, + row_detail: &RowDetail, + cell_data: &Self::CellData, + ) -> Vec { + let mut changesets = vec![]; + let setting_content = self.context.get_setting_content(); + self.context.iter_mut_status_groups(|group| { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + if group.id + == group_id( + &cell_data.into(), + self.type_option.as_ref(), + &setting_content, + ) + { + if !group.contains_row(&row_detail.row.id) { + changeset + .inserted_rows + .push(InsertedRowPB::new(RowMetaPB::from(&row_detail.meta))); + group.add_row(row_detail.clone()); + } + } else if group.contains_row(&row_detail.row.id) { + group.remove_row(&row_detail.row.id); + changeset + .deleted_rows + .push(row_detail.row.id.clone().into_inner()); + } + + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets + } + + fn delete_row(&mut self, row: &Row, _cell_data: &Self::CellData) -> Vec { + let mut changesets = vec![]; + self.context.iter_mut_groups(|group| { + let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); + if group.contains_row(&row.id) { + group.remove_row(&row.id); + changeset.deleted_rows.push(row.id.clone().into_inner()); + } + + if !changeset.is_empty() { + changesets.push(changeset); + } + }); + changesets + } + + fn move_row( + &mut self, + _cell_data: &Self::CellData, + mut context: MoveGroupRowContext, + ) -> Vec { + let mut group_changeset = vec![]; + self.context.iter_mut_groups(|group| { + if let Some(changeset) = move_group_row(group, &mut context) { + group_changeset.push(changeset); + } + }); + group_changeset + } + + fn delete_group_when_move_row( + &mut self, + _row: &Row, + cell_data: &Self::CellData, + ) -> Option { + let mut deleted_group = None; + let setting_content = self.context.get_setting_content(); + if let Some((_, group)) = self.context.get_group(&group_id( + &cell_data.into(), + self.type_option.as_ref(), + &setting_content, + )) { + if group.rows.len() == 1 { + deleted_group = Some(GroupPB::from(group.clone())); + } + } + if deleted_group.is_some() { + let _ = self + .context + .delete_group(&deleted_group.as_ref().unwrap().group_id); + } + deleted_group + } +} + +impl GroupController for DateGroupController { + fn did_update_field_type_option(&mut self, _field: &Arc) {} + + fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + match self.context.get_group(group_id) { + None => tracing::warn!("Can not find the group: {}", group_id), + Some((_, _)) => { + let date = DateTime::parse_from_str(&group_id, GROUP_ID_DATE_FORMAT).unwrap(); + let cell = insert_date_cell(date.timestamp(), None, field); + cells.insert(field.id.clone(), cell); + }, + } + } + + fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { + if let Some(group) = self.context.get_mut_group(group_id) { + group.add_row(row_detail.clone()) + } + } +} + +pub struct DateGroupGenerator(); +impl GroupsBuilder for DateGroupGenerator { + type Context = DateGroupContext; + type TypeOptionType = DateTypeOption; + + fn build( + field: &Field, + context: &Self::Context, + type_option: &Option, + ) -> GeneratedGroups { + // Read all the cells for the grouping field + let cells = futures::executor::block_on(context.get_all_cells()); + + // Generate the groups + let mut group_configs: Vec = cells + .into_iter() + .flat_map(|value| value.into_date_field_cell_data()) + .filter(|cell| cell.timestamp.is_some()) + .map(|cell| { + let group = + make_group_from_date_cell(&cell, type_option.as_ref(), &context.get_setting_content()); + GeneratedGroupConfig { + filter_content: group.id.clone(), + group, + } + }) + .collect(); + group_configs.sort_by(|a, b| a.filter_content.cmp(&b.filter_content)); + + let no_status_group = Some(make_no_status_group(field)); + GeneratedGroups { + no_status_group, + group_configs, + } + } +} + +fn make_group_from_date_cell( + cell_data: &DateCellData, + type_option: Option<&DateTypeOption>, + setting_content: &String, +) -> Group { + let group_id = group_id(cell_data, type_option, setting_content); + Group::new( + group_id.clone(), + group_name_from_id(&group_id, type_option, setting_content), + ) +} + +const GROUP_ID_DATE_FORMAT: &'static str = "%Y/%m/%d"; + +fn group_id( + cell_data: &DateCellData, + type_option: Option<&DateTypeOption>, + setting_content: &String, +) -> String { + let binding = DateTypeOption::default(); + let type_option = type_option.unwrap_or(&binding); + let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default(); + let date_time = date_time_from_timestamp(cell_data.timestamp, &type_option.timezone_id); + + let date_format = GROUP_ID_DATE_FORMAT; + let month_format = &date_format.replace("%d", "01"); + let year_format = &month_format.replace("%m", "01"); + + let date = match config.condition { + DateCondition::Day => date_time.format(date_format), + DateCondition::Month => date_time.format(month_format), + DateCondition::Year => date_time.format(year_format), + DateCondition::Week => date_time + .checked_sub_days(Days::new(date_time.weekday().num_days_from_monday() as u64)) + .unwrap() + .format(date_format), + DateCondition::Relative => { + let now = date_time_from_timestamp(Some(timestamp()), &type_option.timezone_id).date_naive(); + let date_time = date_time.date_naive(); + + let diff = date_time.signed_duration_since(now).num_days(); + let result = if diff == 0 { + Some(now) + } else if diff == -1 { + now.checked_add_signed(Duration::days(-1)) + } else if diff == 1 { + now.checked_add_signed(Duration::days(1)) + } else if diff >= -7 && diff < -1 { + now.checked_add_signed(Duration::days(-7)) + } else if diff > 1 && diff <= 7 { + now.checked_add_signed(Duration::days(2)) + } else if diff >= -30 && diff < -7 { + now.checked_add_signed(Duration::days(-30)) + } else if diff > 7 && diff <= 30 { + now.checked_add_signed(Duration::days(8)) + } else { + let mut res = date_time + .checked_sub_days(Days::new(date_time.day() as u64 - 1)) + .unwrap(); + // if beginning of the month is within next 30 days of current day, change to + // first day which is greater than 30 days far from current day. + let diff = res.signed_duration_since(now).num_days(); + if diff > 7 && diff <= 30 { + res = res + .checked_add_days(Days::new((30 - diff + 1) as u64)) + .unwrap(); + } + Some(res) + }; + + result.unwrap().format(GROUP_ID_DATE_FORMAT) + }, + }; + + date.to_string() +} + +fn group_name_from_id( + group_id: &String, + type_option: Option<&DateTypeOption>, + setting_content: &String, +) -> String { + let binding = DateTypeOption::default(); + let type_option = type_option.unwrap_or(&binding); + let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default(); + let date = NaiveDate::parse_from_str(group_id, GROUP_ID_DATE_FORMAT).unwrap(); + + let tmp; + match config.condition { + DateCondition::Day => { + tmp = format!( + "{} {}, {}", + date.format("%b").to_string(), + date.day(), + date.year(), + ); + tmp + }, + DateCondition::Week => { + let begin_of_week = date + .checked_sub_days(Days::new(date.weekday().num_days_from_monday() as u64)) + .unwrap() + .format("%d"); + let end_of_week = date + .checked_add_days(Days::new(6 - date.weekday().num_days_from_monday() as u64)) + .unwrap() + .format("%d"); + + tmp = format!( + "Week of {} {}-{} {}", + date.format("%b").to_string(), + begin_of_week.to_string(), + end_of_week.to_string(), + date.year() + ); + tmp + }, + DateCondition::Month => { + tmp = format!("{} {}", date.format("%b").to_string(), date.year(),); + tmp + }, + DateCondition::Year => date.year().to_string(), + DateCondition::Relative => { + let now = date_time_from_timestamp(Some(timestamp()), &type_option.timezone_id); + + let diff = date.signed_duration_since(now.date_naive()); + let result = match diff.num_days() { + 0 => "Today", + -1 => "Yesterday", + 1 => "Tomorrow", + -7 => "Last 7 days", + 2 => "Next 7 days", + -30 => "Last 30 days", + 8 => "Next 30 days", + _ => { + tmp = format!("{} {}", date.format("%b").to_string(), date.year(),); + &tmp + }, + }; + + result.to_string() + }, + } +} + +fn date_time_from_timestamp(timestamp: Option, timezone_id: &String) -> DateTime { + match timestamp { + Some(timestamp) => { + let naive = NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(); + let offset = match Tz::from_str(timezone_id) { + Ok(timezone) => timezone.offset_from_utc_datetime(&naive).fix(), + Err(_) => *Local::now().offset(), + }; + + DateTime::::from_utc(naive, offset) + }, + None => DateTime::default(), + } +} + +#[cfg(test)] +mod tests { + use crate::services::{ + field::{date_type_option::DateTypeOption, DateCellData}, + group::controller_impls::date_controller::{ + group_id, group_name_from_id, GROUP_ID_DATE_FORMAT, + }, + }; + use chrono::{offset, Days, Duration, NaiveDateTime}; + use std::vec; + + #[test] + fn group_id_name_test() { + struct GroupIDTest<'a> { + cell_data: DateCellData, + setting_content: String, + exp_group_id: String, + exp_group_name: String, + type_option: &'a DateTypeOption, + } + + let mar_14_2022 = NaiveDateTime::from_timestamp_opt(1647251762, 0).unwrap(); + let mar_14_2022_cd = DateCellData { + timestamp: Some(mar_14_2022.timestamp()), + include_time: false, + }; + let today = offset::Local::now(); + let three_days_before = today.checked_add_signed(Duration::days(-3)).unwrap(); + + let mut local_date_type_option = DateTypeOption::default(); + local_date_type_option.timezone_id = today.offset().to_string(); + let mut default_date_type_option = DateTypeOption::default(); + default_date_type_option.timezone_id = "".to_string(); + + let tests = vec![ + GroupIDTest { + cell_data: mar_14_2022_cd.clone(), + type_option: &local_date_type_option, + setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(), + exp_group_id: "2022/03/01".to_string(), + exp_group_name: "Mar 2022".to_string(), + }, + GroupIDTest { + cell_data: DateCellData { + timestamp: Some(today.timestamp()), + include_time: false, + }, + type_option: &local_date_type_option, + setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(), + exp_group_id: today.format(GROUP_ID_DATE_FORMAT).to_string(), + exp_group_name: "Today".to_string(), + }, + GroupIDTest { + cell_data: DateCellData { + timestamp: Some(three_days_before.timestamp()), + include_time: false, + }, + type_option: &local_date_type_option, + setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(), + exp_group_id: today + .checked_sub_days(Days::new(7)) + .unwrap() + .format(GROUP_ID_DATE_FORMAT) + .to_string(), + exp_group_name: "Last 7 days".to_string(), + }, + GroupIDTest { + cell_data: mar_14_2022_cd.clone(), + type_option: &local_date_type_option, + setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), + exp_group_id: "2022/03/14".to_string(), + exp_group_name: "Mar 14, 2022".to_string(), + }, + GroupIDTest { + cell_data: DateCellData { + timestamp: Some( + mar_14_2022 + .checked_add_signed(Duration::days(3)) + .unwrap() + .timestamp(), + ), + include_time: false, + }, + type_option: &local_date_type_option, + setting_content: r#"{"condition": 2, "hide_empty": false}"#.to_string(), + exp_group_id: "2022/03/14".to_string(), + exp_group_name: "Week of Mar 14-20 2022".to_string(), + }, + GroupIDTest { + cell_data: mar_14_2022_cd.clone(), + type_option: &local_date_type_option, + setting_content: r#"{"condition": 3, "hide_empty": false}"#.to_string(), + exp_group_id: "2022/03/01".to_string(), + exp_group_name: "Mar 2022".to_string(), + }, + GroupIDTest { + cell_data: mar_14_2022_cd.clone(), + type_option: &local_date_type_option, + setting_content: r#"{"condition": 4, "hide_empty": false}"#.to_string(), + exp_group_id: "2022/01/01".to_string(), + exp_group_name: "2022".to_string(), + }, + GroupIDTest { + cell_data: DateCellData { + timestamp: Some(1685715999), + include_time: false, + }, + type_option: &default_date_type_option, + setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), + exp_group_id: "2023/06/02".to_string(), + exp_group_name: "".to_string(), + }, + GroupIDTest { + cell_data: DateCellData { + timestamp: Some(1685802386), + include_time: false, + }, + type_option: &default_date_type_option, + setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), + exp_group_id: "2023/06/03".to_string(), + exp_group_name: "".to_string(), + }, + ]; + + for (i, test) in tests.iter().enumerate() { + let group_id = group_id( + &test.cell_data, + Some(test.type_option), + &test.setting_content, + ); + assert_eq!(test.exp_group_id, group_id, "test {}", i); + + if test.exp_group_name != "" { + let group_name = + group_name_from_id(&group_id, Some(test.type_option), &test.setting_content); + assert_eq!(test.exp_group_name, group_name, "test {}", i); + } + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/mod.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/mod.rs index fb890e6d29..3ab5cb598e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/mod.rs @@ -1,9 +1,11 @@ mod checkbox_controller; +mod date_controller; mod default_controller; mod select_option_controller; mod url_controller; pub use checkbox_controller::*; +pub use date_controller::*; pub use default_controller::*; pub use select_option_controller::*; pub use url_controller::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index d2face26c2..90e53a1101 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -1,10 +1,13 @@ +use chrono::NaiveDateTime; use collab_database::fields::Field; use collab_database::rows::{Cell, Row, RowDetail}; use crate::entities::{ FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, SelectOptionCellDataPB, }; -use crate::services::cell::{insert_checkbox_cell, insert_select_option_cell, insert_url_cell}; +use crate::services::cell::{ + insert_checkbox_cell, insert_date_cell, insert_select_option_cell, insert_url_cell, +}; use crate::services::field::{SelectOption, CHECK}; use crate::services::group::controller::MoveGroupRowContext; use crate::services::group::{GeneratedGroupConfig, Group, GroupData}; @@ -170,6 +173,15 @@ pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option { let cell = insert_url_cell(group_id.to_owned(), field); Some(cell) }, + FieldType::DateTime => { + let date = NaiveDateTime::parse_from_str( + &format!("{} 00:00:00", group_id).to_string(), + "%Y/%m/%d %H:%M:%S", + ) + .unwrap(); + let cell = insert_date_cell(date.timestamp(), None, field); + Some(cell) + }, _ => { tracing::warn!("Unknown field type: {:?}", field_type); None diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs index f8b4601120..87f8930234 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs @@ -7,10 +7,9 @@ use collab_database::views::DatabaseLayout; use flowy_error::FlowyResult; use crate::entities::FieldType; -use crate::services::group::configuration::GroupSettingReader; -use crate::services::group::controller::GroupController; use crate::services::group::{ - CheckboxGroupContext, CheckboxGroupController, DefaultGroupController, Group, GroupSetting, + CheckboxGroupContext, CheckboxGroupController, DateGroupContext, DateGroupController, + DefaultGroupController, Group, GroupController, GroupSetting, GroupSettingReader, GroupSettingWriter, MultiSelectGroupController, MultiSelectOptionGroupContext, SingleSelectGroupController, SingleSelectOptionGroupContext, URLGroupContext, URLGroupController, }; @@ -95,6 +94,17 @@ where let controller = URLGroupController::new(&grouping_field, configuration).await?; group_controller = Box::new(controller); }, + FieldType::DateTime => { + let configuration = DateGroupContext::new( + view_id, + grouping_field.clone(), + configuration_reader, + configuration_writer, + ) + .await?; + let controller = DateGroupController::new(&grouping_field, configuration).await?; + group_controller = Box::new(controller); + }, _ => { group_controller = Box::new(DefaultGroupController::new(&grouping_field)); }, diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs new file mode 100644 index 0000000000..298476e1d1 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs @@ -0,0 +1,208 @@ +use crate::database::group_test::script::DatabaseGroupTest; +use crate::database::group_test::script::GroupScript::*; +use chrono::NaiveDateTime; +use chrono::{offset, Duration}; +use collab_database::database::gen_row_id; +use collab_database::rows::CreateRowParams; +use flowy_database2::entities::FieldType; +use flowy_database2::services::cell::CellBuilder; +use flowy_database2::services::field::DateCellData; +use std::collections::HashMap; +use std::vec; + +#[tokio::test] +async fn group_by_date_test() { + let date_diffs = vec![-1, 0, 7, -15, -1]; + let mut test = DatabaseGroupTest::new().await; + let date_field = test.get_field(FieldType::DateTime).await; + + for diff in date_diffs { + let timestamp = offset::Local::now() + .checked_add_signed(Duration::days(diff)) + .unwrap() + .timestamp() + .to_string(); + let mut cells = HashMap::new(); + cells.insert(date_field.id.clone(), timestamp); + let cells = CellBuilder::with_cells(cells, &[date_field.clone()]).build(); + + let params = CreateRowParams { + id: gen_row_id(), + cells, + height: 60, + visibility: true, + prev_row_id: None, + timestamp: 0, + }; + let res = test.editor.create_row(&test.view_id, None, params).await; + assert!(res.is_ok()); + } + + let today = offset::Local::now(); + let last_day = today + .checked_add_signed(Duration::days(-1)) + .unwrap() + .format("%Y/%m/%d") + .to_string(); + let last_30_days = today + .checked_add_signed(Duration::days(-30)) + .unwrap() + .format("%Y/%m/%d") + .to_string(); + let next_7_days = today + .checked_add_signed(Duration::days(2)) + .unwrap() + .format("%Y/%m/%d") + .to_string(); + + let scripts = vec![ + GroupByField { + field_id: date_field.id.clone(), + }, + AssertGroupCount(7), + AssertGroupRowCount { + group_index: 0, + row_count: 0, + }, + // Added via `make_test_board` + AssertGroupIDName { + group_index: 1, + group_id: "2022/03/01".to_string(), + group_name: "Mar 2022".to_string(), + }, + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + // Added via `make_test_board` + AssertGroupIDName { + group_index: 2, + group_id: "2022/11/01".to_string(), + group_name: "Nov 2022".to_string(), + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + AssertGroupIDName { + group_index: 3, + group_id: last_30_days, + group_name: "Last 30 days".to_string(), + }, + AssertGroupRowCount { + group_index: 3, + row_count: 1, + }, + AssertGroupIDName { + group_index: 4, + group_id: last_day, + group_name: "Yesterday".to_string(), + }, + AssertGroupRowCount { + group_index: 4, + row_count: 2, + }, + AssertGroupIDName { + group_index: 5, + group_id: today.format("%Y/%m/%d").to_string(), + group_name: "Today".to_string(), + }, + AssertGroupRowCount { + group_index: 5, + row_count: 1, + }, + AssertGroupIDName { + group_index: 6, + group_id: next_7_days, + group_name: "Next 7 days".to_string(), + }, + AssertGroupRowCount { + group_index: 6, + row_count: 1, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn change_row_group_on_date_cell_changed_test() { + let mut test = DatabaseGroupTest::new().await; + let date_field = test.get_field(FieldType::DateTime).await; + let scripts = vec![ + GroupByField { + field_id: date_field.id.clone(), + }, + AssertGroupCount(3), + // Nov 2, 2022 + UpdateGroupedCellWithData { + from_group_index: 1, + row_index: 0, + cell_data: "1667408732".to_string(), + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn change_date_on_moving_row_to_another_group() { + let mut test = DatabaseGroupTest::new().await; + let date_field = test.get_field(FieldType::DateTime).await; + let scripts = vec![ + GroupByField { + field_id: date_field.id.clone(), + }, + AssertGroupCount(3), + AssertGroupRowCount { + group_index: 1, + row_count: 3, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 2, + }, + MoveRow { + from_group_index: 1, + from_row_index: 0, + to_group_index: 2, + to_row_index: 0, + }, + AssertGroupRowCount { + group_index: 1, + row_count: 2, + }, + AssertGroupRowCount { + group_index: 2, + row_count: 3, + }, + AssertGroupIDName { + group_index: 2, + group_id: "2022/11/01".to_string(), + group_name: "Nov 2022".to_string(), + }, + ]; + test.run_scripts(scripts).await; + + let group = test.group_at_index(2).await; + let rows = group.clone().rows; + let row_id = &rows.get(0).unwrap().id; + let row_detail = test + .get_rows() + .await + .into_iter() + .find(|r| r.row.id.to_string() == row_id.to_string()) + .unwrap(); + let cell = row_detail.row.cells.get(&date_field.id.clone()).unwrap(); + let date_cell = DateCellData::from(cell); + + let date_time = + NaiveDateTime::parse_from_str("2022/11/01 00:00:00", "%Y/%m/%d %H:%M:%S").unwrap(); + assert_eq!(date_time.timestamp(), date_cell.timestamp.unwrap()); +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/mod.rs index 67671ae7f5..db3549957d 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/mod.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/mod.rs @@ -1,3 +1,4 @@ +mod date_group_test; mod script; mod test; mod url_group_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index b69bbd68bc..a8a39c645a 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -4,7 +4,7 @@ use collab_database::rows::{CreateRowParams, RowId}; use flowy_database2::entities::{FieldType, GroupPB, RowMetaPB}; use flowy_database2::services::cell::{ - delete_select_option_cell, insert_select_option_cell, insert_url_cell, + delete_select_option_cell, insert_date_cell, insert_select_option_cell, insert_url_cell, }; use flowy_database2::services::field::{ edit_single_select_type_option, SelectOption, SelectTypeOptionSharedAction, @@ -62,6 +62,11 @@ pub enum GroupScript { GroupByField { field_id: String, }, + AssertGroupIDName { + group_index: usize, + group_id: String, + group_name: String, + }, } pub struct DatabaseGroupTest { @@ -203,6 +208,9 @@ impl DatabaseGroupTest { let field_type = FieldType::from(field.field_type); let cell = match field_type { FieldType::URL => insert_url_cell(cell_data, &field), + FieldType::DateTime => { + insert_date_cell(cell_data.parse::().unwrap(), Some(true), &field) + }, _ => { panic!("Unsupported group field type"); }, @@ -252,6 +260,15 @@ impl DatabaseGroupTest { .await .unwrap(); }, + GroupScript::AssertGroupIDName { + group_index, + group_id, + group_name, + } => { + let group = self.group_at_index(group_index).await; + assert_eq!(group_id, group.group_id, "group index: {}", group_index); + assert_eq!(group_name, group.group_name, "group index: {}", group_index); + }, } } @@ -267,27 +284,11 @@ impl DatabaseGroupTest { #[allow(dead_code)] pub async fn get_multi_select_field(&self) -> Field { - self - .inner - .get_fields() - .into_iter() - .find(|field_rev| { - let field_type = FieldType::from(field_rev.field_type); - field_type.is_multi_select() - }) - .unwrap() + self.get_field(FieldType::MultiSelect).await } pub async fn get_single_select_field(&self) -> Field { - self - .inner - .get_fields() - .into_iter() - .find(|field| { - let field_type = FieldType::from(field.field_type); - field_type.is_single_select() - }) - .unwrap() + self.get_field(FieldType::SingleSelect).await } pub async fn edit_single_select_type_option( @@ -306,13 +307,17 @@ impl DatabaseGroupTest { } pub async fn get_url_field(&self) -> Field { + self.get_field(FieldType::URL).await + } + + pub async fn get_field(&self, field_type: FieldType) -> Field { self .inner .get_fields() .into_iter() .find(|field| { - let field_type = FieldType::from(field.field_type); - field_type.is_url() + let ft = FieldType::from(field.field_type); + ft == field_type }) .unwrap() }