mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
9643315d5f
commit
2f8edf1fd1
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
},
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
},
|
||||
|
@ -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());
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod date_group_test;
|
||||
mod script;
|
||||
mod test;
|
||||
mod url_group_test;
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user