浏览代码

feat: created at and updated at field type (#2572)

* feat: created at and updated at field type

* style: context for rust asserts, change checks in flutter

* fix: mistake in if condition

* style: add comma end of array

* feat: created at and updated at field type

* fix: typo in const variable

* style: cargo fmt

* refactor: opti cell insert

* chore: remove redundant clone

* refactor: date type option

* fix: tauri build

---------

Co-authored-by: nathan <[email protected]>
Mohammad Zolfaghari 2 年之前
父节点
当前提交
9a213fa562
共有 43 个文件被更改,包括 598 次插入188 次删除
  1. 2 0
      frontend/appflowy_flutter/assets/translations/en.json
  2. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart
  3. 7 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart
  4. 10 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart
  5. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart
  6. 3 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart
  7. 5 1
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart
  8. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart
  9. 6 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart
  10. 4 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart
  11. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
  12. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart
  13. 2 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart
  14. 10 10
      frontend/appflowy_tauri/src-tauri/Cargo.lock
  15. 2 0
      frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts
  16. 14 2
      frontend/rust-lib/flowy-database2/src/entities/field_entities.rs
  17. 4 2
      frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs
  18. 2 0
      frontend/rust-lib/flowy-database2/src/entities/macros.rs
  19. 6 1
      frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs
  20. 24 8
      frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs
  21. 2 0
      frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs
  22. 38 14
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  23. 0 2
      frontend/rust-lib/flowy-database2/src/services/field/mod.rs
  24. 0 16
      frontend/rust-lib/flowy-database2/src/services/field/type_option_builder.rs
  25. 19 16
      frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs
  26. 26 12
      frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs
  27. 26 3
      frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs
  28. 10 5
      frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs
  29. 2 2
      frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs
  30. 1 1
      frontend/rust-lib/flowy-database2/src/services/filter/controller.rs
  31. 1 11
      frontend/rust-lib/flowy-database2/src/services/group/group_util.rs
  32. 2 2
      frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs
  33. 2 0
      frontend/rust-lib/flowy-database2/tests/database/block_test/mod.rs
  34. 53 0
      frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs
  35. 59 0
      frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs
  36. 87 5
      frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs
  37. 2 1
      frontend/rust-lib/flowy-database2/tests/database/database_editor.rs
  38. 10 0
      frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs
  39. 35 2
      frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs
  40. 49 32
      frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs
  41. 5 0
      frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs
  42. 57 38
      frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs
  43. 1 0
      frontend/rust-lib/flowy-database2/tests/database/mod.rs

+ 2 - 0
frontend/appflowy_flutter/assets/translations/en.json

@@ -280,6 +280,8 @@
       "textFieldName": "Text",
       "checkboxFieldName": "Checkbox",
       "dateFieldName": "Date",
+      "updatedAtFieldName": "Updated At",
+      "createdAtFieldName": "Created At",
       "numberFieldName": "Numbers",
       "singleSelectFieldName": "Select",
       "multiSelectFieldName": "Multiselect",

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

@@ -40,6 +40,8 @@ class CellControllerBuilder {
           cellDataPersistence: TextCellDataPersistence(cellId: _cellId),
         );
       case FieldType.DateTime:
+      case FieldType.UpdatedAt:
+      case FieldType.CreatedAt:
         final cellDataLoader = CellDataLoader(
           cellId: _cellId,
           parser: DateCellDataParser(),

+ 7 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart

@@ -353,7 +353,13 @@ class RowDataBuilder {
   }
 
   void insertDate(FieldInfo fieldInfo, DateTime date) {
-    assert(fieldInfo.fieldType == FieldType.DateTime);
+    assert(
+      [
+        FieldType.DateTime,
+        FieldType.UpdatedAt,
+        FieldType.CreatedAt,
+      ].contains(fieldInfo.fieldType),
+    );
     final timestamp = date.millisecondsSinceEpoch ~/ 1000;
     _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
   }

+ 10 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/filter/filter_service.dart

@@ -84,10 +84,19 @@ class FilterBackendService {
     required String fieldId,
     String? filterId,
     required DateFilterConditionPB condition,
+    required FieldType fieldType,
     int? start,
     int? end,
     int? timestamp,
   }) {
+    assert(
+      [
+        FieldType.DateTime,
+        FieldType.UpdatedAt,
+        FieldType.CreatedAt,
+      ].contains(fieldType),
+    );
+
     var filter = DateFilterPB();
     if (timestamp != null) {
       filter.timestamp = $fixnum.Int64(timestamp);
@@ -105,7 +114,7 @@ class FilterBackendService {
     return insertFilter(
       fieldId: fieldId,
       filterId: filterId,
-      fieldType: FieldType.DateTime,
+      fieldType: fieldType,
       data: filter.writeToBuffer(),
     );
   }

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart

@@ -362,6 +362,8 @@ Widget? _buildHeaderIcon(GroupData customData) {
       }
       break;
     case FieldType.DateTime:
+    case FieldType.UpdatedAt:
+    case FieldType.CreatedAt:
       break;
     case FieldType.MultiSelect:
       break;

+ 3 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/application/filter/filter_create_bloc.dart

@@ -93,11 +93,14 @@ class GridCreateFilterBloc
           condition: CheckboxFilterConditionPB.IsChecked,
         );
       case FieldType.DateTime:
+      case FieldType.UpdatedAt:
+      case FieldType.CreatedAt:
         final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
         return _filterBackendSvc.insertDateFilter(
           fieldId: fieldId,
           condition: DateFilterConditionPB.DateIs,
           timestamp: timestamp,
+          fieldType: field.fieldType,
         );
       case FieldType.MultiSelect:
         return _filterBackendSvc.insertSelectOptionFilter(

+ 5 - 1
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_info.dart

@@ -23,7 +23,11 @@ class FilterInfo {
   }
 
   DateFilterPB? dateFilter() {
-    if (filter.fieldType != FieldType.DateTime) {
+    if (![
+      FieldType.DateTime,
+      FieldType.UpdatedAt,
+      FieldType.CreatedAt,
+    ].contains(filter.fieldType)) {
       return null;
     }
     return DateFilterPB.fromBuffer(filter.data);

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/filter/filter_menu_item.dart

@@ -25,6 +25,8 @@ Widget buildFilterChoicechip(FilterInfo filterInfo) {
     case FieldType.Checkbox:
       return CheckboxFilterChoicechip(filterInfo: filterInfo);
     case FieldType.DateTime:
+    case FieldType.UpdatedAt:
+    case FieldType.CreatedAt:
       return DateFilterChoicechip(filterInfo: filterInfo);
     case FieldType.MultiSelect:
       return SelectOptionFilterChoicechip(filterInfo: filterInfo);

+ 6 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart

@@ -8,6 +8,8 @@ extension FieldTypeListExtension on FieldType {
       case FieldType.Checkbox:
         return "grid/field/checkbox";
       case FieldType.DateTime:
+      case FieldType.UpdatedAt:
+      case FieldType.CreatedAt:
         return "grid/field/date";
       case FieldType.MultiSelect:
         return "grid/field/multi_select";
@@ -31,6 +33,10 @@ extension FieldTypeListExtension on FieldType {
         return LocaleKeys.grid_field_checkboxFieldName.tr();
       case FieldType.DateTime:
         return LocaleKeys.grid_field_dateFieldName.tr();
+      case FieldType.UpdatedAt:
+        return LocaleKeys.grid_field_updatedAtFieldName.tr();
+      case FieldType.CreatedAt:
+        return LocaleKeys.grid_field_createdAtFieldName.tr();
       case FieldType.MultiSelect:
         return LocaleKeys.grid_field_multiSelectFieldName.tr();
       case FieldType.Number:

+ 4 - 0
frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart

@@ -73,6 +73,8 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
         ),
       );
     case FieldType.DateTime:
+    case FieldType.UpdatedAt:
+    case FieldType.CreatedAt:
       return DateTypeOptionWidgetBuilder(
         makeTypeOptionContextWithDataController<DateTypeOptionPB>(
           viewId: viewId,
@@ -202,6 +204,8 @@ TypeOptionContext<T>
         dataParser: CheckboxTypeOptionWidgetDataParser(),
       ) as TypeOptionContext<T>;
     case FieldType.DateTime:
+    case FieldType.UpdatedAt:
+    case FieldType.CreatedAt:
       return DateTypeOptionContext(
         dataController: dataController,
         dataParser: DateTypeOptionDataParser(),

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart

@@ -39,6 +39,8 @@ class CardCellBuilder<CustomCardData> {
           key: key,
         );
       case FieldType.DateTime:
+      case FieldType.UpdatedAt:
+      case FieldType.CreatedAt:
         return DateCardCell<CustomCardData>(
           renderHook: renderHook?.renderHook[FieldType.DateTime],
           cellControllerBuilder: cellControllerBuilder,

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart

@@ -34,6 +34,8 @@ class GridCellBuilder {
           key: key,
         );
       case FieldType.DateTime:
+      case FieldType.UpdatedAt:
+      case FieldType.CreatedAt:
         return GridDateCell(
           cellControllerBuilder: cellControllerBuilder,
           key: key,

+ 2 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_detail.dart

@@ -335,6 +335,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
     case FieldType.Checkbox:
       return null;
     case FieldType.DateTime:
+    case FieldType.UpdatedAt:
+    case FieldType.CreatedAt:
       return DateCellStyle(
         alignment: Alignment.centerLeft,
       );

+ 10 - 10
frontend/appflowy_tauri/src-tauri/Cargo.lock

@@ -99,7 +99,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
 [[package]]
 name = "appflowy-integrate"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "collab",
@@ -1023,7 +1023,7 @@ dependencies = [
 [[package]]
 name = "collab"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "bytes",
@@ -1040,7 +1040,7 @@ dependencies = [
 [[package]]
 name = "collab-client-ws"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "bytes",
  "collab-sync",
@@ -1058,7 +1058,7 @@ dependencies = [
 [[package]]
 name = "collab-database"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1083,7 +1083,7 @@ dependencies = [
 [[package]]
 name = "collab-derive"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1095,7 +1095,7 @@ dependencies = [
 [[package]]
 name = "collab-document"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "collab",
@@ -1112,7 +1112,7 @@ dependencies = [
 [[package]]
 name = "collab-folder"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "collab",
@@ -1130,7 +1130,7 @@ dependencies = [
 [[package]]
 name = "collab-persistence"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "bincode",
  "chrono",
@@ -1150,7 +1150,7 @@ dependencies = [
 [[package]]
 name = "collab-plugins"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -1180,7 +1180,7 @@ dependencies = [
 [[package]]
 name = "collab-sync"
 version = "0.1.0"
-source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=6052d5#6052d509982f705c6d43a859c937a722a8c3358b"
+source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d6af3a#d6af3a7662a57202e4e5b0a297c28757a18f3372"
 dependencies = [
  "bytes",
  "collab",

+ 2 - 0
frontend/appflowy_tauri/src/appflowy_app/stores/effects/database/cell/controller_builder.ts

@@ -45,6 +45,8 @@ export class CellControllerBuilder {
       case FieldType.Number:
         return this.makeNumberCellController();
       case FieldType.DateTime:
+      case FieldType.UpdatedAt:
+      case FieldType.CreatedAt:
         return this.makeDateCellController();
       case FieldType.URL:
         return this.makeURLCellController();

+ 14 - 2
frontend/rust-lib/flowy-database2/src/entities/field_entities.rs

@@ -492,6 +492,8 @@ pub enum FieldType {
   Checkbox = 5,
   URL = 6,
   Checklist = 7,
+  UpdatedAt = 8,
+  CreatedAt = 9,
 }
 
 pub const RICH_TEXT_FIELD: FieldType = FieldType::RichText;
@@ -502,6 +504,8 @@ pub const MULTI_SELECT_FIELD: FieldType = FieldType::MultiSelect;
 pub const CHECKBOX_FIELD: FieldType = FieldType::Checkbox;
 pub const URL_FIELD: FieldType = FieldType::URL;
 pub const CHECKLIST_FIELD: FieldType = FieldType::Checklist;
+pub const UPDATED_AT_FIELD: FieldType = FieldType::UpdatedAt;
+pub const CREATED_AT_FIELD: FieldType = FieldType::CreatedAt;
 
 impl std::default::Default for FieldType {
   fn default() -> Self {
@@ -529,9 +533,13 @@ impl From<&FieldType> for FieldType {
 }
 
 impl FieldType {
+  pub fn value(&self) -> i64 {
+    self.clone().into()
+  }
+
   pub fn default_cell_width(&self) -> i32 {
     match self {
-      FieldType::DateTime => 180,
+      FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => 180,
       _ => 150,
     }
   }
@@ -546,6 +554,8 @@ impl FieldType {
       FieldType::Checkbox => "Checkbox",
       FieldType::URL => "URL",
       FieldType::Checklist => "Checklist",
+      FieldType::UpdatedAt => "Updated At",
+      FieldType::CreatedAt => "Created At",
     };
     s.to_string()
   }
@@ -563,7 +573,7 @@ impl FieldType {
   }
 
   pub fn is_date(&self) -> bool {
-    self == &DATE_FIELD
+    self == &DATE_FIELD || self == &UPDATED_AT_FIELD || self == &CREATED_AT_FIELD
   }
 
   pub fn is_single_select(&self) -> bool {
@@ -605,6 +615,8 @@ impl From<FieldType> for i64 {
       FieldType::Checkbox => 5,
       FieldType::URL => 6,
       FieldType::Checklist => 7,
+      FieldType::UpdatedAt => 8,
+      FieldType::CreatedAt => 9,
     }
   }
 }

+ 4 - 2
frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs

@@ -35,7 +35,9 @@ impl std::convert::From<&Filter> for FilterPB {
     let bytes: Bytes = match filter.field_type {
       FieldType::RichText => TextFilterPB::from(filter).try_into().unwrap(),
       FieldType::Number => NumberFilterPB::from(filter).try_into().unwrap(),
-      FieldType::DateTime => DateFilterPB::from(filter).try_into().unwrap(),
+      FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
+        DateFilterPB::from(filter).try_into().unwrap()
+      },
       FieldType::SingleSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(),
       FieldType::MultiSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(),
       FieldType::Checklist => ChecklistFilterPB::from(filter).try_into().unwrap(),
@@ -198,7 +200,7 @@ impl TryInto<AlterFilterParams> for AlterFilterPayloadPB {
         condition = filter.condition as u8;
         content = filter.content;
       },
-      FieldType::DateTime => {
+      FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
         let filter = DateFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?;
         condition = filter.condition as u8;
         content = DateFilterContentPB {

+ 2 - 0
frontend/rust-lib/flowy-database2/src/entities/macros.rs

@@ -12,6 +12,8 @@ macro_rules! impl_into_field_type {
           5 => FieldType::Checkbox,
           6 => FieldType::URL,
           7 => FieldType::Checklist,
+          8 => FieldType::UpdatedAt,
+          9 => FieldType::CreatedAt,
           _ => {
             tracing::error!("Can't parser FieldType from value: {}", ty);
             FieldType::RichText

+ 6 - 1
frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs

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

+ 24 - 8
frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs

@@ -3,6 +3,7 @@ use std::fmt::Debug;
 
 use collab_database::fields::Field;
 use collab_database::rows::{get_field_type_from_cell, Cell, Cells};
+use lib_infra::util::timestamp;
 
 use flowy_error::{ErrorCode, FlowyError, FlowyResult};
 
@@ -205,11 +206,11 @@ pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
   apply_cell_changeset(s, None, field, None).unwrap()
 }
 
-pub fn insert_date_cell(timestamp: i64, 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.to_string()),
     time: None,
-    include_time: Some(false),
+    include_time,
     timezone_id: None,
   })
   .unwrap();
@@ -299,6 +300,7 @@ pub struct CellBuilder<'a> {
 }
 
 impl<'a> CellBuilder<'a> {
+  /// Build list of Cells from HashMap of cell string by field id.
   pub fn with_cells(cell_by_field_id: HashMap<String, String>, fields: &'a [Field]) -> Self {
     let field_maps = fields
       .into_iter()
@@ -306,7 +308,7 @@ impl<'a> CellBuilder<'a> {
       .collect::<HashMap<String, &Field>>();
 
     let mut cells = Cells::new();
-    for (field_id, cell_str) in cell_by_field_id {
+    for (field_id, cell_str) in cell_by_field_id.clone() {
       if let Some(field) = field_maps.get(&field_id) {
         let field_type = FieldType::from(field.field_type);
         match field_type {
@@ -318,9 +320,9 @@ impl<'a> CellBuilder<'a> {
               cells.insert(field_id, insert_number_cell(num, field));
             }
           },
-          FieldType::DateTime => {
+          FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
             if let Ok(timestamp) = cell_str.parse::<i64>() {
-              cells.insert(field_id, insert_date_cell(timestamp, field));
+              cells.insert(field_id, insert_date_cell(timestamp, Some(false), field));
             }
           },
           FieldType::SingleSelect | FieldType::MultiSelect => {
@@ -345,6 +347,19 @@ impl<'a> CellBuilder<'a> {
       }
     }
 
+    // Auto insert the cell data if the field is not in the cell_by_field_id.
+    // Currently, the auto fill field type is `UpdatedAt` or `CreatedAt`.
+    for field in fields {
+      if !cell_by_field_id.contains_key(&field.id) {
+        let field_type = FieldType::from(field.field_type);
+        if field_type == FieldType::UpdatedAt || field_type == FieldType::CreatedAt {
+          cells.insert(
+            field.id.clone(),
+            insert_date_cell(timestamp(), Some(true), field),
+          );
+        }
+      }
+    }
     CellBuilder { cells, field_maps }
   }
 
@@ -400,9 +415,10 @@ impl<'a> CellBuilder<'a> {
     match self.field_maps.get(&field_id.to_owned()) {
       None => tracing::warn!("Can't find the date field with id: {}", field_id),
       Some(field) => {
-        self
-          .cells
-          .insert(field_id.to_owned(), insert_date_cell(timestamp, field));
+        self.cells.insert(
+          field_id.to_owned(),
+          insert_date_cell(timestamp, Some(false), field),
+        );
       },
     }
   }

+ 2 - 0
frontend/rust-lib/flowy-database2/src/services/cell/type_cell_data.rs

@@ -82,6 +82,8 @@ impl TypeCellData {
 
   pub fn is_date(&self) -> bool {
     self.field_type == FieldType::DateTime
+      || self.field_type == FieldType::UpdatedAt
+      || self.field_type == FieldType::CreatedAt
   }
 
   pub fn is_single_select(&self) -> bool {

+ 38 - 14
frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs

@@ -23,15 +23,15 @@ use crate::entities::{
 };
 use crate::notification::{send_notification, DatabaseNotification};
 use crate::services::cell::{
-  apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellBuilder, CellCache, ToCellChangeset,
+  apply_cell_changeset, get_cell_protobuf, insert_date_cell, AnyTypeCache, CellBuilder, CellCache,
+  ToCellChangeset,
 };
 use crate::services::database::util::database_view_setting_pb_from_view;
 use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews};
 use crate::services::field::{
-  default_type_option_data_for_type, 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, SelectOptionCellChangeset, SelectOptionIds, TypeOptionCellDataHandler,
-  TypeOptionCellExt,
+  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, SelectOptionCellChangeset,
+  SelectOptionIds, TypeOptionCellDataHandler, TypeOptionCellExt,
 };
 use crate::services::filter::Filter;
 use crate::services::group::{default_group_setting, GroupSetting, RowChangeset};
@@ -237,7 +237,7 @@ impl DatabaseEditor {
         let old_type_option = field.get_any_type_option(old_field_type.clone());
         let new_type_option = field
           .get_any_type_option(new_field_type)
-          .unwrap_or_else(|| default_type_option_data_for_type(new_field_type));
+          .unwrap_or_else(|| default_type_option_data_from_type(new_field_type));
 
         let transformed_type_option = transform_type_option(
           &new_type_option,
@@ -352,7 +352,7 @@ impl DatabaseEditor {
   ) -> (Field, Bytes) {
     let name = field_type.default_name();
     let type_option_data = match type_option_data {
-      None => default_type_option_data_for_type(field_type),
+      None => default_type_option_data_from_type(field_type),
       Some(type_option_data) => type_option_data_from_pb_or_default(type_option_data, field_type),
     };
     let (index, field) =
@@ -492,9 +492,23 @@ impl DatabaseEditor {
   ) -> FlowyResult<()> {
     // Get the old row before updating the cell. It would be better to get the old cell
     let old_row = { self.database.lock().get_row(&row_id) };
+
+    // Get all the updated_at fields. We will update all of them.
+    let updated_at_fields = self
+      .database
+      .lock()
+      .get_fields(view_id, None)
+      .into_iter()
+      .filter(|f| FieldType::from(f.field_type) == FieldType::UpdatedAt)
+      .collect::<Vec<Field>>();
+
     self.database.lock().update_row(&row_id, |row_update| {
       row_update.update_cells(|cell_update| {
-        cell_update.insert(field_id, new_cell);
+        let mut cells_update = cell_update.insert(field_id, new_cell);
+        for field in &updated_at_fields {
+          cells_update =
+            cells_update.insert(&field.id, insert_date_cell(timestamp(), Some(true), field));
+        }
       });
     });
 
@@ -505,12 +519,22 @@ impl DatabaseEditor {
       }
     }
 
-    notify_did_update_cell(vec![CellChangesetNotifyPB {
-      view_id: view_id.to_string(),
-      row_id: row_id.into_inner(),
-      field_id: field_id.to_string(),
-    }])
-    .await;
+    // Collect all the updated field's id. Notify the frontend that all of them have been updated.
+    let mut updated_field_ids = updated_at_fields
+      .into_iter()
+      .map(|field| field.id)
+      .collect::<Vec<String>>();
+    updated_field_ids.push(field_id.to_string());
+    let changeset = updated_field_ids
+      .into_iter()
+      .map(|field_id| CellChangesetNotifyPB {
+        view_id: view_id.to_string(),
+        row_id: row_id.clone().into_inner(),
+        field_id,
+      })
+      .collect();
+    notify_did_update_cell(changeset).await;
+
     Ok(())
   }
 

+ 0 - 2
frontend/rust-lib/flowy-database2/src/services/field/mod.rs

@@ -1,9 +1,7 @@
 mod field_builder;
 mod field_operation;
-mod type_option_builder;
 mod type_options;
 
 pub use field_builder::*;
 pub use field_operation::*;
-pub use type_option_builder::*;
 pub use type_options::*;

+ 0 - 16
frontend/rust-lib/flowy-database2/src/services/field/type_option_builder.rs

@@ -1,16 +0,0 @@
-use crate::entities::FieldType;
-use crate::services::field::type_options::*;
-use collab_database::fields::TypeOptionData;
-
-pub fn default_type_option_data_from_type(field_type: &FieldType) -> TypeOptionData {
-  match field_type {
-    FieldType::RichText => RichTextTypeOption::default().into(),
-    FieldType::Number => NumberTypeOption::default().into(),
-    FieldType::DateTime => DateTypeOption::default().into(),
-    FieldType::SingleSelect => SingleSelectTypeOption::default().into(),
-    FieldType::MultiSelect => MultiSelectTypeOption::default().into(),
-    FieldType::Checkbox => CheckboxTypeOption::default().into(),
-    FieldType::URL => URLTypeOption::default().into(),
-    FieldType::Checklist => ChecklistTypeOption::default().into(),
-  }
-}

+ 19 - 16
frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs

@@ -14,7 +14,7 @@ mod tests {
 
   #[test]
   fn date_type_option_date_format_test() {
-    let mut type_option = DateTypeOption::default();
+    let mut type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
     for date_format in DateFormat::iter() {
       type_option.date_format = date_format;
@@ -95,7 +95,7 @@ mod tests {
 
   #[test]
   fn date_type_option_different_time_format_test() {
-    let mut type_option = DateTypeOption::default();
+    let mut type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
 
     for time_format in TimeFormat::iter() {
@@ -183,8 +183,8 @@ mod tests {
 
   #[test]
   fn date_type_option_invalid_date_str_test() {
-    let type_option = DateTypeOption::default();
     let field_type = FieldType::DateTime;
+    let type_option = DateTypeOption::new(field_type.clone());
     let field = FieldBuilder::from_field_type(field_type).build();
     assert_date(
       &type_option,
@@ -203,8 +203,9 @@ mod tests {
   #[test]
   #[should_panic]
   fn date_type_option_invalid_include_time_str_test() {
-    let type_option = DateTypeOption::new();
-    let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
+    let field_type = FieldType::DateTime;
+    let type_option = DateTypeOption::new(field_type.clone());
+    let field = FieldBuilder::from_field_type(field_type).build();
 
     assert_date(
       &type_option,
@@ -223,8 +224,9 @@ mod tests {
   #[test]
   #[should_panic]
   fn date_type_option_empty_include_time_str_test() {
-    let type_option = DateTypeOption::new();
-    let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
+    let field_type = FieldType::DateTime;
+    let type_option = DateTypeOption::new(field_type.clone());
+    let field = FieldBuilder::from_field_type(field_type).build();
 
     assert_date(
       &type_option,
@@ -242,8 +244,8 @@ mod tests {
 
   #[test]
   fn date_type_midnight_include_time_str_test() {
-    let type_option = DateTypeOption::new();
     let field_type = FieldType::DateTime;
+    let type_option = DateTypeOption::new(field_type.clone());
     let field = FieldBuilder::from_field_type(field_type).build();
     assert_date(
       &type_option,
@@ -264,7 +266,7 @@ mod tests {
   #[test]
   #[should_panic]
   fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() {
-    let type_option = DateTypeOption::new();
+    let type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
     assert_date(
       &type_option,
@@ -285,9 +287,10 @@ mod tests {
   #[test]
   #[should_panic]
   fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() {
-    let mut type_option = DateTypeOption::new();
+    let field_type = FieldType::DateTime;
+    let mut type_option = DateTypeOption::new(field_type.clone());
     type_option.time_format = TimeFormat::TwelveHour;
-    let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
+    let field = FieldBuilder::from_field_type(field_type).build();
 
     assert_date(
       &type_option,
@@ -335,7 +338,7 @@ mod tests {
   #[test]
   #[should_panic]
   fn update_date_keep_time() {
-    let type_option = DateTypeOption::new();
+    let type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
 
     let old_cell_data = initialize_date_cell(
@@ -363,7 +366,7 @@ mod tests {
 
   #[test]
   fn update_time_keep_date() {
-    let type_option = DateTypeOption::new();
+    let type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
 
     let old_cell_data = initialize_date_cell(
@@ -391,7 +394,7 @@ mod tests {
 
   #[test]
   fn timezone_no_daylight_saving_time() {
-    let type_option = DateTypeOption::new();
+    let type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
 
     assert_date(
@@ -422,7 +425,7 @@ mod tests {
 
   #[test]
   fn timezone_with_daylight_saving_time() {
-    let type_option = DateTypeOption::new();
+    let type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
 
     assert_date(
@@ -453,7 +456,7 @@ mod tests {
 
   #[test]
   fn change_timezone() {
-    let type_option = DateTypeOption::new();
+    let type_option = DateTypeOption::new(FieldType::DateTime);
     let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
 
     let old_cell_data = initialize_date_cell(

+ 26 - 12
frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs

@@ -1,8 +1,9 @@
 use crate::entities::{DateCellDataPB, DateFilterPB, FieldType};
 use crate::services::cell::{CellDataChangeset, CellDataDecoder};
 use crate::services::field::{
-  default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption,
-  TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform,
+  default_order, DateCellChangeset, DateCellData, DateCellDataWrapper, DateFormat, TimeFormat,
+  TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter,
+  TypeOptionTransform,
 };
 use chrono::format::strftime::StrftimeItems;
 use chrono::{DateTime, Local, NaiveDateTime, NaiveTime, Offset, TimeZone};
@@ -15,11 +16,14 @@ use serde::{Deserialize, Serialize};
 use std::cmp::Ordering;
 use std::str::FromStr;
 
-// Date
-#[derive(Clone, Debug, Default, Serialize, Deserialize)]
+/// The [DateTypeOption] is used by [FieldType::Date], [FieldType::UpdatedAt], and [FieldType::CreatedAt].
+/// So, storing the field type is necessary to distinguish the field type.
+/// Most of the cases, each [FieldType] has its own [TypeOption] implementation.
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub struct DateTypeOption {
   pub date_format: DateFormat,
   pub time_format: TimeFormat,
+  pub field_type: FieldType,
 }
 
 impl TypeOption for DateTypeOption {
@@ -39,9 +43,14 @@ impl From<TypeOptionData> for DateTypeOption {
       .get_i64_value("time_format")
       .map(TimeFormat::from)
       .unwrap_or_default();
+    let field_type = data
+      .get_i64_value("field_type")
+      .map(FieldType::from)
+      .unwrap_or(FieldType::DateTime);
     Self {
       date_format,
       time_format,
+      field_type,
     }
   }
 }
@@ -51,6 +60,7 @@ impl From<DateTypeOption> for TypeOptionData {
     TypeOptionDataBuilder::new()
       .insert_i64_value("data_format", data.date_format.value())
       .insert_i64_value("time_format", data.time_format.value())
+      .insert_i64_value("field_type", data.field_type.value())
       .build()
   }
 }
@@ -69,9 +79,12 @@ impl TypeOptionCellData for DateTypeOption {
 }
 
 impl DateTypeOption {
-  #[allow(dead_code)]
-  pub fn new() -> Self {
-    Self::default()
+  pub fn new(field_type: FieldType) -> Self {
+    Self {
+      date_format: Default::default(),
+      time_format: Default::default(),
+      field_type,
+    }
   }
 
   fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB {
@@ -179,8 +192,8 @@ impl CellDataChangeset for DateTypeOption {
     // old date cell data
     let (previous_timestamp, include_time, timezone_id) = match cell {
       None => (None, false, "".to_owned()),
-      Some(type_cell_data) => {
-        let cell_data = DateCellData::from(&type_cell_data);
+      Some(cell) => {
+        let cell_data = DateCellData::from(&cell);
         (
           cell_data.timestamp,
           cell_data.include_time,
@@ -201,7 +214,6 @@ impl CellDataChangeset for DateTypeOption {
     // in the changeset without an accompanying time string, the old timestamp
     // will simply be overwritten. Meaning, in order to change the day without
     // changing the time, the old time string should be passed in as well.
-
     let changeset_timestamp = changeset.date_timestamp();
 
     // parse the time string, which is in the timezone corresponding to
@@ -241,12 +253,14 @@ impl CellDataChangeset for DateTypeOption {
       changeset_timestamp,
     );
 
-    let date_cell_data = DateCellData {
+    let cell_data = DateCellData {
       timestamp,
       include_time,
       timezone_id,
     };
-    Ok((Cell::from(date_cell_data.clone()), date_cell_data))
+
+    let cell_wrapper: DateCellDataWrapper = (self.field_type.clone(), cell_data.clone()).into();
+    Ok((Cell::from(cell_wrapper), cell_data))
   }
 }
 

+ 26 - 3
frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs

@@ -70,13 +70,29 @@ impl From<&Cell> for DateCellData {
   }
 }
 
-impl From<DateCellData> for Cell {
-  fn from(data: DateCellData) -> Self {
+/// 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 {
       Some(timestamp) => timestamp.to_string(),
       None => "".to_owned(),
     };
-    new_cell_builder(FieldType::DateTime)
+    // Most of the case, don't use these keys in other places. Otherwise, we should define
+    // constants for them.
+    new_cell_builder(field_type)
       .insert_str_value(CELL_DATA, timestamp_string)
       .insert_bool_value("include_time", data.include_time)
       .insert_str_value("timezone_id", data.timezone_id)
@@ -84,6 +100,13 @@ impl From<DateCellData> for Cell {
   }
 }
 
+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

+ 10 - 5
frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs

@@ -146,7 +146,7 @@ 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::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
       DateTypeOptionPB::try_from(bytes).map(|pb| DateTypeOption::from(pb).into())
     },
     FieldType::SingleSelect => {
@@ -164,7 +164,7 @@ pub fn type_option_data_from_pb_or_default<T: Into<Bytes>>(
     },
   };
 
-  result.unwrap_or_else(|_| default_type_option_data_for_type(field_type))
+  result.unwrap_or_else(|_| default_type_option_data_from_type(field_type))
 }
 
 pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> Bytes {
@@ -181,7 +181,7 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
         .try_into()
         .unwrap()
     },
-    FieldType::DateTime => {
+    FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
       let date_type_option: DateTypeOption = type_option.into();
       DateTypeOptionPB::from(date_type_option).try_into().unwrap()
     },
@@ -216,11 +216,16 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) ->
   }
 }
 
-pub fn default_type_option_data_for_type(field_type: &FieldType) -> TypeOptionData {
+pub fn default_type_option_data_from_type(field_type: &FieldType) -> TypeOptionData {
   match field_type {
     FieldType::RichText => RichTextTypeOption::default().into(),
     FieldType::Number => NumberTypeOption::default().into(),
-    FieldType::DateTime => DateTypeOption::default().into(),
+    FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => DateTypeOption {
+      date_format: Default::default(),
+      time_format: Default::default(),
+      field_type: field_type.clone(),
+    }
+    .into(),
     FieldType::SingleSelect => SingleSelectTypeOption::default().into(),
     FieldType::MultiSelect => MultiSelectTypeOption::default().into(),
     FieldType::Checkbox => CheckboxTypeOption::default().into(),

+ 2 - 2
frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs

@@ -350,7 +350,7 @@ impl<'a> TypeOptionCellExt<'a> {
             self.cell_data_cache.clone(),
           )
         }),
-      FieldType::DateTime => self
+      FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => self
         .field
         .get_type_option::<DateTypeOption>(field_type)
         .map(|type_option| {
@@ -470,7 +470,7 @@ fn get_type_option_transform_handler(
     FieldType::Number => {
       Box::new(NumberTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
     },
-    FieldType::DateTime => {
+    FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
       Box::new(DateTypeOption::from(type_option_data)) as Box<dyn TypeOptionTransformHandler>
     },
     FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data))

+ 1 - 1
frontend/rust-lib/flowy-database2/src/services/filter/controller.rs

@@ -323,7 +323,7 @@ impl FilterController {
             .write()
             .insert(field_id, NumberFilterPB::from_filter(filter.as_ref()));
         },
-        FieldType::DateTime => {
+        FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
           self
             .cell_filter_cache
             .write()

+ 1 - 11
frontend/rust-lib/flowy-database2/src/services/group/group_util.rs

@@ -139,17 +139,7 @@ pub fn find_new_grouping_field(
 ///
 pub fn default_group_setting(field: &Field) -> GroupSetting {
   let field_id = field.id.clone();
-  let field_type = FieldType::from(field.field_type);
-  match field_type {
-    FieldType::RichText => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-    FieldType::Number => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-    FieldType::DateTime => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-    FieldType::SingleSelect => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-    FieldType::MultiSelect => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-    FieldType::Checklist => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-    FieldType::Checkbox => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-    FieldType::URL => GroupSetting::new(field_id, field.field_type, "".to_owned()),
-  }
+  GroupSetting::new(field_id, field.field_type, "".to_owned())
 }
 
 pub fn make_no_status_group(field: &Field) -> Group {

+ 2 - 2
frontend/rust-lib/flowy-database2/src/services/share/csv/import.rs

@@ -1,6 +1,6 @@
 use crate::entities::FieldType;
 use crate::services::cell::CellBuilder;
-use crate::services::field::default_type_option_data_for_type;
+use crate::services::field::default_type_option_data_from_type;
 use collab_database::database::{gen_database_id, gen_database_view_id, gen_field_id, gen_row_id};
 use collab_database::fields::Field;
 use collab_database::rows::CreateRowParams;
@@ -73,7 +73,7 @@ fn database_from_fields_and_rows(fields_and_rows: FieldsRows) -> CreateDatabaseP
         Ok(field) => field,
         Err(_) => {
           let field_type = FieldType::RichText;
-          let type_option_data = default_type_option_data_for_type(&field_type);
+          let type_option_data = default_type_option_data_from_type(&field_type);
           let is_primary = index == 0;
           Field::new(
             gen_field_id(),

+ 2 - 0
frontend/rust-lib/flowy-database2/tests/database/block_test/mod.rs

@@ -0,0 +1,2 @@
+mod row_test;
+mod script;

+ 53 - 0
frontend/rust-lib/flowy-database2/tests/database/block_test/row_test.rs

@@ -0,0 +1,53 @@
+use crate::database::block_test::script::DatabaseRowTest;
+use crate::database::block_test::script::RowScript::*;
+use flowy_database2::entities::FieldType;
+use flowy_database2::services::field::DateCellData;
+
+#[tokio::test]
+async fn set_created_at_field_on_create_row() {
+  let mut test = DatabaseRowTest::new().await;
+  let row_count = test.rows.len();
+
+  let before_create_timestamp = chrono::offset::Utc::now().timestamp();
+  test
+    .run_scripts(vec![CreateEmptyRow, AssertRowCount(row_count + 1)])
+    .await;
+  let after_create_timestamp = chrono::offset::Utc::now().timestamp();
+
+  let mut rows = test.rows.clone();
+  rows.sort_by(|r1, r2| r1.created_at.cmp(&r2.created_at));
+  let row = rows.last().unwrap();
+
+  let fields = test.fields.clone();
+  let created_at_field = fields
+    .iter()
+    .find(|&f| FieldType::from(f.field_type) == FieldType::CreatedAt)
+    .unwrap();
+  let cell = row.cells.cell_for_field_id(&created_at_field.id).unwrap();
+  let created_at_timestamp = DateCellData::from(cell).timestamp.unwrap();
+
+  assert!(
+    created_at_timestamp >= before_create_timestamp
+      && created_at_timestamp <= after_create_timestamp,
+    "timestamp: {}, before: {}, after: {}",
+    created_at_timestamp,
+    before_create_timestamp,
+    after_create_timestamp
+  );
+
+  let updated_at_field = fields
+    .iter()
+    .find(|&f| FieldType::from(f.field_type) == FieldType::UpdatedAt)
+    .unwrap();
+  let cell = row.cells.cell_for_field_id(&updated_at_field.id).unwrap();
+  let updated_at_timestamp = DateCellData::from(cell).timestamp.unwrap();
+
+  assert!(
+    updated_at_timestamp >= before_create_timestamp
+      && updated_at_timestamp <= after_create_timestamp,
+    "timestamp: {}, before: {}, after: {}",
+    updated_at_timestamp,
+    before_create_timestamp,
+    after_create_timestamp
+  );
+}

+ 59 - 0
frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs

@@ -0,0 +1,59 @@
+use crate::database::database_editor::DatabaseEditorTest;
+use flowy_database2::entities::CreateRowParams;
+
+pub enum RowScript {
+  CreateEmptyRow,
+  AssertRowCount(usize),
+}
+
+pub struct DatabaseRowTest {
+  inner: DatabaseEditorTest,
+}
+
+impl DatabaseRowTest {
+  pub async fn new() -> Self {
+    let editor_test = DatabaseEditorTest::new_grid().await;
+    Self { inner: editor_test }
+  }
+
+  pub async fn run_scripts(&mut self, scripts: Vec<RowScript>) {
+    for script in scripts {
+      self.run_script(script).await;
+    }
+  }
+
+  pub async fn run_script(&mut self, script: RowScript) {
+    match script {
+      RowScript::CreateEmptyRow => {
+        let params = CreateRowParams {
+          view_id: self.view_id.clone(),
+          start_row_id: None,
+          group_id: None,
+          cell_data_by_field_id: None,
+        };
+        let row_order = self.editor.create_row(params).await.unwrap().unwrap();
+        self
+          .row_by_row_id
+          .insert(row_order.id.to_string(), row_order.into());
+        self.rows = self.get_rows().await;
+      },
+      RowScript::AssertRowCount(expected_row_count) => {
+        assert_eq!(expected_row_count, self.rows.len());
+      },
+    }
+  }
+}
+
+impl std::ops::Deref for DatabaseRowTest {
+  type Target = DatabaseEditorTest;
+
+  fn deref(&self) -> &Self::Target {
+    &self.inner
+  }
+}
+
+impl std::ops::DerefMut for DatabaseRowTest {
+  fn deref_mut(&mut self) -> &mut Self::Target {
+    &mut self.inner
+  }
+}

+ 87 - 5
frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs

@@ -1,8 +1,8 @@
 use flowy_database2::entities::{CellChangesetPB, FieldType};
 use flowy_database2::services::cell::ToCellChangeset;
 use flowy_database2::services::field::{
-  ChecklistTypeOption, MultiSelectTypeOption, SelectOptionCellChangeset, SingleSelectTypeOption,
-  StrCellData, URLCellData,
+  ChecklistTypeOption, DateCellData, MultiSelectTypeOption, SelectOptionCellChangeset,
+  SingleSelectTypeOption, StrCellData, URLCellData,
 };
 
 use crate::database::cell_test::script::CellScript::UpdateCell;
@@ -22,7 +22,9 @@ async fn grid_cell_update() {
       let cell_changeset = match field_type {
         FieldType::RichText => "".to_string(),
         FieldType::Number => "123".to_string(),
-        FieldType::DateTime => make_date_cell_string("123"),
+        FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
+          make_date_cell_string("123")
+        },
         FieldType::SingleSelect => {
           let type_option = field
             .get_type_option::<SingleSelectTypeOption>(field.field_type)
@@ -64,7 +66,7 @@ async fn grid_cell_update() {
 }
 
 #[tokio::test]
-async fn text_cell_date_test() {
+async fn text_cell_data_test() {
   let test = DatabaseCellTest::new().await;
   let text_field = test.get_first_field(FieldType::RichText);
 
@@ -88,7 +90,7 @@ async fn text_cell_date_test() {
 }
 
 #[tokio::test]
-async fn url_cell_date_test() {
+async fn url_cell_data_test() {
   let test = DatabaseCellTest::new().await;
   let url_field = test.get_first_field(FieldType::URL);
   let cells = test
@@ -103,3 +105,83 @@ async fn url_cell_date_test() {
     }
   }
 }
+
+#[tokio::test]
+async fn update_updated_at_field_on_other_cell_update() {
+  let mut test = DatabaseCellTest::new().await;
+  let updated_at_field = test.get_first_field(FieldType::UpdatedAt);
+
+  let text_field = test
+    .fields
+    .iter()
+    .find(|&f| FieldType::from(f.field_type) == FieldType::RichText)
+    .unwrap();
+
+  let before_update_timestamp = chrono::offset::Utc::now().timestamp();
+  test
+    .run_script(UpdateCell {
+      changeset: CellChangesetPB {
+        view_id: test.view_id.clone(),
+        row_id: test.rows[0].id.to_string(),
+        field_id: text_field.id.clone(),
+        cell_changeset: "change".to_string(),
+      },
+      is_err: false,
+    })
+    .await;
+  let after_update_timestamp = chrono::offset::Utc::now().timestamp();
+
+  let cells = test
+    .editor
+    .get_cells_for_field(&test.view_id, &updated_at_field.id)
+    .await;
+  assert!(cells.len() > 0);
+  for (i, cell) in cells.into_iter().enumerate() {
+    let timestamp = DateCellData::from(cell.as_ref()).timestamp.unwrap();
+    println!(
+      "{}, bf: {}, af: {}",
+      timestamp, before_update_timestamp, after_update_timestamp
+    );
+    match i {
+      0 => assert!(
+        timestamp >= before_update_timestamp && timestamp <= after_update_timestamp,
+        "{} >= {} && {} <= {}",
+        timestamp,
+        before_update_timestamp,
+        timestamp,
+        after_update_timestamp
+      ),
+      1 => assert!(
+        timestamp <= before_update_timestamp,
+        "{} <= {}",
+        timestamp,
+        before_update_timestamp
+      ),
+      2 => assert!(
+        timestamp <= before_update_timestamp,
+        "{} <= {}",
+        timestamp,
+        before_update_timestamp
+      ),
+      3 => assert!(
+        timestamp <= before_update_timestamp,
+        "{} <= {}",
+        timestamp,
+        before_update_timestamp
+      ),
+      4 => assert!(
+        timestamp <= before_update_timestamp,
+        "{} <= {}",
+        timestamp,
+        before_update_timestamp
+      ),
+      5 => assert!(
+        timestamp <= before_update_timestamp,
+        "{} <= {}",
+        timestamp,
+        before_update_timestamp
+      ),
+      _ => {},
+    }
+  }
+}

+ 2 - 1
frontend/rust-lib/flowy-database2/tests/database/database_editor.rs

@@ -277,6 +277,7 @@ impl<'a> TestRowBuilder<'a> {
     time: Option<String>,
     include_time: Option<bool>,
     timezone_id: Option<String>,
+    field_type: &FieldType,
   ) -> String {
     let value = serde_json::to_string(&DateCellChangeset {
       date: Some(data.to_string()),
@@ -285,7 +286,7 @@ impl<'a> TestRowBuilder<'a> {
       timezone_id,
     })
     .unwrap();
-    let date_field = self.field_with_type(&FieldType::DateTime);
+    let date_field = self.field_with_type(field_type);
     self.cell_build.insert_text_cell(&date_field.id, value);
     date_field.id.clone()
   }

+ 10 - 0
frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs

@@ -30,6 +30,16 @@ async fn grid_create_field() {
     },
   ];
   test.run_scripts(scripts).await;
+
+  let (params, field) = create_date_field(&test.view_id(), FieldType::CreatedAt);
+  let scripts = vec![
+    CreateField { params },
+    AssertFieldTypeOptionEqual {
+      field_index: test.field_count(),
+      expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(),
+    },
+  ];
+  test.run_scripts(scripts).await;
 }
 
 #[tokio::test]

+ 35 - 2
frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs

@@ -1,8 +1,8 @@
 use collab_database::fields::Field;
 use flowy_database2::entities::{CreateFieldParams, FieldType};
 use flowy_database2::services::field::{
-  type_option_to_pb, DateCellChangeset, FieldBuilder, RichTextTypeOption, SelectOption,
-  SingleSelectTypeOption,
+  type_option_to_pb, DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder,
+  RichTextTypeOption, SelectOption, SingleSelectTypeOption, TimeFormat,
 };
 
 pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) {
@@ -42,6 +42,39 @@ 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) {
+  let date_type_option = DateTypeOption {
+    date_format: DateFormat::US,
+    time_format: TimeFormat::TwentyFourHour,
+    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::UpdatedAt => FieldBuilder::new(field_type.clone(), date_type_option.clone())
+      .name("Updated At")
+      .visibility(true)
+      .build(),
+    FieldType::CreatedAt => FieldBuilder::new(field_type.clone(), date_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 params = CreateFieldParams {
+    view_id: grid_id.to_owned(),
+    field_type,
+    type_option_data: Some(type_option_data),
+  };
+  (params, field)
+}
+
 //  The grid will contains all existing field types and there are three empty rows in this grid.
 
 pub fn make_date_cell_string(s: &str) -> String {

+ 49 - 32
frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs

@@ -38,14 +38,21 @@ pub fn make_test_board() -> DatabaseData {
           .build();
         fields.push(number_field);
       },
-      FieldType::DateTime => {
+      FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
         // Date
         let date_type_option = DateTypeOption {
           date_format: DateFormat::US,
           time_format: TimeFormat::TwentyFourHour,
+          field_type: field_type.clone(),
+        };
+        let name = match field_type {
+          FieldType::DateTime => "Time",
+          FieldType::UpdatedAt => "Updated At",
+          FieldType::CreatedAt => "Created At",
+          _ => "",
         };
         let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
-          .name("Time")
+          .name(name)
           .visibility(true)
           .build();
         fields.push(date_field);
@@ -119,12 +126,14 @@ 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 => row_builder.insert_date_cell(
-              "1647251762",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1647251762",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(0))
             },
@@ -142,12 +151,14 @@ 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 => row_builder.insert_date_cell(
-              "1647251762",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1647251762",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(0))
             },
@@ -164,12 +175,14 @@ 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 => row_builder.insert_date_cell(
-              "1647251762",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1647251762",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(1))
             },
@@ -189,12 +202,14 @@ 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 => row_builder.insert_date_cell(
-              "1668704685",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1668704685",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(1))
             },
@@ -209,12 +224,14 @@ 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 => row_builder.insert_date_cell(
-              "1668359085",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1668359085",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(2))
             },

+ 5 - 0
frontend/rust-lib/flowy-database2/tests/database/mock_data/calendar_mock_data.rs

@@ -51,6 +51,7 @@ pub fn make_test_calendar() -> DatabaseData {
               None,
               None,
               Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+              &field_type,
             ),
             _ => "".to_owned(),
           };
@@ -65,6 +66,7 @@ pub fn make_test_calendar() -> DatabaseData {
               None,
               None,
               Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+              &field_type,
             ),
             _ => "".to_owned(),
           };
@@ -79,6 +81,7 @@ pub fn make_test_calendar() -> DatabaseData {
               None,
               None,
               Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+              &field_type,
             ),
             _ => "".to_owned(),
           };
@@ -93,6 +96,7 @@ pub fn make_test_calendar() -> DatabaseData {
               None,
               None,
               Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+              &field_type,
             ),
             _ => "".to_owned(),
           };
@@ -107,6 +111,7 @@ pub fn make_test_calendar() -> DatabaseData {
               None,
               None,
               Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+              &field_type,
             ),
             _ => "".to_owned(),
           };

+ 57 - 38
frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs

@@ -39,14 +39,21 @@ pub fn make_test_grid() -> DatabaseData {
           .build();
         fields.push(number_field);
       },
-      FieldType::DateTime => {
+      FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => {
         // Date
         let date_type_option = DateTypeOption {
           date_format: DateFormat::US,
           time_format: TimeFormat::TwentyFourHour,
+          field_type: field_type.clone(),
+        };
+        let name = match field_type {
+          FieldType::DateTime => "Time",
+          FieldType::UpdatedAt => "Updated At",
+          FieldType::CreatedAt => "Created At",
+          _ => "",
         };
         let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
-          .name("Time")
+          .name(name)
           .visibility(true)
           .build();
         fields.push(date_field);
@@ -118,12 +125,14 @@ 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 => row_builder.insert_date_cell(
-              "1647251762",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1647251762",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::MultiSelect => row_builder
               .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(0)]),
             FieldType::Checklist => row_builder.insert_checklist_cell(|options| options),
@@ -140,12 +149,14 @@ 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 => row_builder.insert_date_cell(
-              "1647251762",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1647251762",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::MultiSelect => row_builder
               .insert_multi_select_cell(|mut options| vec![options.remove(0), options.remove(1)]),
             FieldType::Checkbox => row_builder.insert_checkbox_cell("true"),
@@ -158,12 +169,14 @@ 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 => row_builder.insert_date_cell(
-              "1647251762",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1647251762",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(0))
             },
@@ -180,12 +193,14 @@ 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 => row_builder.insert_date_cell(
-              "1668704685",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1668704685",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(0))
             },
@@ -199,12 +214,14 @@ 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 => row_builder.insert_date_cell(
-              "1668359085",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1668359085",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(1))
             },
@@ -219,12 +236,14 @@ 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 => row_builder.insert_date_cell(
-              "1671938394",
-              None,
-              None,
-              Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
-            ),
+            FieldType::DateTime | FieldType::UpdatedAt | FieldType::CreatedAt => row_builder
+              .insert_date_cell(
+                "1671938394",
+                None,
+                None,
+                Some(chrono_tz::Tz::Etc__GMTPlus8.to_string()),
+                &field_type,
+              ),
             FieldType::SingleSelect => {
               row_builder.insert_single_select_cell(|mut options| options.remove(1))
             },

+ 1 - 0
frontend/rust-lib/flowy-database2/tests/database/mod.rs

@@ -1,3 +1,4 @@
+mod block_test;
 mod cell_test;
 mod database_editor;
 mod field_test;