refactor: different TypeOption between DateTime and LastModified/CreatedAt (#3356)

* fix: stringify date cell includes time if true

* refactor: LastModified and CreatedAt type option

* chore: frontend implementation

* chore: some adjustments and fix tests

* fix: integration tests

* chore: timestamp type option ui
This commit is contained in:
Richard Shiue
2023-09-08 10:07:24 +08:00
committed by GitHub
parent a0fc4b86b0
commit 8bcc6384f2
42 changed files with 1256 additions and 289 deletions

View File

@ -4,7 +4,7 @@ use strum_macros::EnumIter;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
use crate::entities::{CellIdPB, FieldType};
use crate::entities::CellIdPB;
use crate::services::field::{DateFormat, DateTypeOption, TimeFormat};
#[derive(Clone, Debug, Default, ProtoBuf)]
@ -51,9 +51,6 @@ pub struct DateTypeOptionPB {
#[pb(index = 3)]
pub timezone_id: String,
#[pb(index = 4)]
pub field_type: FieldType,
}
impl From<DateTypeOption> for DateTypeOptionPB {
@ -62,7 +59,6 @@ impl From<DateTypeOption> for DateTypeOptionPB {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
timezone_id: data.timezone_id,
field_type: data.field_type,
}
}
}
@ -73,7 +69,6 @@ impl From<DateTypeOptionPB> for DateTypeOption {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
timezone_id: data.timezone_id,
field_type: data.field_type,
}
}
}

View File

@ -4,6 +4,7 @@ mod date_entities;
mod number_entities;
mod select_option;
mod text_entities;
mod timestamp_entities;
mod url_entities;
pub use checkbox_entities::*;
@ -12,4 +13,5 @@ pub use date_entities::*;
pub use number_entities::*;
pub use select_option::*;
pub use text_entities::*;
pub use timestamp_entities::*;
pub use url_entities::*;

View File

@ -0,0 +1,50 @@
use flowy_derive::ProtoBuf;
use crate::entities::{DateFormatPB, FieldType, TimeFormatPB};
use crate::services::field::TimestampTypeOption;
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct TimestampCellDataPB {
#[pb(index = 1)]
pub date_time: String,
#[pb(index = 2, one_of)]
pub timestamp: Option<i64>,
}
#[derive(Clone, Debug, Default, ProtoBuf)]
pub struct TimestampTypeOptionPB {
#[pb(index = 1)]
pub date_format: DateFormatPB,
#[pb(index = 2)]
pub time_format: TimeFormatPB,
#[pb(index = 3)]
pub include_time: bool,
#[pb(index = 4)]
pub field_type: FieldType,
}
impl From<TimestampTypeOption> for TimestampTypeOptionPB {
fn from(data: TimestampTypeOption) -> Self {
Self {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
include_time: data.include_time,
field_type: data.field_type,
}
}
}
impl From<TimestampTypeOptionPB> for TimestampTypeOption {
fn from(data: TimestampTypeOptionPB) -> Self {
Self {
date_format: data.date_format.into(),
time_format: data.time_format.into(),
include_time: data.include_time,
field_type: data.field_type,
}
}
}

View File

