feat: add end date to time cell data (#3369)

* feat: add end date to time cell data

* feat: implement ui for end time

* test: add date to text test

* chore: clippy warnings

* fix: unexpected time parsing

* fix: fix the date logic when toggling end date

---------

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Richard Shiue
2023-09-19 09:58:15 +08:00
committed by GitHub
parent b700f95c7f
commit 124d435f09
21 changed files with 629 additions and 124 deletions

View File

@ -19,7 +19,19 @@ pub struct DateCellDataPB {
pub timestamp: i64,
#[pb(index = 4)]
pub end_date: String,
#[pb(index = 5)]
pub end_time: String,
#[pb(index = 6)]
pub end_timestamp: i64,
#[pb(index = 7)]
pub include_time: bool,
#[pb(index = 8)]
pub is_range: bool,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
@ -34,9 +46,18 @@ pub struct DateChangesetPB {
pub time: Option<String>,
#[pb(index = 4, one_of)]
pub include_time: Option<bool>,
pub end_date: Option<i64>,
#[pb(index = 5, one_of)]
pub end_time: Option<String>,
#[pb(index = 6, one_of)]
pub include_time: Option<bool>,
#[pb(index = 7, one_of)]
pub is_range: Option<bool>,
#[pb(index = 8, one_of)]
pub clear_flag: Option<bool>,
}

View File

@ -641,7 +641,10 @@ pub(crate) async fn update_date_cell_handler(
let cell_changeset = DateCellChangeset {
date: data.date,
time: data.time,
end_date: data.end_date,
end_time: data.end_time,
include_time: data.include_time,
is_range: data.is_range,
clear_flag: data.clear_flag,
};
let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?;

View File

@ -210,9 +210,8 @@ pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
pub fn insert_date_cell(timestamp: i64, include_time: Option<bool>, field: &Field) -> Cell {
let cell_data = serde_json::to_string(&DateCellChangeset {
date: Some(timestamp),
time: None,
include_time,
clear_flag: None,
..Default::default()
})
.unwrap();
apply_cell_changeset(cell_data, None, field, None).unwrap()

View File

@ -27,7 +27,7 @@ mod tests {
date: Some(1647251762),
time: None,
include_time: None,
clear_flag: None,
..Default::default()
},
None,
"Mar 14, 2022",
@ -41,7 +41,7 @@ mod tests {
date: Some(1647251762),
time: None,
include_time: None,
clear_flag: None,
..Default::default()
},
None,
"2022/03/14",
@ -55,7 +55,7 @@ mod tests {
date: Some(1647251762),
time: None,
include_time: None,
clear_flag: None,
..Default::default()
},
None,
"2022-03-14",
@ -69,7 +69,7 @@ mod tests {
date: Some(1647251762),
time: None,
include_time: None,
clear_flag: None,
..Default::default()
},
None,
"03/14/2022",
@ -83,7 +83,7 @@ mod tests {
date: Some(1647251762),
time: None,
include_time: None,
clear_flag: None,
..Default::default()
},
None,
"14/03/2022",
@ -109,7 +109,7 @@ mod tests {
date: Some(1653609600),
time: None,
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 00:00",
@ -121,7 +121,7 @@ mod tests {
date: Some(1653609600),
time: Some("9:00".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 09:00",
@ -133,7 +133,7 @@ mod tests {
date: Some(1653609600),
time: Some("23:00".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 23:00",
@ -147,7 +147,7 @@ mod tests {
date: Some(1653609600),
time: None,
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 12:00 AM",
@ -159,7 +159,7 @@ mod tests {
date: Some(1653609600),
time: Some("9:00 AM".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 09:00 AM",
@ -171,7 +171,7 @@ mod tests {
date: Some(1653609600),
time: Some("11:23 pm".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 11:23 PM",
@ -195,7 +195,7 @@ mod tests {
date: Some(1653609600),
time: Some("1:".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 01:00",
@ -216,7 +216,7 @@ mod tests {
date: Some(1653609600),
time: Some("".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 01:00",
@ -235,7 +235,7 @@ mod tests {
date: Some(1653609600),
time: Some("00:00".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 00:00",
@ -256,7 +256,7 @@ mod tests {
date: Some(1653609600),
time: Some("1:00 am".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 01:00 AM",
@ -280,7 +280,7 @@ mod tests {
date: Some(1653609600),
time: Some("20:00".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
None,
"May 27, 2022 08:00 PM",
@ -329,7 +329,7 @@ mod tests {
date: Some(1700006400),
time: Some("08:00".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
);
assert_date(
@ -339,7 +339,7 @@ mod tests {
date: Some(1701302400),
time: None,
include_time: None,
clear_flag: None,
..Default::default()
},
Some(old_cell_data),
"Nov 30, 2023 08:00",
@ -357,7 +357,7 @@ mod tests {
date: Some(1700006400),
time: Some("08:00".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
);
assert_date(
@ -367,7 +367,7 @@ mod tests {
date: None,
time: Some("14:00".to_owned()),
include_time: None,
clear_flag: None,
..Default::default()
},
Some(old_cell_data),
"Nov 15, 2023 14:00",
@ -385,7 +385,7 @@ mod tests {
date: Some(1700006400),
time: Some("08:00".to_owned()),
include_time: Some(true),
clear_flag: None,
..Default::default()
},
);
assert_date(
@ -396,12 +396,142 @@ mod tests {
time: None,
include_time: Some(true),
clear_flag: Some(true),
..Default::default()
},
Some(old_cell_data),
"",
);
}
#[test]
fn end_date_time_test() {
let type_option = DateTypeOption::test();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some(1653609600),
end_date: Some(1653782400),
include_time: Some(false),
is_range: Some(true),
..Default::default()
},
None,
"May 27, 2022 → May 29, 2022",
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some(1653609600),
time: Some("20:00".to_owned()),
end_date: Some(1653782400),
end_time: Some("08:00".to_owned()),
include_time: Some(true),
is_range: Some(true),
..Default::default()
},
None,
"May 27, 2022 20:00 → May 29, 2022 08:00",
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: Some(1653609600),
time: Some("20:00".to_owned()),
end_date: Some(1653782400),
include_time: Some(true),
is_range: Some(true),
..Default::default()
},
None,
"May 27, 2022 20:00 → May 29, 2022 00:00",
);
}
#[test]
fn turn_on_date_range() {
let type_option = DateTypeOption::test();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
let old_cell_data = initialize_date_cell(
&type_option,
DateCellChangeset {
date: Some(1653609600),
time: Some("08:00".to_owned()),
include_time: Some(true),
..Default::default()
},
);
assert_date(
&type_option,
&field,
DateCellChangeset {
is_range: Some(true),
..Default::default()
},
Some(old_cell_data),
"May 27, 2022 08:00 → May 27, 2022 08:00",
);
}
#[test]
fn add_an_end_time() {
let type_option = DateTypeOption::test();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
let old_cell_data = initialize_date_cell(
&type_option,
DateCellChangeset {
date: Some(1653609600),
time: Some("08:00".to_owned()),
include_time: Some(true),
..Default::default()
},
);
assert_date(
&type_option,
&field,
DateCellChangeset {
date: None,
time: None,
end_date: Some(1700006400),
end_time: Some("16:00".to_owned()),
include_time: Some(true),
is_range: Some(true),
..Default::default()
},
Some(old_cell_data),
"May 27, 2022 08:00 → Nov 15, 2023 16:00",
);
}
#[test]
#[should_panic]
fn end_date_with_no_start_date() {
let type_option = DateTypeOption::test();
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
assert_date(
&type_option,
&field,
DateCellChangeset {
date: None,
end_date: Some(1653782400),
include_time: Some(false),
is_range: Some(true),
..Default::default()
},
None,
"→ May 29, 2022",
);
}
fn assert_date(
type_option: &DateTypeOption,
field: &Field,

View File

@ -70,15 +70,24 @@ impl TypeOptionCellDataSerde for DateTypeOption {
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
let timestamp = cell_data.timestamp;
let include_time = cell_data.include_time;
let is_range = cell_data.is_range;
let timestamp = cell_data.timestamp;
let (date, time) = self.formatted_date_time_from_timestamp(&timestamp);
let end_timestamp = cell_data.end_timestamp;
let (end_date, end_time) = self.formatted_date_time_from_timestamp(&end_timestamp);
DateCellDataPB {
date,
time,
timestamp: timestamp.unwrap_or_default(),
end_date,
end_time,
end_timestamp: end_timestamp.unwrap_or_default(),
include_time,
is_range,
}
}
@ -135,6 +144,8 @@ impl DateTypeOption {
}
}
/// combine the changeset_timestamp and parsed_time if provided. if
/// changeset_timestamp is None, fallback to previous_timestamp
fn timestamp_from_parsed_time_previous_and_new_timestamp(
&self,
parsed_time: Option<NaiveTime>,
@ -142,7 +153,7 @@ impl DateTypeOption {
changeset_timestamp: Option<i64>,
) -> Option<i64> {
if let Some(time) = parsed_time {
// a valid time is provided, so we replace the time component of old
// a valid time is provided, so we replace the time component of old timestamp
// (or new timestamp if provided) with it.
let utc_date = changeset_timestamp
.or(previous_timestamp)
@ -206,13 +217,30 @@ impl CellDataDecoder for DateTypeOption {
}
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
let timestamp = cell_data.timestamp;
let include_time = cell_data.include_time;
let (date_string, time_string) = self.formatted_date_time_from_timestamp(&timestamp);
if include_time && timestamp.is_some() {
format!("{} {}", date_string, time_string)
let timestamp = cell_data.timestamp;
let is_range = cell_data.is_range;
let (date, time) = self.formatted_date_time_from_timestamp(&timestamp);
if is_range {
let (end_date, end_time) = match cell_data.end_timestamp {
Some(timestamp) => self.formatted_date_time_from_timestamp(&Some(timestamp)),
None => (date.clone(), time.clone()),
};
if include_time && timestamp.is_some() {
format!("{} {}{} {}", date, time, end_date, end_time)
.trim()
.to_string()
} else if timestamp.is_some() {
format!("{}{}", date, end_date).trim().to_string()
} else {
"".to_string()
}
} else if include_time {
format!("{} {}", date, time).trim().to_string()
} else {
date_string
date
}
}
@ -229,25 +257,33 @@ impl CellDataChangeset for DateTypeOption {
cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
// old date cell data
let (previous_timestamp, include_time) = match cell {
let (previous_timestamp, previous_end_timestamp, include_time, is_range) = match cell {
Some(cell) => {
let cell_data = DateCellData::from(&cell);
(cell_data.timestamp, cell_data.include_time)
(
cell_data.timestamp,
cell_data.end_timestamp,
cell_data.include_time,
cell_data.is_range,
)
},
None => (None, false),
None => (None, None, false, false),
};
if changeset.clear_flag == Some(true) {
let cell_data = DateCellData {
timestamp: None,
end_timestamp: None,
include_time,
is_range,
};
return Ok((Cell::from(&cell_data), cell_data));
}
// update include_time if necessary
// update include_time and is_range if necessary
let include_time = changeset.include_time.unwrap_or(include_time);
let is_range = changeset.is_range.unwrap_or(is_range);
// Calculate the timestamp in the time zone specified in type option. If
// a new timestamp is included in the changeset without an accompanying
@ -255,17 +291,38 @@ impl CellDataChangeset for DateTypeOption {
// order to change the day without changing the time, the old time string
// should be passed in as well.
let parsed_time = self.naive_time_from_time_string(include_time, changeset.time)?;
// parse the time string, which is in the local timezone
let parsed_start_time = self.naive_time_from_time_string(include_time, changeset.time)?;
let timestamp = self.timestamp_from_parsed_time_previous_and_new_timestamp(
parsed_time,
parsed_start_time,
previous_timestamp,
changeset.date,
);
let end_timestamp =
if is_range && changeset.end_date.is_none() && previous_end_timestamp.is_none() {
// just toggled is_range so no passed in or existing end time data
timestamp
} else if is_range {
// parse the changeset's end time data or fallback to previous version
let parsed_end_time = self.naive_time_from_time_string(include_time, changeset.end_time)?;
self.timestamp_from_parsed_time_previous_and_new_timestamp(
parsed_end_time,
previous_end_timestamp,
changeset.end_date,
)
} else {
// clear the end time data
None
};
let cell_data = DateCellData {
timestamp,
end_timestamp,
include_time,
is_range,
};
Ok((Cell::from(&cell_data), cell_data))

View File

@ -20,7 +20,10 @@ use crate::services::field::{TypeOptionCellData, CELL_DATA};
pub struct DateCellChangeset {
pub date: Option<i64>,
pub time: Option<String>,
pub end_date: Option<i64>,
pub end_time: Option<String>,
pub include_time: Option<bool>,
pub is_range: Option<bool>,
pub clear_flag: Option<bool>,
}
@ -42,15 +45,20 @@ impl ToCellChangeset for DateCellChangeset {
#[derive(Default, Clone, Debug, Serialize)]
pub struct DateCellData {
pub timestamp: Option<i64>,
pub end_timestamp: Option<i64>,
#[serde(default)]
pub include_time: bool,
#[serde(default)]
pub is_range: bool,
}
impl DateCellData {
pub fn new(timestamp: i64, include_time: bool) -> Self {
pub fn new(timestamp: i64, include_time: bool, is_range: bool) -> Self {
Self {
timestamp: Some(timestamp),
end_timestamp: None,
include_time,
is_range,
}
}
}
@ -66,10 +74,16 @@ impl From<&Cell> for DateCellData {
let timestamp = cell
.get_str_value(CELL_DATA)
.and_then(|data| data.parse::<i64>().ok());
let end_timestamp = cell
.get_str_value("end_timestamp")
.and_then(|data| data.parse::<i64>().ok());
let include_time = cell.get_bool_value("include_time").unwrap_or_default();
let is_range = cell.get_bool_value("is_range").unwrap_or_default();
Self {
timestamp,
end_timestamp,
include_time,
is_range,
}
}
}
@ -78,7 +92,9 @@ impl From<&DateCellDataPB> for DateCellData {
fn from(data: &DateCellDataPB) -> Self {
Self {
timestamp: Some(data.timestamp),
end_timestamp: Some(data.end_timestamp),
include_time: data.include_time,
is_range: data.is_range,
}
}
}
@ -89,9 +105,17 @@ impl From<&DateCellData> for Cell {
Some(timestamp) => timestamp.to_string(),
None => "".to_owned(),
};
let end_timestamp_string = match cell_data.end_timestamp {
Some(timestamp) => timestamp.to_string(),
None => "".to_owned(),
};
// Most of the case, don't use these keys in other places. Otherwise, we should define
// constants for them.
new_cell_builder(FieldType::DateTime)
.insert_str_value(CELL_DATA, timestamp_string)
.insert_str_value("end_timestamp", end_timestamp_string)
.insert_bool_value("include_time", cell_data.include_time)
.insert_bool_value("is_range", cell_data.is_range)
.build()
}
}
@ -118,7 +142,9 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
{
Ok(DateCellData {
timestamp: Some(value),
end_timestamp: None,
include_time: false,
is_range: false,
})
}
@ -134,25 +160,36 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
M: serde::de::MapAccess<'de>,
{
let mut timestamp: Option<i64> = None;
let mut end_timestamp: Option<i64> = None;
let mut include_time: Option<bool> = None;
let mut is_range: Option<bool> = None;
while let Some(key) = map.next_key()? {
match key {
"timestamp" => {
timestamp = map.next_value()?;
},
"end_timestamp" => {
end_timestamp = map.next_value()?;
},
"include_time" => {
include_time = map.next_value()?;
},
"is_range" => {
is_range = map.next_value()?;
},
_ => {},
}
}
let include_time = include_time.unwrap_or_default();
let is_range = is_range.unwrap_or_default();
Ok(DateCellData {
timestamp,
end_timestamp,
include_time,
is_range,
})
}
}

View File

@ -26,13 +26,39 @@ mod tests {
let data = DateCellData {
timestamp: Some(1647251762),
end_timestamp: None,
include_time: true,
is_range: false,
};
assert_eq!(
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
"Mar 14, 2022 09:56"
);
let data = DateCellData {
timestamp: Some(1647251762),
end_timestamp: Some(1648533809),
include_time: true,
is_range: false,
};
assert_eq!(
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
"Mar 14, 2022 09:56"
);
let data = DateCellData {
timestamp: Some(1647251762),
end_timestamp: Some(1648533809),
include_time: true,
is_range: true,
};
assert_eq!(
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
"Mar 14, 2022 09:56 → Mar 29, 2022 06:03"
);
}
fn to_text_cell(s: String) -> Cell {

View File

@ -476,6 +476,7 @@ mod tests {
let mar_14_2022_cd = DateCellData {
timestamp: Some(mar_14_2022.timestamp()),
include_time: false,
..Default::default()
};
let today = offset::Local::now();
let three_days_before = today.checked_add_signed(Duration::days(-3)).unwrap();
@ -497,6 +498,7 @@ mod tests {
cell_data: DateCellData {
timestamp: Some(today.timestamp()),
include_time: false,
..Default::default()
},
type_option: &local_date_type_option,
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
@ -507,6 +509,7 @@ mod tests {
cell_data: DateCellData {
timestamp: Some(three_days_before.timestamp()),
include_time: false,
..Default::default()
},
type_option: &local_date_type_option,
setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(),
@ -533,6 +536,7 @@ mod tests {
.timestamp(),
),
include_time: false,
..Default::default()
},
type_option: &local_date_type_option,
setting_content: r#"{"condition": 2, "hide_empty": false}"#.to_string(),
@ -557,6 +561,7 @@ mod tests {
cell_data: DateCellData {
timestamp: Some(1685715999),
include_time: false,
..Default::default()
},
type_option: &default_date_type_option,
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),
@ -567,6 +572,7 @@ mod tests {
cell_data: DateCellData {
timestamp: Some(1685802386),
include_time: false,
..Default::default()
},
type_option: &default_date_type_option,
setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(),