feat: group by date (#2641)

* feat: group by date

* test: added more tests for group by date

* fix: print month in abbrev format

* chore: adapt group event changes

* style: remove comment

* fix: change date on changing group

* fix: dont count time in relative group

* fix: check beginning of month is within 30 days

* refactor: unify group id date format

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Mohammad Zolfaghari 2023-08-14 07:07:35 +03:30 committed by GitHub
parent 9643315d5f
commit 2f8edf1fd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 891 additions and 40 deletions

View File

@ -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;

View File

@ -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,
}

View File

@ -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::<Local>::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)
},

View File

@ -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 {

View File

@ -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

View File

@ -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<Self, serde_json::Error>;
fn to_json(&self) -> Result<String, serde_json::Error>;
}
#[derive(Default, Serialize, Deserialize)]
pub struct DateGroupConfiguration {
pub hide_empty: bool,
pub condition: DateCondition,
}
impl GroupConfigurationContentSerde for DateGroupConfiguration {
fn from_json(s: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(s)
}
fn to_json(&self) -> Result<String, serde_json::Error> {
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<DateGroupConfiguration>;
impl GroupCustomize for DateGroupController {
type CellData = DateCellDataPB;
fn placeholder_cell(&self) -> Option<Cell> {
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<InsertedGroupPB>, Option<GroupPB>)> {
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<GroupRowsNotificationPB> {
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<GroupRowsNotificationPB> {
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<GroupRowsNotificationPB> {
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<GroupPB> {
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<Field>) {}
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<Self::TypeOptionType>,
) -> 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<GeneratedGroupConfig> = 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<i64>, timezone_id: &String) -> DateTime<Local> {
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::<Local>::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);
}
}
}
}

View File

@ -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::*;

View File

@ -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<Cell> {
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

View File

@ -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));
},

View File

@ -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());
}

View File

@ -1,3 +1,4 @@
mod date_group_test;
mod script;
mod test;
mod url_group_test;

View File

@ -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::<i64>().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()
}