@ -24,8 +24,8 @@ use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, Data
use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData};
use crate::services::field::{
default_type_option_data_from_type, select_type_option_from_field, transform_type_option,
type_option_data_from_pb_or_default, type_option_to_pb, DateCellData, SelectOptionCellChangeset,
SelectOptionIds, TypeOptionCellDataHandler, TypeOptionCellExt,
type_option_data_from_pb_or_default, type_option_to_pb, SelectOptionCellChangeset,
SelectOptionIds, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt,
};
use crate::services::field_settings::{
default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams,
@ -614,9 +614,9 @@ impl DatabaseEditor {
FieldType::LastEditedTime | FieldType::CreatedTime => {
let row = database.get_row(row_id);
let cell_data = if field_type.is_created_time() {
DateCellData::new(row.created_at, true)
TimestampCellData::new(row.created_at)
} else {
DateCellData::new(row.modified_at, true)
TimestampCellData::new(row.modified_at)
};
Some(Cell::from(cell_data))
},
@ -651,9 +651,9 @@ impl DatabaseEditor {
.into_iter()
.map(|row| {
let data = if field_type.is_created_time() {
DateCellData::new(row.created_at, true)
TimestampCellData::new(row.created_at)
} else {
DateCellData::new(row.modified_at, true)
TimestampCellData::new(row.modified_at)
};
RowCell {
row_id: row.id,

View File

@ -10,7 +10,6 @@ mod tests {
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat,
TypeOptionCellDataSerde,
};
#[test]
@ -409,33 +408,18 @@ mod tests {
old_cell_data: Option<Cell>,
expected_str: &str,
) {
let (cell, cell_data) = type_option
let (cell, _) = type_option
.apply_changeset(changeset, old_cell_data)
.unwrap();
assert_eq!(
decode_cell_data(&cell, type_option, cell_data.include_time, field),
expected_str,
);
assert_eq!(decode_cell_data(&cell, type_option, field), expected_str,);
}
fn decode_cell_data(
cell: &Cell,
type_option: &DateTypeOption,
include_time: bool,
field: &Field,
) -> String {
fn decode_cell_data(cell: &Cell, type_option: &DateTypeOption, field: &Field) -> String {
let decoded_data = type_option
.decode_cell(cell, &FieldType::DateTime, field)
.unwrap();
let decoded_data = type_option.protobuf_encode(decoded_data);
if include_time {
format!("{} {}", decoded_data.date, decoded_data.time)
.trim_end()
.to_owned()
} else {
decoded_data.date
}
type_option.stringify_cell_data(decoded_data)
}
fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell {

View File

@ -13,8 +13,8 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult};
use crate::entities::{DateCellDataPB, DateFilterPB, FieldType};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
default_order, DateCellChangeset, DateCellData, DateCellDataWrapper, DateFormat, TimeFormat,
TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption,
TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde,
TypeOptionTransform,
};
use crate::services::sort::SortCondition;
@ -27,7 +27,6 @@ pub struct DateTypeOption {
pub date_format: DateFormat,
pub time_format: TimeFormat,
pub timezone_id: String,
pub field_type: FieldType,
}
impl Default for DateTypeOption {
@ -36,7 +35,6 @@ impl Default for DateTypeOption {
date_format: Default::default(),
time_format: Default::default(),
timezone_id: Default::default(),
field_type: FieldType::DateTime,
}
}
}
@ -59,15 +57,10 @@ impl From<TypeOptionData> for DateTypeOption {
.map(TimeFormat::from)
.unwrap_or_default();
let timezone_id = data.get_str_value("timezone_id").unwrap_or_default();
let field_type = data
.get_i64_value("field_type")
.map(FieldType::from)
.unwrap_or(FieldType::DateTime);
Self {
date_format,
time_format,
timezone_id,
field_type,
}
}
}
@ -78,7 +71,6 @@ impl From<DateTypeOption> for TypeOptionData {
.insert_i64_value("date_format", data.date_format.value())
.insert_i64_value("time_format", data.time_format.value())
.insert_str_value("timezone_id", data.timezone_id)
.insert_i64_value("field_type", data.field_type.value())
.build()
}
}
@ -88,7 +80,16 @@ impl TypeOptionCellDataSerde for DateTypeOption {
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
self.today_desc_from_timestamp(cell_data)
let timestamp = cell_data.timestamp;
let include_time = cell_data.include_time;
let (date, time) = self.formatted_date_time_from_timestamp(&timestamp);
DateCellDataPB {
date,
time,
timestamp: timestamp.unwrap_or_default(),
include_time,
}
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
@ -97,46 +98,50 @@ impl TypeOptionCellDataSerde for DateTypeOption {
}
impl DateTypeOption {
pub fn new(field_type: FieldType) -> Self {
Self {
field_type,
..Default::default()
}
pub fn new() -> Self {
Self::default()
}
pub fn test() -> Self {
Self {
timezone_id: "Etc/UTC".to_owned(),
field_type: FieldType::DateTime,
..Self::default()
}
}
fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB {
let timestamp = cell_data.timestamp.unwrap_or_default();
let include_time = cell_data.include_time;
fn formatted_date_time_from_timestamp(&self, timestamp: &Option<i64>) -> (String, String) {
if let Some(timestamp) = timestamp {
let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap();
let offset = self.get_timezone_offset(naive);
let date_time = DateTime::<Local>::from_utc(naive, offset);
let (date, time) = match cell_data.timestamp {
Some(timestamp) => {
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap();
let offset = self.get_timezone_offset(naive);
let date_time = DateTime::<Local>::from_utc(naive, offset);
let fmt = self.date_format.format_str();
let date = format!("{}", date_time.format(fmt));
let fmt = self.time_format.format_str();
let time = format!("{}", date_time.format(fmt));
(date, time)
} else {
("".to_owned(), "".to_owned())
}
}
let fmt = self.date_format.format_str();
let date = format!("{}", date_time.format(fmt));
let fmt = self.time_format.format_str();
let time = format!("{}", date_time.format(fmt));
(date, time)
fn naive_time_from_time_string(
&self,
include_time: bool,
time_str: Option<String>,
) -> FlowyResult<Option<NaiveTime>> {
match (include_time, time_str) {
(true, Some(time_str)) => {
let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
match result {
Ok(time) => Ok(Some(time)),
Err(_e) => {
let msg = format!("Parse {} failed", time_str);
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, msg))
},
}
},
None => ("".to_owned(), "".to_owned()),
};
DateCellDataPB {
date,
time,
include_time,
timestamp,
_ => Ok(None),
}
}
@ -211,7 +216,14 @@ impl CellDataDecoder for DateTypeOption {
}
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
self.today_desc_from_timestamp(cell_data).date
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)
} else {
date_string
}
}
fn stringify_cell(&self, cell: &Cell) -> String {
@ -236,15 +248,12 @@ impl CellDataChangeset for DateTypeOption {
};
if changeset.clear_flag == Some(true) {
let (timestamp, include_time) = (None, include_time);
let cell_data = DateCellData {
timestamp,
timestamp: None,
include_time,
};
let cell_wrapper: DateCellDataWrapper = (self.field_type.clone(), cell_data.clone()).into();
return Ok((Cell::from(cell_wrapper), cell_data));
return Ok((Cell::from(&cell_data), cell_data));
}
// update include_time if necessary
@ -256,27 +265,12 @@ impl CellDataChangeset for DateTypeOption {
// order to change the day without changing the time, the old time string
// should be passed in as well.
let changeset_timestamp = changeset.date;
// parse the time string, which is in the local timezone
let parsed_time = match (include_time, changeset.time) {
(true, Some(time_str)) => {
let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
match result {
Ok(time) => Ok(Some(time)),
Err(_e) => {
let msg = format!("Parse {} failed", time_str);
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, msg))
},
}
},
_ => Ok(None),
}?;
let parsed_time = self.naive_time_from_time_string(include_time, changeset.time)?;
let timestamp = self.timestamp_from_parsed_time_previous_and_new_timestamp(
parsed_time,
previous_timestamp,
changeset_timestamp,
changeset.date,
);
let cell_data = DateCellData {
@ -284,8 +278,7 @@ impl CellDataChangeset for DateTypeOption {
include_time,
};
let cell_wrapper: DateCellDataWrapper = (self.field_type.clone(), cell_data.clone()).into();
Ok((Cell::from(cell_wrapper), cell_data))
Ok((Cell::from(&cell_data), cell_data))
}
}

View File

@ -83,42 +83,19 @@ impl From<&DateCellDataPB> for DateCellData {
}
}
/// 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 {
data: DateCellData,
field_type: FieldType,
}
impl From<(FieldType, DateCellData)> for DateCellDataWrapper {
fn from((field_type, data): (FieldType, DateCellData)) -> Self {
Self { data, field_type }
}
}
impl From<DateCellDataWrapper> for Cell {
fn from(wrapper: DateCellDataWrapper) -> Self {
let (field_type, data) = (wrapper.field_type, wrapper.data);
let timestamp_string = match data.timestamp {
impl From<&DateCellData> for Cell {
fn from(cell_data: &DateCellData) -> Self {
let timestamp_string = match cell_data.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(field_type)
new_cell_builder(FieldType::DateTime)
.insert_str_value(CELL_DATA, timestamp_string)
.insert_bool_value("include_time", data.include_time)
.insert_bool_value("include_time", cell_data.include_time)
.build()
}
}
impl From<DateCellData> for Cell {
fn from(data: DateCellData) -> Self {
let data: DateCellDataWrapper = (FieldType::DateTime, data).into();
Cell::from(data)
}
}
impl<'de> serde::Deserialize<'de> for DateCellData {
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
where

View File

@ -4,6 +4,7 @@ pub mod date_type_option;
pub mod number_type_option;
pub mod selection_type_option;
pub mod text_type_option;
pub mod timestamp_type_option;
mod type_option;
mod type_option_cell;
mod url_type_option;
@ -14,6 +15,7 @@ pub use date_type_option::*;
pub use number_type_option::*;
pub use selection_type_option::*;
pub use text_type_option::*;
pub use timestamp_type_option::*;
pub use type_option::*;
pub use type_option_cell::*;
pub use url_type_option::*;

View File

@ -30,8 +30,8 @@ mod tests {
};
assert_eq!(
stringify_cell_data(&data.into(), &FieldType::RichText, &field_type, &field),
"Mar 14, 2022"
stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field),
"Mar 14, 2022 09:56"
);
}

View File

@ -238,14 +238,14 @@ impl TypeOptionCellData for StrCellData {
impl From<&Cell> for StrCellData {
fn from(cell: &Cell) -> Self {
Self(cell.get_str_value("data").unwrap_or_default())
Self(cell.get_str_value(CELL_DATA).unwrap_or_default())
}
}
impl From<StrCellData> for Cell {
fn from(data: StrCellData) -> Self {
new_cell_builder(FieldType::RichText)
.insert_str_value("data", data.0)
.insert_str_value(CELL_DATA, data.0)
.build()
}
}

View File

@ -0,0 +1,6 @@
#![allow(clippy::module_inception)]
mod timestamp_type_option;
mod timestamp_type_option_entities;
pub use timestamp_type_option::*;
pub use timestamp_type_option_entities::*;

View File

@ -0,0 +1,205 @@
use std::cmp::Ordering;
use chrono::{DateTime, Local, Offset};
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 crate::entities::{DateFilterPB, FieldType, TimestampCellDataPB};
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
use crate::services::field::{
default_order, DateFormat, TimeFormat, TimestampCellData, TypeOption, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform,
};
use crate::services::sort::SortCondition;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TimestampTypeOption {
pub date_format: DateFormat,
pub time_format: TimeFormat,
pub include_time: bool,
pub field_type: FieldType,
}
impl Default for TimestampTypeOption {
fn default() -> Self {
Self {
date_format: Default::default(),
time_format: Default::default(),
include_time: true,
field_type: FieldType::LastEditedTime,
}
}
}
impl TypeOption for TimestampTypeOption {
type CellData = TimestampCellData;
type CellChangeset = String;
type CellProtobufType = TimestampCellDataPB;
type CellFilter = DateFilterPB;
}
impl From<TypeOptionData> for TimestampTypeOption {
fn from(data: TypeOptionData) -> Self {
let date_format = data
.get_i64_value("date_format")
.map(DateFormat::from)
.unwrap_or_default();
let time_format = data
.get_i64_value("time_format")
.map(TimeFormat::from)
.unwrap_or_default();
let include_time = data.get_bool_value("include_time").unwrap_or_default();
let field_type = data
.get_i64_value("field_type")
.map(FieldType::from)
.unwrap_or(FieldType::LastEditedTime);
Self {
date_format,
time_format,
include_time,
field_type,
}
}
}
impl From<TimestampTypeOption> for TypeOptionData {
fn from(option: TimestampTypeOption) -> Self {
TypeOptionDataBuilder::new()
.insert_i64_value("date_format", option.date_format.value())
.insert_i64_value("time_format", option.time_format.value())
.insert_bool_value("include_time", option.include_time)
.insert_i64_value("field_type", option.field_type.value())
.build()
}
}
impl TypeOptionCellDataSerde for TimestampTypeOption {
fn protobuf_encode(
&self,
cell_data: <Self as TypeOption>::CellData,
) -> <Self as TypeOption>::CellProtobufType {
let timestamp = cell_data.timestamp;
let date_time = self.stringify_cell_data(cell_data);
TimestampCellDataPB {
date_time,
timestamp,
}
}
fn parse_cell(&self, cell: &Cell) -> FlowyResult<<Self as TypeOption>::CellData> {
Ok(TimestampCellData::from(cell))
}
}
impl TimestampTypeOption {
pub fn new(field_type: FieldType) -> Self {
Self {
field_type,
include_time: true,
..Default::default()
}
}
fn formatted_date_time_from_timestamp(&self, timestamp: &Option<i64>) -> (String, String) {
if let Some(timestamp) = timestamp {
let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap();
let offset = Local::now().offset().fix();
let date_time = DateTime::<Local>::from_utc(naive, offset);
let fmt = self.date_format.format_str();
let date = format!("{}", date_time.format(fmt));
let fmt = self.time_format.format_str();
let time = format!("{}", date_time.format(fmt));
(date, time)
} else {
("".to_owned(), "".to_owned())
}
}
}
impl TypeOptionTransform for TimestampTypeOption {}
impl CellDataDecoder for TimestampTypeOption {
fn decode_cell(
&self,
cell: &Cell,
decoded_field_type: &FieldType,
_field: &Field,
) -> FlowyResult<<Self as TypeOption>::CellData> {
// Return default data if the type_option_cell_data is not FieldType::DateTime.
// It happens when switching from one field to another.
// For example:
// FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen.
if !decoded_field_type.is_date() {
return Ok(Default::default());
}
self.parse_cell(cell)
}
fn stringify_cell_data(&self, cell_data: <Self as TypeOption>::CellData) -> String {
let timestamp = cell_data.timestamp;
let (date_string, time_string) = self.formatted_date_time_from_timestamp(&timestamp);
if self.include_time {
format!("{} {}", date_string, time_string)
} else {
date_string
}
}
fn stringify_cell(&self, cell: &Cell) -> String {
let cell_data = Self::CellData::from(cell);
self.stringify_cell_data(cell_data)
}
}
impl CellDataChangeset for TimestampTypeOption {
fn apply_changeset(
&self,
_changeset: <Self as TypeOption>::CellChangeset,
_cell: Option<Cell>,
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
Err(FlowyError::new(
ErrorCode::FieldInvalidOperation,
"Cells of this field type cannot be edited",
))
}
}
impl TypeOptionCellDataFilter for TimestampTypeOption {
fn apply_filter(
&self,
filter: &<Self as TypeOption>::CellFilter,
field_type: &FieldType,
cell_data: &<Self as TypeOption>::CellData,
) -> bool {
if !field_type.is_date() {
return true;
}
filter.is_visible(cell_data.timestamp)
}
}
impl TypeOptionCellDataCompare for TimestampTypeOption {
fn apply_cmp(
&self,
cell_data: &<Self as TypeOption>::CellData,
other_cell_data: &<Self as TypeOption>::CellData,
sort_condition: SortCondition,
) -> Ordering {
match (cell_data.timestamp, other_cell_data.timestamp) {
(Some(left), Some(right)) => {
let order = left.cmp(&right);
sort_condition.evaluate_order(order)
},
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => default_order(),
}
}
}

View File

@ -0,0 +1,69 @@
use collab::core::any_map::AnyMapExtension;
use collab_database::rows::{new_cell_builder, Cell};
use serde::Serialize;
use crate::{
entities::FieldType,
services::field::{TypeOptionCellData, CELL_DATA},
};
#[derive(Clone, Debug, Default, Serialize)]
pub struct TimestampCellData {
pub timestamp: Option<i64>,
}
impl TimestampCellData {
pub fn new(timestamp: i64) -> Self {
Self {
timestamp: Some(timestamp),
}
}
}
impl From<&Cell> for TimestampCellData {
fn from(cell: &Cell) -> Self {
let timestamp = cell
.get_str_value(CELL_DATA)
.and_then(|data| data.parse::<i64>().ok());
Self { timestamp }
}
}
/// 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 TimestampCellDataWrapper {
data: TimestampCellData,
field_type: FieldType,
}
impl From<(FieldType, TimestampCellData)> for TimestampCellDataWrapper {
fn from((field_type, data): (FieldType, TimestampCellData)) -> Self {
Self { data, field_type }
}
}
impl From<TimestampCellDataWrapper> for Cell {
fn from(wrapper: TimestampCellDataWrapper) -> Self {
let (field_type, data) = (wrapper.field_type, wrapper.data);
let timestamp_string = data.timestamp.unwrap_or_default();
new_cell_builder(field_type)
.insert_str_value(CELL_DATA, timestamp_string)
.build()
}
}
impl From<TimestampCellData> for Cell {
fn from(data: TimestampCellData) -> Self {
let data: TimestampCellDataWrapper = (FieldType::LastEditedTime, data).into();
Cell::from(data)
}
}
impl TypeOptionCellData for TimestampCellData {}
impl ToString for TimestampCellData {
fn to_string(&self) -> String {
serde_json::to_string(self).unwrap()
}
}

View File

@ -11,13 +11,13 @@ use flowy_error::FlowyResult;
use crate::entities::{
CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType,
MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB,
URLTypeOptionPB,
TimestampTypeOptionPB, URLTypeOptionPB,
};
use crate::services::cell::{CellDataDecoder, FromCellChangeset, ToCellChangeset};
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateFormat, DateTypeOption, MultiSelectTypeOption, NumberTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimeFormat, URLTypeOption,
RichTextTypeOption, SingleSelectTypeOption, TimeFormat, TimestampTypeOption, URLTypeOption,
};
use crate::services::filter::FromFilterString;
use crate::services::sort::SortCondition;
@ -179,9 +179,12 @@ pub fn type_option_data_from_pb_or_default<T: Into<Bytes>>(
FieldType::Number => {
NumberTypeOptionPB::try_from(bytes).map(|pb| NumberTypeOption::from(pb).into())
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
DateTypeOptionPB::try_from(bytes).map(|pb| DateTypeOption::from(pb).into())
},
FieldType::LastEditedTime | FieldType::CreatedTime => {
TimestampTypeOptionPB::try_from(bytes).map(|pb| TimestampTypeOption::from(pb).into())
},
FieldType::SingleSelect => {
SingleSelectTypeOptionPB::try_from(bytes).map(|pb| SingleSelectTypeOption::from(pb).into())
},
@ -214,10 +217,16 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
.try_into()
.unwrap()
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
let date_type_option: DateTypeOption = type_option.into();
DateTypeOptionPB::from(date_type_option).try_into().unwrap()
},
FieldType::LastEditedTime | FieldType::CreatedTime => {
let timestamp_type_option: TimestampTypeOption = type_option.into();
TimestampTypeOptionPB::from(timestamp_type_option)
.try_into()
.unwrap()
},
FieldType::SingleSelect => {
let single_select_type_option: SingleSelectTypeOption = type_option.into();
SingleSelectTypeOptionPB::from(single_select_type_option)
@ -254,14 +263,14 @@ pub fn default_type_option_data_from_type(field_type: &FieldType) -> TypeOptionD
FieldType::RichText => RichTextTypeOption::default().into(),
FieldType::Number => NumberTypeOption::default().into(),
FieldType::DateTime => DateTypeOption {
field_type: field_type.clone(),
..Default::default()
}
.into(),
FieldType::LastEditedTime | FieldType::CreatedTime => DateTypeOption {
FieldType::LastEditedTime | FieldType::CreatedTime => TimestampTypeOption {
field_type: field_type.clone(),
date_format: DateFormat::Friendly,
time_format: TimeFormat::TwelveHour,
include_time: true,
..Default::default()
}
.into(),

View File

@ -16,8 +16,8 @@ use crate::services::cell::{
use crate::services::field::checklist_type_option::ChecklistTypeOption;
use crate::services::field::{
CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption,
SingleSelectTypeOption, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
SingleSelectTypeOption, TimestampTypeOption, TypeOption, TypeOptionCellDataCompare,
TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption,
};
use crate::services::sort::SortCondition;
@ -407,7 +407,7 @@ impl<'a> TypeOptionCellExt<'a> {
self.cell_data_cache.clone(),
)
}),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => self
FieldType::DateTime => self
.field
.get_type_option::<DateTypeOption>(field_type)
.map(|type_option| {
@ -417,6 +417,16 @@ impl<'a> TypeOptionCellExt<'a> {
self.cell_data_cache.clone(),
)
}),
FieldType::LastEditedTime | FieldType::CreatedTime => self
.field
.get_type_option::<TimestampTypeOption>(field_type)
.map(|type_option| {
TypeOptionCellDataHandlerImpl::new_with_boxed(
type_option,
self.cell_filter_cache.clone(),
self.cell_data_cache.clone(),
)
}),
FieldType::SingleSelect => self
.field
.get_type_option::<SingleSelectTypeOption>(field_type)
@ -527,9 +537,12 @@ fn get_type_option_transform_handler(
FieldType::Number => {
Box::new(NumberTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
Box::new(DateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::LastEditedTime | FieldType::CreatedTime => {
Box::new(TimestampTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
},
FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data))
as Box<dyn TypeOptionTransformHandler>,
FieldType::MultiSelect => {
@ -590,6 +603,10 @@ impl RowSingleCellData {
into_date_field_cell_data,
<DateTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_timestamp_field_cell_data,
<TimestampTypeOption as TypeOption>::CellData
);
into_cell_data!(
into_check_list_field_cell_data,
<CheckboxTypeOption as TypeOption>::CellData

View File

@ -455,7 +455,6 @@ mod tests {
use chrono::{offset, Days, Duration, NaiveDateTime};
use crate::entities::FieldType;
use crate::services::{
field::{date_type_option::DateTypeOption, DateCellData},
group::controller_impls::date_controller::{
@ -481,9 +480,9 @@ mod tests {
let today = offset::Local::now();
let three_days_before = today.checked_add_signed(Duration::days(-3)).unwrap();
let mut local_date_type_option = DateTypeOption::new(FieldType::DateTime);
let mut local_date_type_option = DateTypeOption::new();
local_date_type_option.timezone_id = today.offset().to_string();
let mut default_date_type_option = DateTypeOption::new(FieldType::DateTime);
let mut default_date_type_option = DateTypeOption::new();
default_date_type_option.timezone_id = "".to_string();
let tests = vec![

View File

@ -22,12 +22,13 @@ async fn grid_cell_update() {
for (_, row_detail) in rows.iter().enumerate() {
for field in &fields {
let field_type = FieldType::from(field.field_type);
if field_type == FieldType::LastEditedTime || field_type == FieldType::CreatedTime {
continue;
}
let cell_changeset = match field_type {
FieldType::RichText => "".to_string(),
FieldType::Number => "123".to_string(),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
make_date_cell_string(123)
},
FieldType::DateTime => make_date_cell_string(123),
FieldType::SingleSelect => {
let type_option = field
.get_type_option::<SingleSelectTypeOption>(field.field_type)
@ -49,6 +50,7 @@ async fn grid_cell_update() {
.to_cell_changeset_str(),
FieldType::Checkbox => "1".to_string(),
FieldType::URL => "1".to_string(),
_ => "".to_string(),
};
scripts.push(UpdateCell {

View File

@ -31,7 +31,7 @@ async fn grid_create_field() {
];
test.run_scripts(scripts).await;
let (params, field) = create_date_field(&test.view_id(), FieldType::CreatedTime);
let (params, field) = create_timestamp_field(&test.view_id(), FieldType::CreatedTime);
let scripts = vec![
CreateField { params },
AssertFieldTypeOptionEqual {

View File

@ -2,7 +2,7 @@ use collab_database::fields::Field;
use flowy_database2::entities::{CreateFieldParams, FieldType};
use flowy_database2::services::field::{
type_option_to_pb, DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder,
RichTextTypeOption, SelectOption, SingleSelectTypeOption, TimeFormat,
RichTextTypeOption, SelectOption, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
};
pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) {
@ -41,32 +41,52 @@ pub fn create_single_select_field(grid_id: &str) -> (CreateFieldParams, Field) {
};
(params, single_select_field)
}
pub fn create_date_field(grid_id: &str, field_type: FieldType) -> (CreateFieldParams, Field) {
#[allow(dead_code)]
pub fn create_date_field(grid_id: &str) -> (CreateFieldParams, Field) {
let date_type_option = DateTypeOption {
date_format: DateFormat::US,
time_format: TimeFormat::TwentyFourHour,
timezone_id: "Etc/UTC".to_owned(),
};
let field = FieldBuilder::new(FieldType::DateTime, date_type_option.clone())
.name("Date")
.visibility(true)
.build();
let type_option_data = type_option_to_pb(date_type_option.into(), &FieldType::DateTime).to_vec();
let params = CreateFieldParams {
view_id: grid_id.to_owned(),
field_type: FieldType::DateTime,
type_option_data: Some(type_option_data),
};
(params, field)
}
pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFieldParams, Field) {
let timestamp_type_option = TimestampTypeOption {
date_format: DateFormat::US,
time_format: TimeFormat::TwentyFourHour,
include_time: true,
field_type: field_type.clone(),
};
let field: Field = match field_type {
FieldType::DateTime => FieldBuilder::new(field_type.clone(), date_type_option.clone())
.name("Date")
.visibility(true)
.build(),
FieldType::LastEditedTime => FieldBuilder::new(field_type.clone(), date_type_option.clone())
.name("Updated At")
.visibility(true)
.build(),
FieldType::CreatedTime => FieldBuilder::new(field_type.clone(), date_type_option.clone())
FieldType::LastEditedTime => {
FieldBuilder::new(field_type.clone(), timestamp_type_option.clone())
.name("Updated At")
.visibility(true)
.build()
},
FieldType::CreatedTime => FieldBuilder::new(field_type.clone(), timestamp_type_option.clone())
.name("Created At")
.visibility(true)
.build(),
_ => panic!("Unsupported group field type"),
};
let type_option_data = type_option_to_pb(date_type_option.into(), &field_type).to_vec();
let type_option_data = type_option_to_pb(timestamp_type_option.into(), &field_type).to_vec();
let params = CreateFieldParams {
view_id: grid_id.to_owned(),

View File

@ -7,7 +7,7 @@ use flowy_database2::entities::FieldType;
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
use flowy_database2::services::field::{
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor,
SingleSelectTypeOption, TimeFormat,
SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
};
use crate::database::database_editor::TestRowBuilder;
@ -36,17 +36,30 @@ pub fn make_test_board() -> DatabaseData {
.build();
fields.push(number_field);
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
// Date
let date_type_option = DateTypeOption {
date_format: DateFormat::US,
time_format: TimeFormat::TwentyFourHour,
timezone_id: "Etc/UTC".to_owned(),
};
let name = "Time";
let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
.name(name)
.visibility(true)
.build();
fields.push(date_field);
},
FieldType::LastEditedTime | FieldType::CreatedTime => {
// LastEditedTime and CreatedTime
let date_type_option = TimestampTypeOption {
date_format: DateFormat::US,
time_format: TimeFormat::TwentyFourHour,
include_time: true,
field_type: field_type.clone(),
};
let name = match field_type {
FieldType::DateTime => "Time",
FieldType::LastEditedTime => "Updated At",
FieldType::LastEditedTime => "Last Modified",
FieldType::CreatedTime => "Created At",
_ => "",
};
@ -128,7 +141,7 @@ pub fn make_test_board() -> DatabaseData {
FieldType::RichText => row_builder.insert_text_cell("A"),
FieldType::Number => row_builder.insert_number_cell("1"),
// 1647251762 => Mar 14,2022
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1647251762, None, None, &field_type)
},
FieldType::SingleSelect => {
@ -148,7 +161,7 @@ pub fn make_test_board() -> DatabaseData {
FieldType::RichText => row_builder.insert_text_cell("B"),
FieldType::Number => row_builder.insert_number_cell("2"),
// 1647251762 => Mar 14,2022
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1647251762, None, None, &field_type)
},
FieldType::SingleSelect => {
@ -167,7 +180,7 @@ pub fn make_test_board() -> DatabaseData {
FieldType::RichText => row_builder.insert_text_cell("C"),
FieldType::Number => row_builder.insert_number_cell("3"),
// 1647251762 => Mar 14,2022
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1647251762, None, None, &field_type)
},
FieldType::SingleSelect => {
@ -189,7 +202,7 @@ pub fn make_test_board() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("DA"),
FieldType::Number => row_builder.insert_number_cell("4"),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1668704685, None, None, &field_type)
},
FieldType::SingleSelect => {
@ -206,7 +219,7 @@ pub fn make_test_board() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("AE"),
FieldType::Number => row_builder.insert_number_cell(""),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1668359085, None, None, &field_type)
},
FieldType::SingleSelect => {

View File

@ -7,7 +7,7 @@ use flowy_database2::entities::FieldType;
use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption;
use flowy_database2::services::field::{
DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption,
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat,
SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption,
};
use crate::database::database_editor::TestRowBuilder;
@ -39,26 +39,39 @@ pub fn make_test_grid() -> DatabaseData {
.build();
fields.push(number_field);
},
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
// Date
let date_type_option = DateTypeOption {
date_format: DateFormat::US,
time_format: TimeFormat::TwentyFourHour,
timezone_id: "Etc/UTC".to_owned(),
field_type: field_type.clone(),
};
let name = match field_type {
FieldType::DateTime => "Time",
FieldType::LastEditedTime => "Updated At",
FieldType::CreatedTime => "Created At",
_ => "",
};
let name = "Time";
let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
.name(name)
.visibility(true)
.build();
fields.push(date_field);
},
FieldType::LastEditedTime | FieldType::CreatedTime => {
// LastEditedTime and CreatedTime
let timestamp_type_option = TimestampTypeOption {
date_format: DateFormat::US,
time_format: TimeFormat::TwentyFourHour,
include_time: true,
field_type: field_type.clone(),
};
let name = match field_type {
FieldType::LastEditedTime => "Last Modified",
FieldType::CreatedTime => "Created At",
_ => "",
};
let timestamp_field = FieldBuilder::new(field_type.clone(), timestamp_type_option)
.name(name)
.visibility(true)
.build();
fields.push(timestamp_field);
},
FieldType::SingleSelect => {
// Single Select
let option1 = SelectOption::with_color(COMPLETED, SelectOptionColor::Purple);
@ -129,7 +142,7 @@ pub fn make_test_grid() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("A"),
FieldType::Number => row_builder.insert_number_cell("1"),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1647251762, None, None, &field_type)
},
FieldType::MultiSelect => row_builder
@ -150,7 +163,7 @@ pub fn make_test_grid() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell(""),
FieldType::Number => row_builder.insert_number_cell("2"),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1647251762, None, None, &field_type)
},
FieldType::MultiSelect => row_builder
@ -165,7 +178,7 @@ pub fn make_test_grid() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("C"),
FieldType::Number => row_builder.insert_number_cell("3"),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1647251762, None, None, &field_type)
},
FieldType::SingleSelect => {
@ -184,7 +197,7 @@ pub fn make_test_grid() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("DA"),
FieldType::Number => row_builder.insert_number_cell("14"),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1668704685, None, None, &field_type)
},
FieldType::SingleSelect => {
@ -200,7 +213,7 @@ pub fn make_test_grid() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("AE"),
FieldType::Number => row_builder.insert_number_cell(""),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1668359085, None, None, &field_type)
},
FieldType::SingleSelect => {
@ -218,7 +231,7 @@ pub fn make_test_grid() -> DatabaseData {
match field_type {
FieldType::RichText => row_builder.insert_text_cell("AE"),
FieldType::Number => row_builder.insert_number_cell("5"),
FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => {
FieldType::DateTime => {
row_builder.insert_date_cell(1671938394, None, None, &field_type)
},
FieldType::SingleSelect => {

View File

@ -27,13 +27,13 @@ async fn export_csv_test() {
let test = DatabaseEditorTest::new_grid().await;
let database = test.editor.clone();
let s = database.export_csv(CSVFormat::Original).await.unwrap();
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Updated At,Created At
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,2022/03/14,2022/03/14
,$2,2022/03/14,,"Google,Twitter",Yes,,,2022/03/14,2022/03/14
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,2022/03/14,2022/03/14
DA,$14,2022/11/17,Completed,,No,,,2022/11/17,2022/11/17
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,2022/11/13,2022/11/13
AE,$5,2022/12/25,Planned,Facebook,Yes,,,2022/12/25,2022/12/25
let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At
A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,,
,$2,2022/03/14,,"Google,Twitter",Yes,,,,
C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,,
DA,$14,2022/11/17,Completed,,No,,,,
AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,,
AE,$5,2022/12/25,Planned,Facebook,Yes,,,,
CB,,,,,,,,,
"#;
println!("{}", s);