Ver Fonte

chore: include time per cell (#1901)

* style: autoformat

* chore: add include_time to cell data

* chore: remove include_time from date field type options

* chore: fix tests

* chore: custom deserializer for date cell data

* chore: add more tests

* chore: simplify date calculation logic

* chore: move include time to per-cell setting in UI

* test: add another text str test

* chore: adapt changes from upstream
Richard Shiue há 2 anos atrás
pai
commit
77ff2e987a

+ 1 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart

@@ -13,7 +13,7 @@ typedef SelectOptionCellController
     = CellController<SelectOptionCellDataPB, String>;
 typedef ChecklistCellController
     = CellController<SelectOptionCellDataPB, String>;
-typedef DateCellController = CellController<DateCellDataPB, CalendarData>;
+typedef DateCellController = CellController<DateCellDataPB, DateCellData>;
 typedef URLCellController = CellController<URLCellDataPB, String>;
 
 class CellControllerBuilder {

+ 9 - 5
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart

@@ -27,24 +27,28 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
 }
 
 @freezed
-class CalendarData with _$CalendarData {
-  const factory CalendarData({required DateTime date, String? time}) =
-      _CalendarData;
+class DateCellData with _$DateCellData {
+  const factory DateCellData({
+    required DateTime date,
+    String? time,
+    required bool includeTime,
+  }) = _DateCellData;
 }
 
-class DateCellDataPersistence implements CellDataPersistence<CalendarData> {
+class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
   final CellIdentifier cellId;
   DateCellDataPersistence({
     required this.cellId,
   });
 
   @override
-  Future<Option<FlowyError>> save(CalendarData data) {
+  Future<Option<FlowyError>> save(DateCellData data) {
     var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
 
     final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
     payload.date = date;
     payload.isUtc = data.date.isUtc;
+    payload.includeTime = data.includeTime;
 
     if (data.time != null) {
       payload.time = data.time!;

+ 45 - 40
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart

@@ -15,7 +15,7 @@ import 'package:table_calendar/table_calendar.dart';
 import 'dart:async';
 import 'package:dartz/dartz.dart';
 import 'package:protobuf/protobuf.dart';
-import 'package:fixnum/fixnum.dart' as $fixnum;
+
 part 'date_cal_bloc.freezed.dart';
 
 class DateCellCalendarBloc
@@ -42,13 +42,13 @@ class DateCellCalendarBloc
             emit(state.copyWith(focusedDay: focusedDay));
           },
           didReceiveCellUpdate: (DateCellDataPB? cellData) {
-            final calData = calDataFromCellData(cellData);
-            final time = calData.foldRight(
+            final dateCellData = calDataFromCellData(cellData);
+            final time = dateCellData.foldRight(
                 "", (dateData, previous) => dateData.time ?? '');
-            emit(state.copyWith(calData: calData, time: time));
+            emit(state.copyWith(dateCellData: dateCellData, time: time));
           },
           setIncludeTime: (includeTime) async {
-            await _updateTypeOption(emit, includeTime: includeTime);
+            await _updateDateData(emit, includeTime: includeTime);
           },
           setDateFormat: (dateFormat) async {
             await _updateTypeOption(emit, dateFormat: dateFormat);
@@ -57,14 +57,14 @@ class DateCellCalendarBloc
             await _updateTypeOption(emit, timeFormat: timeFormat);
           },
           setTime: (time) async {
-            if (state.calData.isSome()) {
+            if (state.dateCellData.isSome()) {
               await _updateDateData(emit, time: time);
             }
           },
           didUpdateCalData:
-              (Option<CalendarData> data, Option<String> timeFormatError) {
+              (Option<DateCellData> data, Option<String> timeFormatError) {
             emit(state.copyWith(
-                calData: data, timeFormatError: timeFormatError));
+                dateCellData: data, timeFormatError: timeFormatError));
           },
         );
       },
@@ -72,9 +72,13 @@ class DateCellCalendarBloc
   }
 
   Future<void> _updateDateData(Emitter<DateCellCalendarState> emit,
-      {DateTime? date, String? time}) {
-    final CalendarData newDateData = state.calData.fold(
-      () => CalendarData(date: date ?? DateTime.now(), time: time),
+      {DateTime? date, String? time, bool? includeTime}) {
+    final DateCellData newDateData = state.dateCellData.fold(
+      () => DateCellData(
+        date: date ?? DateTime.now(),
+        time: time,
+        includeTime: includeTime ?? false,
+      ),
       (dateData) {
         var newDateData = dateData;
         if (date != null && !isSameDay(newDateData.date, date)) {
@@ -84,6 +88,11 @@ class DateCellCalendarBloc
         if (newDateData.time != time) {
           newDateData = newDateData.copyWith(time: time);
         }
+
+        if (includeTime != null && newDateData.includeTime != includeTime) {
+          newDateData = newDateData.copyWith(includeTime: includeTime);
+        }
+
         return newDateData;
       },
     );
@@ -92,15 +101,16 @@ class DateCellCalendarBloc
   }
 
   Future<void> _saveDateData(
-      Emitter<DateCellCalendarState> emit, CalendarData newCalData) async {
-    if (state.calData == Some(newCalData)) {
+      Emitter<DateCellCalendarState> emit, DateCellData newCalData) async {
+    if (state.dateCellData == Some(newCalData)) {
       return;
     }
 
     updateCalData(
-        Option<CalendarData> calData, Option<String> timeFormatError) {
+        Option<DateCellData> dateCellData, Option<String> timeFormatError) {
       if (!isClosed) {
-        add(DateCellCalendarEvent.didUpdateCalData(calData, timeFormatError));
+        add(DateCellCalendarEvent.didUpdateCalData(
+            dateCellData, timeFormatError));
       }
     }
 
@@ -110,7 +120,7 @@ class DateCellCalendarBloc
         (err) {
           switch (ErrorCode.valueOf(err.code)!) {
             case ErrorCode.InvalidDateTimeFormat:
-              updateCalData(state.calData, Some(timeFormatPrompt(err)));
+              updateCalData(state.dateCellData, Some(timeFormatPrompt(err)));
               break;
             default:
               Log.error(err);
@@ -159,7 +169,6 @@ class DateCellCalendarBloc
     Emitter<DateCellCalendarState> emit, {
     DateFormat? dateFormat,
     TimeFormat? timeFormat,
-    bool? includeTime,
   }) async {
     state.dateTypeOptionPB.freeze();
     final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) {
@@ -170,10 +179,6 @@ class DateCellCalendarBloc
       if (timeFormat != null) {
         typeOption.timeFormat = timeFormat;
       }
-
-      if (includeTime != null) {
-        typeOption.includeTime = includeTime;
-      }
     });
 
     final result = await FieldBackendService.updateFieldTypeOption(
@@ -208,7 +213,7 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
   const factory DateCellCalendarEvent.didReceiveCellUpdate(
       DateCellDataPB? data) = _DidReceiveCellUpdate;
   const factory DateCellCalendarEvent.didUpdateCalData(
-          Option<CalendarData> data, Option<String> timeFormatError) =
+          Option<DateCellData> data, Option<String> timeFormatError) =
       _DidUpdateCalData;
 }
 
@@ -219,7 +224,7 @@ class DateCellCalendarState with _$DateCellCalendarState {
     required CalendarFormat format,
     required DateTime focusedDay,
     required Option<String> timeFormatError,
-    required Option<CalendarData> calData,
+    required Option<DateCellData> dateCellData,
     required String? time,
     required String timeHintText,
   }) = _DateCellCalendarState;
@@ -228,15 +233,15 @@ class DateCellCalendarState with _$DateCellCalendarState {
     DateTypeOptionPB dateTypeOptionPB,
     DateCellDataPB? cellData,
   ) {
-    Option<CalendarData> calData = calDataFromCellData(cellData);
+    Option<DateCellData> dateCellData = calDataFromCellData(cellData);
     final time =
-        calData.foldRight("", (dateData, previous) => dateData.time ?? '');
+        dateCellData.foldRight("", (dateData, previous) => dateData.time ?? '');
     return DateCellCalendarState(
       dateTypeOptionPB: dateTypeOptionPB,
       format: CalendarFormat.month,
       focusedDay: DateTime.now(),
       time: time,
-      calData: calData,
+      dateCellData: dateCellData,
       timeFormatError: none(),
       timeHintText: _timeHintText(dateTypeOptionPB),
     );
@@ -249,30 +254,30 @@ String _timeHintText(DateTypeOptionPB typeOption) {
       return LocaleKeys.document_date_timeHintTextInTwelveHour.tr();
     case TimeFormat.TwentyFourHour:
       return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr();
+    default:
+      return "";
   }
-  return "";
 }
 
-Option<CalendarData> calDataFromCellData(DateCellDataPB? cellData) {
+Option<DateCellData> calDataFromCellData(DateCellDataPB? cellData) {
   String? time = timeFromCellData(cellData);
-  Option<CalendarData> calData = none();
+  Option<DateCellData> dateData = none();
   if (cellData != null) {
     final timestamp = cellData.timestamp * 1000;
     final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
-    calData = Some(CalendarData(date: date, time: time));
+    dateData = Some(DateCellData(
+      date: date,
+      time: time,
+      includeTime: cellData.includeTime,
+    ));
   }
-  return calData;
-}
-
-$fixnum.Int64 timestampFromDateTime(DateTime dateTime) {
-  final timestamp = (dateTime.millisecondsSinceEpoch ~/ 1000);
-  return $fixnum.Int64(timestamp);
+  return dateData;
 }
 
 String? timeFromCellData(DateCellDataPB? cellData) {
-  String? time;
-  if (cellData?.hasTime() ?? false) {
-    time = cellData?.time;
+  if (cellData == null || !cellData.hasTime()) {
+    return null;
   }
-  return time;
+
+  return cellData.time;
 }

+ 8 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart

@@ -111,12 +111,14 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
       child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
         buildWhen: (p, c) => p != c,
         builder: (context, state) {
+          bool includeTime = state.dateCellData
+              .fold(() => false, (dateData) => dateData.includeTime);
           List<Widget> children = [
             Padding(
               padding: const EdgeInsets.symmetric(horizontal: 12.0),
               child: _buildCalendar(context),
             ),
-            if (state.dateTypeOptionPB.includeTime) ...[
+            if (includeTime) ...[
               const VSpace(12.0),
               _TimeTextField(
                 bloc: context.read<DateCellCalendarBloc>(),
@@ -206,7 +208,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
                 textStyle.textColor(Theme.of(context).disabledColor),
           ),
           selectedDayPredicate: (day) {
-            return state.calData.fold(
+            return state.dateCellData.fold(
               () => false,
               (dateData) => isSameDay(dateData.date, day),
             );
@@ -238,7 +240,10 @@ class _IncludeTimeButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
-      selector: (state) => state.dateTypeOptionPB.includeTime,
+      selector: (state) => state.dateCellData.fold(
+        () => false,
+        (dateData) => dateData.includeTime,
+      ),
       builder: (context, includeTime) {
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 12.0),

+ 1 - 0
frontend/rust-lib/flowy-database/src/event_handler.rs

@@ -517,6 +517,7 @@ pub(crate) async fn update_date_cell_handler(
   let cell_changeset = DateCellChangeset {
     date: data.date,
     time: data.time,
+    include_time: data.include_time,
     is_utc: data.is_utc,
   };
 

+ 4 - 3
frontend/rust-lib/flowy-database/src/services/cell/cell_operation.rs

@@ -39,7 +39,7 @@ pub trait CellDataChangeset: TypeOption {
   /// The changeset is able to parse into the concrete data struct if `TypeOption::CellChangeset`
   /// implements the `FromCellChangesetString` trait.
   /// For example,the SelectOptionCellChangeset,DateCellChangeset. etc.
-  ///  
+  ///
   fn apply_changeset(
     &self,
     changeset: <Self as TypeOption>::CellChangeset,
@@ -142,7 +142,7 @@ where
 
 /// Decode the opaque cell data from one field type to another using the corresponding `TypeOption`
 ///
-/// The cell data might become an empty string depends on the to_field_type's `TypeOption`   
+/// The cell data might become an empty string depends on the to_field_type's `TypeOption`
 /// support transform the from_field_type's cell data or not.
 ///
 /// # Arguments
@@ -252,6 +252,7 @@ pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevisi
   let cell_data = serde_json::to_string(&DateCellChangeset {
     date: Some(timestamp.to_string()),
     time: None,
+    include_time: Some(false),
     is_utc: true,
   })
   .unwrap();
@@ -279,7 +280,7 @@ pub fn delete_select_option_cell(
   CellRevision::new(data)
 }
 
-/// Deserialize the String into cell specific data type.  
+/// Deserialize the String into cell specific data type.
 pub trait FromCellString {
   fn from_cell_str(s: &str) -> FlowyResult<Self>
   where

+ 3 - 2
frontend/rust-lib/flowy-database/src/services/database_view/editor.rs

@@ -862,7 +862,8 @@ impl DatabaseViewEditor {
     let timestamp = date_cell
       .into_date_field_cell_data()
       .unwrap_or_default()
-      .into();
+      .timestamp
+      .unwrap_or_default();
 
     Some(CalendarEventPB {
       row_id: row_id.to_string(),
@@ -896,7 +897,7 @@ impl DatabaseViewEditor {
         // timestamp
         let timestamp = date_cell
           .into_date_field_cell_data()
-          .map(|date_cell_data| date_cell_data.0.unwrap_or_default())
+          .map(|date_cell_data| date_cell_data.timestamp.unwrap_or_default())
           .unwrap_or_default();
 
         (row_id, timestamp)

+ 116 - 20
frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_tests.rs

@@ -3,8 +3,9 @@ mod tests {
   use crate::entities::FieldType;
   use crate::services::cell::{CellDataChangeset, CellDataDecoder};
 
-  use crate::services::field::*;
-  // use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOptionPB, TimeFormat};
+  use crate::services::field::{
+    DateCellChangeset, DateFormat, DateTypeOptionPB, FieldBuilder, TimeFormat, TypeOptionCellData,
+  };
   use chrono::format::strftime::StrftimeItems;
   use chrono::{FixedOffset, NaiveDateTime};
   use database_model::FieldRevision;
@@ -18,16 +19,44 @@ mod tests {
       type_option.date_format = date_format;
       match date_format {
         DateFormat::Friendly => {
-          assert_date(&type_option, 1647251762, None, "Mar 14,2022", &field_rev);
+          assert_date(
+            &type_option,
+            1647251762,
+            None,
+            "Mar 14,2022",
+            false,
+            &field_rev,
+          );
         },
         DateFormat::US => {
-          assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev);
+          assert_date(
+            &type_option,
+            1647251762,
+            None,
+            "2022/03/14",
+            false,
+            &field_rev,
+          );
         },
         DateFormat::ISO => {
-          assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev);
+          assert_date(
+            &type_option,
+            1647251762,
+            None,
+            "2022-03-14",
+            false,
+            &field_rev,
+          );
         },
         DateFormat::Local => {
-          assert_date(&type_option, 1647251762, None, "03/14/2022", &field_rev);
+          assert_date(
+            &type_option,
+            1647251762,
+            None,
+            "03/14/2022",
+            false,
+            &field_rev,
+          );
         },
       }
     }
@@ -41,25 +70,56 @@ mod tests {
 
     for time_format in TimeFormat::iter() {
       type_option.time_format = time_format;
-      type_option.include_time = true;
       match time_format {
         TimeFormat::TwentyFourHour => {
-          assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev);
+          assert_date(
+            &type_option,
+            1653609600,
+            None,
+            "May 27,2022 00:00",
+            true,
+            &field_rev,
+          );
+          assert_date(
+            &type_option,
+            1653609600,
+            Some("9:00".to_owned()),
+            "May 27,2022 09:00",
+            true,
+            &field_rev,
+          );
           assert_date(
             &type_option,
             1653609600,
             Some("23:00".to_owned()),
             "May 27,2022 23:00",
+            true,
             &field_rev,
           );
         },
         TimeFormat::TwelveHour => {
-          assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev);
+          assert_date(
+            &type_option,
+            1653609600,
+            None,
+            "May 27,2022 12:00 AM",
+            true,
+            &field_rev,
+          );
+          assert_date(
+            &type_option,
+            1653609600,
+            Some("9:00 AM".to_owned()),
+            "May 27,2022 09:00 AM",
+            true,
+            &field_rev,
+          );
           assert_date(
             &type_option,
             1653609600,
             Some("11:23 pm".to_owned()),
             "May 27,2022 11:23 PM",
+            true,
             &field_rev,
           );
         },
@@ -72,14 +132,13 @@ mod tests {
     let type_option = DateTypeOptionPB::default();
     let field_type = FieldType::DateTime;
     let field_rev = FieldBuilder::from_field_type(&field_type).build();
-    assert_date(&type_option, "abc", None, "", &field_rev);
+    assert_date(&type_option, "abc", None, "", false, &field_rev);
   }
 
   #[test]
   #[should_panic]
   fn date_type_option_invalid_include_time_str_test() {
-    let mut type_option = DateTypeOptionPB::new();
-    type_option.include_time = true;
+    let type_option = DateTypeOptionPB::new();
     let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
 
     assert_date(
@@ -87,31 +146,46 @@ mod tests {
       1653609600,
       Some("1:".to_owned()),
       "May 27,2022 01:00",
+      true,
       &field_rev,
     );
   }
 
   #[test]
   fn date_type_option_empty_include_time_str_test() {
-    let mut type_option = DateTypeOptionPB::new();
-    type_option.include_time = true;
+    let type_option = DateTypeOptionPB::new();
     let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
 
     assert_date(
       &type_option,
       1653609600,
       Some("".to_owned()),
-      "May 27,2022",
+      "May 27,2022 00:00",
+      true,
+      &field_rev,
+    );
+  }
+
+  #[test]
+  fn date_type_midnight_include_time_str_test() {
+    let type_option = DateTypeOptionPB::new();
+    let field_type = FieldType::DateTime;
+    let field_rev = FieldBuilder::from_field_type(&field_type).build();
+    assert_date(
+      &type_option,
+      1653609600,
+      Some("00:00".to_owned()),
+      "May 27,2022 00:00",
+      true,
       &field_rev,
     );
   }
 
-  /// The default time format is TwentyFourHour, so the include_time_str  in twelve_hours_format will cause parser error.
+  /// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error.
   #[test]
   #[should_panic]
   fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() {
-    let mut type_option = DateTypeOptionPB::new();
-    type_option.include_time = true;
+    let type_option = DateTypeOptionPB::new();
     let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
 
     assert_date(
@@ -119,6 +193,25 @@ mod tests {
       1653609600,
       Some("1:00 am".to_owned()),
       "May 27,2022 01:00 AM",
+      true,
+      &field_rev,
+    );
+  }
+
+  // Attempting to parse include_time_str as TwelveHour when TwentyFourHour format is given should cause parser error.
+  #[test]
+  #[should_panic]
+  fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() {
+    let mut type_option = DateTypeOptionPB::new();
+    type_option.time_format = TimeFormat::TwelveHour;
+    let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
+
+    assert_date(
+      &type_option,
+      1653609600,
+      Some("20:00".to_owned()),
+      "May 27,2022 08:00 PM",
+      true,
       &field_rev,
     );
   }
@@ -154,17 +247,19 @@ mod tests {
     timestamp: T,
     include_time_str: Option<String>,
     expected_str: &str,
+    include_time: bool,
     field_rev: &FieldRevision,
   ) {
     let changeset = DateCellChangeset {
       date: Some(timestamp.to_string()),
       time: include_time_str,
       is_utc: false,
+      include_time: Some(include_time),
     };
     let (cell_str, _) = type_option.apply_changeset(changeset, None).unwrap();
 
     assert_eq!(
-      decode_cell_data(cell_str, type_option, field_rev),
+      decode_cell_data(cell_str, type_option, include_time, field_rev),
       expected_str.to_owned(),
     );
   }
@@ -172,13 +267,14 @@ mod tests {
   fn decode_cell_data(
     cell_str: String,
     type_option: &DateTypeOptionPB,
+    include_time: bool,
     field_rev: &FieldRevision,
   ) -> String {
     let decoded_data = type_option
       .decode_cell_str(cell_str, &FieldType::DateTime, field_rev)
       .unwrap();
     let decoded_data = type_option.convert_to_protobuf(decoded_data);
-    if type_option.include_time {
+    if include_time {
       format!("{} {}", decoded_data.date, decoded_data.time)
         .trim_end()
         .to_owned()

+ 56 - 79
frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option.rs

@@ -8,7 +8,7 @@ use crate::services::field::{
 };
 use bytes::Bytes;
 use chrono::format::strftime::StrftimeItems;
-use chrono::{NaiveDateTime, Timelike};
+use chrono::NaiveDateTime;
 use database_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
 use flowy_derive::ProtoBuf;
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
@@ -58,97 +58,59 @@ impl DateTypeOptionPB {
     Self::default()
   }
 
-  fn today_desc_from_timestamp<T: Into<i64>>(&self, timestamp: T) -> DateCellDataPB {
-    let timestamp = timestamp.into();
-    let native = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0);
-    if native.is_none() {
+  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;
+
+    let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0);
+    if naive.is_none() {
       return DateCellDataPB::default();
     }
-    let native = native.unwrap();
-    if native.timestamp() == 0 {
+    let naive = naive.unwrap();
+    if timestamp == 0 {
       return DateCellDataPB::default();
     }
-
-    let time = native.time();
-    let has_time = time.hour() != 0 || time.second() != 0;
-
-    let utc = self.utc_date_time_from_native(native);
     let fmt = self.date_format.format_str();
-    let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt)));
+    let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt)));
 
-    let mut time = "".to_string();
-    if has_time && self.include_time {
-      let fmt = format!(
-        "{}{}",
-        self.date_format.format_str(),
-        self.time_format.format_str()
-      );
-      time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, "");
-    }
+    let time = if include_time {
+      let fmt = self.time_format.format_str();
+      format!("{}", naive.format_with_items(StrftimeItems::new(&fmt)))
+    } else {
+      "".to_string()
+    };
 
-    let timestamp = native.timestamp();
     DateCellDataPB {
       date,
       time,
+      include_time,
       timestamp,
     }
   }
 
-  fn date_fmt(&self, time: &Option<String>) -> String {
-    if self.include_time {
-      match time.as_ref() {
-        None => self.date_format.format_str().to_string(),
-        Some(time_str) => {
-          if time_str.is_empty() {
-            self.date_format.format_str().to_string()
-          } else {
-            format!(
-              "{} {}",
-              self.date_format.format_str(),
-              self.time_format.format_str()
-            )
-          }
-        },
-      }
-    } else {
-      self.date_format.format_str().to_string()
-    }
-  }
-
   fn timestamp_from_utc_with_time(
     &self,
-    utc: &chrono::DateTime<chrono::Utc>,
-    time: &Option<String>,
+    naive_date: &NaiveDateTime,
+    time_str: &Option<String>,
   ) -> FlowyResult<i64> {
-    if let Some(time_str) = time.as_ref() {
+    if let Some(time_str) = time_str.as_ref() {
       if !time_str.is_empty() {
-        let date_str = format!(
-          "{}{}",
-          utc.format_with_items(StrftimeItems::new(self.date_format.format_str())),
-          &time_str
-        );
+        let naive_time =
+          chrono::NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
 
-        return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) {
-          Ok(native) => {
-            let utc = self.utc_date_time_from_native(native);
-            Ok(utc.timestamp())
+        match naive_time {
+          Ok(naive_time) => {
+            return Ok(naive_date.date().and_time(naive_time).timestamp());
           },
           Err(_e) => {
-            let msg = format!("Parse {} failed", date_str);
-            Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg))
+            let msg = format!("Parse {} failed", time_str);
+            return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg));
           },
         };
       }
     }
 
-    Ok(utc.timestamp())
-  }
-
-  fn utc_date_time_from_native(
-    &self,
-    naive: chrono::NaiveDateTime,
-  ) -> chrono::DateTime<chrono::Utc> {
-    chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
+    Ok(naive_date.timestamp())
   }
 }
 
@@ -181,25 +143,40 @@ impl CellDataChangeset for DateTypeOptionPB {
   fn apply_changeset(
     &self,
     changeset: <Self as TypeOption>::CellChangeset,
-    _type_cell_data: Option<TypeCellData>,
+    type_cell_data: Option<TypeCellData>,
   ) -> FlowyResult<(String, <Self as TypeOption>::CellData)> {
-    let cell_data = match changeset.date_timestamp() {
-      None => 0,
-      Some(date_timestamp) => match (self.include_time, changeset.time) {
+    let (timestamp, include_time) = match type_cell_data {
+      None => (None, false),
+      Some(type_cell_data) => {
+        let cell_data = DateCellData::from_cell_str(&type_cell_data.cell_str).unwrap_or_default();
+        (cell_data.timestamp, cell_data.include_time)
+      },
+    };
+
+    let include_time = match changeset.include_time {
+      None => include_time,
+      Some(include_time) => include_time,
+    };
+    let timestamp = match changeset.date_timestamp() {
+      None => timestamp,
+      Some(date_timestamp) => match (include_time, changeset.time) {
         (true, Some(time)) => {
           let time = Some(time.trim().to_uppercase());
-          let native = NaiveDateTime::from_timestamp_opt(date_timestamp, 0);
-          if let Some(native) = native {
-            let utc = self.utc_date_time_from_native(native);
-            self.timestamp_from_utc_with_time(&utc, &time)?
+          let naive = NaiveDateTime::from_timestamp_opt(date_timestamp, 0);
+          if let Some(naive) = naive {
+            Some(self.timestamp_from_utc_with_time(&naive, &time)?)
           } else {
-            date_timestamp
+            Some(date_timestamp)
           }
         },
-        _ => date_timestamp,
+        _ => Some(date_timestamp),
       },
     };
-    let date_cell_data = DateCellData(Some(cell_data));
+
+    let date_cell_data = DateCellData {
+      timestamp,
+      include_time,
+    };
     Ok((date_cell_data.to_string(), date_cell_data))
   }
 }
@@ -215,7 +192,7 @@ impl TypeOptionCellDataFilter for DateTypeOptionPB {
       return true;
     }
 
-    filter.is_visible(cell_data.0)
+    filter.is_visible(cell_data.timestamp)
   }
 }
 
@@ -225,7 +202,7 @@ impl TypeOptionCellDataCompare for DateTypeOptionPB {
     cell_data: &<Self as TypeOption>::CellData,
     other_cell_data: &<Self as TypeOption>::CellData,
   ) -> Ordering {
-    match (cell_data.0, other_cell_data.0) {
+    match (cell_data.timestamp, other_cell_data.timestamp) {
       (Some(left), Some(right)) => left.cmp(&right),
       (Some(_), None) => Ordering::Greater,
       (None, Some(_)) => Ordering::Less,

+ 80 - 17
frontend/rust-lib/flowy-database/src/services/field/type_options/date_type_option/date_type_option_entities.rs

@@ -1,3 +1,5 @@
+use std::fmt;
+
 use crate::entities::CellIdPB;
 use crate::services::cell::{
   CellProtobufBlobParser, DecodedCellData, FromCellChangesetString, FromCellString,
@@ -6,6 +8,7 @@ use crate::services::cell::{
 use bytes::Bytes;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::{internal_error, FlowyResult};
+use serde::de::Visitor;
 use serde::{Deserialize, Serialize};
 use strum_macros::EnumIter;
 
@@ -19,6 +22,9 @@ pub struct DateCellDataPB {
 
   #[pb(index = 3)]
   pub timestamp: i64,
+
+  #[pb(index = 4)]
+  pub include_time: bool,
 }
 
 #[derive(Clone, Debug, Default, ProtoBuf)]
@@ -32,7 +38,10 @@ pub struct DateChangesetPB {
   #[pb(index = 3, one_of)]
   pub time: Option<String>,
 
-  #[pb(index = 4)]
+  #[pb(index = 4, one_of)]
+  pub include_time: Option<bool>,
+
+  #[pb(index = 5)]
   pub is_utc: bool,
 }
 
@@ -40,6 +49,7 @@ pub struct DateChangesetPB {
 pub struct DateCellChangeset {
   pub date: Option<String>,
   pub time: Option<String>,
+  pub include_time: Option<bool>,
   pub is_utc: bool,
 }
 
@@ -71,18 +81,74 @@ impl ToCellChangesetString for DateCellChangeset {
   }
 }
 
-#[derive(Default, Clone, Debug)]
-pub struct DateCellData(pub Option<i64>);
-
-impl std::convert::From<DateCellData> for i64 {
-  fn from(timestamp: DateCellData) -> Self {
-    timestamp.0.unwrap_or(0)
-  }
+#[derive(Default, Clone, Debug, Serialize)]
+pub struct DateCellData {
+  pub timestamp: Option<i64>,
+  pub include_time: bool,
 }
 
-impl std::convert::From<DateCellData> for Option<i64> {
-  fn from(timestamp: DateCellData) -> Self {
-    timestamp.0
+impl<'de> serde::Deserialize<'de> for DateCellData {
+  fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
+  where
+    D: serde::Deserializer<'de>,
+  {
+    struct DateCellVisitor();
+
+    impl<'de> Visitor<'de> for DateCellVisitor {
+      type Value = DateCellData;
+
+      fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+        formatter.write_str(
+          "DateCellData with type: str containing either an integer timestamp or the JSON representation",
+        )
+      }
+
+      fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
+      where
+        E: serde::de::Error,
+      {
+        Ok(DateCellData {
+          timestamp: Some(value),
+          include_time: false,
+        })
+      }
+
+      fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
+      where
+        E: serde::de::Error,
+      {
+        self.visit_i64(value as i64)
+      }
+
+      fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+      where
+        M: serde::de::MapAccess<'de>,
+      {
+        let mut timestamp: Option<i64> = None;
+        let mut include_time: Option<bool> = None;
+
+        while let Some(key) = map.next_key()? {
+          match key {
+            "timestamp" => {
+              timestamp = map.next_value()?;
+            },
+            "include_time" => {
+              include_time = map.next_value()?;
+            },
+            _ => {},
+          }
+        }
+
+        let include_time = include_time.unwrap_or(false);
+
+        Ok(DateCellData {
+          timestamp,
+          include_time,
+        })
+      }
+    }
+
+    deserializer.deserialize_any(DateCellVisitor())
   }
 }
 
@@ -91,17 +157,14 @@ impl FromCellString for DateCellData {
   where
     Self: Sized,
   {
-    let num = s.parse::<i64>().ok();
-    Ok(DateCellData(num))
+    let result: DateCellData = serde_json::from_str(s).unwrap();
+    Ok(result)
   }
 }
 
 impl ToString for DateCellData {
   fn to_string(&self) -> String {
-    match self.0 {
-      None => "".to_string(),
-      Some(val) => val.to_string(),
-    }
+    serde_json::to_string(self).unwrap()
   }
 }
 

+ 15 - 0
frontend/rust-lib/flowy-database/src/services/field/type_options/text_type_option/text_tests.rs

@@ -22,6 +22,21 @@ mod tests {
       ),
       "Mar 14,2022"
     );
+
+    let data = DateCellData {
+      timestamp: Some(1647251762),
+      include_time: true,
+    };
+
+    assert_eq!(
+      stringify_cell_data(
+        data.to_string(),
+        &FieldType::RichText,
+        &field_type,
+        &field_rev
+      ),
+      "Mar 14,2022"
+    );
   }
 
   // Test parser the cell data which field's type is FieldType::SingleSelect to cell data

+ 1 - 0
frontend/rust-lib/flowy-database/tests/database/block_test/util.rs

@@ -43,6 +43,7 @@ impl DatabaseRowTestBuilder {
       date: Some(data.to_string()),
       time: None,
       is_utc: true,
+      include_time: Some(false),
     })
     .unwrap();
     let date_field = self.field_rev_with_type(&FieldType::DateTime);

+ 1 - 0
frontend/rust-lib/flowy-database/tests/database/field_test/util.rs

@@ -64,6 +64,7 @@ pub fn make_date_cell_string(s: &str) -> String {
     date: Some(s.to_string()),
     time: None,
     is_utc: true,
+    include_time: Some(false),
   })
   .unwrap()
 }