浏览代码

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

* fix: stringify date cell includes time if true

* refactor: LastModified and CreatedAt type option

* chore: frontend implementation

* chore: some adjustments and fix tests

* fix: integration tests

* chore: timestamp type option ui
Richard Shiue 1 年之前
父节点
当前提交
8bcc6384f2
共有 42 个文件被更改,包括 1254 次插入287 次删除
  1. 0 5
      frontend/appflowy_flutter/integration_test/database_cell_test.dart
  2. 0 1
      frontend/appflowy_flutter/integration_test/database_share_test.dart
  3. 3 5
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  4. 16 3
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart
  5. 10 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart
  6. 1 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart
  7. 0 10
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart
  8. 76 0
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/timestamp_bloc.dart
  9. 13 1
      frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart
  10. 18 4
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart
  11. 179 0
      frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart
  12. 80 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart
  13. 13 2
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart
  14. 79 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/timestamp_card_cell.dart
  15. 2 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart
  16. 21 39
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart
  17. 6 26
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart
  18. 109 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart
  19. 80 0
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart
  20. 5 1
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart
  21. 1 6
      frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs
  22. 2 0
      frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs
  23. 50 0
      frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs
  24. 6 6
      frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs
  25. 4 20
      frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs
  26. 58 65
      frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs
  27. 5 28
      frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs
  28. 2 0
      frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs
  29. 2 2
      frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs
  30. 2 2
      frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs
  31. 6 0
      frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs
  32. 205 0
      frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs
  33. 69 0
      frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs
  34. 15 6
      frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs
  35. 21 4
      frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs
  36. 2 3
      frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs
  37. 5 3
      frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs
  38. 1 1
      frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs
  39. 33 13
      frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs
  40. 22 9
      frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs
  41. 25 12
      frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs
  42. 7 7
      frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs

+ 0 - 5
frontend/appflowy_flutter/integration_test/database_cell_test.dart

@@ -218,7 +218,6 @@ void main() {
 
       await tester.assertDateCellInGrid(
         rowIndex: 0,
-        fieldType: fieldType,
         content: DateFormat('MMM dd, y').format(today),
       );
 
@@ -233,7 +232,6 @@ void main() {
 
       await tester.assertDateCellInGrid(
         rowIndex: 0,
-        fieldType: fieldType,
         content: DateFormat('MMM dd, y HH:mm').format(now),
       );
 
@@ -247,7 +245,6 @@ void main() {
 
       await tester.assertDateCellInGrid(
         rowIndex: 0,
-        fieldType: fieldType,
         content: DateFormat('dd/MM/y HH:mm').format(now),
       );
 
@@ -261,7 +258,6 @@ void main() {
 
       await tester.assertDateCellInGrid(
         rowIndex: 0,
-        fieldType: fieldType,
         content: DateFormat('dd/MM/y hh:mm a').format(now),
       );
 
@@ -273,7 +269,6 @@ void main() {
 
       await tester.assertDateCellInGrid(
         rowIndex: 0,
-        fieldType: fieldType,
         content: '',
       );
 

+ 0 - 1
frontend/appflowy_flutter/integration_test/database_share_test.dart

@@ -157,7 +157,6 @@ void main() {
       for (final (index, content) in dateCells.indexed) {
         await tester.assertDateCellInGrid(
           rowIndex: index,
-          fieldType: FieldType.DateTime,
           content: content,
         );
       }

+ 3 - 5
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -43,6 +43,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart';
@@ -262,15 +263,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
 
   Future<void> assertDateCellInGrid({
     required int rowIndex,
-    required FieldType fieldType,
     required String content,
   }) async {
     final findRow = find.byType(GridRow, skipOffstage: false);
     final findCell = find.descendant(
       of: findRow.at(rowIndex),
-      matching: find.byWidgetPredicate(
-        (widget) => widget is GridDateCell && widget.fieldType == fieldType,
-      ),
+      matching: find.byType(GridDateCell),
       skipOffstage: false,
     );
 
@@ -1287,7 +1285,7 @@ Finder finderForFieldType(FieldType fieldType) {
       return find.byType(GridDateCell, skipOffstage: false);
     case FieldType.LastEditedTime:
     case FieldType.CreatedTime:
-      return find.byType(GridDateCell, skipOffstage: false);
+      return find.byType(GridTimestampCell, skipOffstage: false);
     case FieldType.SingleSelect:
       return find.byType(GridSingleSelectCell, skipOffstage: false);
     case FieldType.MultiSelect:

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

@@ -2,6 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
 import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
 
 import 'cell_controller.dart';
@@ -14,6 +15,7 @@ typedef SelectOptionCellController
     = CellController<SelectOptionCellDataPB, String>;
 typedef ChecklistCellController = CellController<ChecklistCellDataPB, String>;
 typedef DateCellController = CellController<DateCellDataPB, String>;
+typedef TimestampCellController = CellController<TimestampCellDataPB, String>;
 typedef URLCellController = CellController<URLCellDataPB, String>;
 
 class CellControllerBuilder {
@@ -41,14 +43,11 @@ class CellControllerBuilder {
               TextCellDataPersistence(cellContext: _cellContext),
         );
       case FieldType.DateTime:
-      case FieldType.LastEditedTime:
-      case FieldType.CreatedTime:
         final cellDataLoader = CellDataLoader(
           cellContext: _cellContext,
           parser: DateCellDataParser(),
           reloadOnFieldChanged: true,
         );
-
         return DateCellController(
           cellContext: _cellContext,
           cellCache: _cellCache,
@@ -56,6 +55,20 @@ class CellControllerBuilder {
           cellDataPersistence:
               TextCellDataPersistence(cellContext: _cellContext),
         );
+      case FieldType.LastEditedTime:
+      case FieldType.CreatedTime:
+        final cellDataLoader = CellDataLoader(
+          cellContext: _cellContext,
+          parser: TimestampCellDataParser(),
+          reloadOnFieldChanged: true,
+        );
+        return TimestampCellController(
+          cellContext: _cellContext,
+          cellCache: _cellCache,
+          cellDataLoader: cellDataLoader,
+          cellDataPersistence:
+              TextCellDataPersistence(cellContext: _cellContext),
+        );
       case FieldType.Number:
         final cellDataLoader = CellDataLoader(
           cellContext: _cellContext,

+ 10 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart

@@ -73,6 +73,16 @@ class DateCellDataParser implements CellDataParser<DateCellDataPB> {
   }
 }
 
+class TimestampCellDataParser implements CellDataParser<TimestampCellDataPB> {
+  @override
+  TimestampCellDataPB? parserData(List<int> data) {
+    if (data.isEmpty) {
+      return null;
+    }
+    return TimestampCellDataPB.fromBuffer(data);
+  }
+}
+
 class SelectOptionCellDataParser
     implements CellDataParser<SelectOptionCellDataPB> {
   @override

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

@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
 import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/dispatch/dispatch.dart';

+ 0 - 10
frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart

@@ -27,13 +27,6 @@ class DateTypeOptionBloc
               ),
             );
           },
-          includeTime: (_IncludeTime value) {
-            emit(
-              state.copyWith(
-                typeOption: _updateTypeOption(includeTime: value.includeTime),
-              ),
-            );
-          },
         );
       },
     );
@@ -42,7 +35,6 @@ class DateTypeOptionBloc
   DateTypeOptionPB _updateTypeOption({
     DateFormatPB? dateFormat,
     TimeFormatPB? timeFormat,
-    bool? includeTime,
   }) {
     state.typeOption.freeze();
     return state.typeOption.rebuild((typeOption) {
@@ -63,8 +55,6 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent {
       _DidSelectDateFormat;
   const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) =
       _DidSelectTimeFormat;
-  const factory DateTypeOptionEvent.includeTime(bool includeTime) =
-      _IncludeTime;
 }
 
 @freezed

+ 76 - 0
frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/timestamp_bloc.dart

@@ -0,0 +1,76 @@
+import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:protobuf/protobuf.dart';
+
+import 'type_option_context.dart';
+part 'timestamp_bloc.freezed.dart';
+
+class TimestampTypeOptionBloc
+    extends Bloc<TimestampTypeOptionEvent, TimestampTypeOptionState> {
+  TimestampTypeOptionBloc({
+    required TimestampTypeOptionContext typeOptionContext,
+  }) : super(TimestampTypeOptionState.initial(typeOptionContext.typeOption)) {
+    on<TimestampTypeOptionEvent>(
+      (event, emit) async {
+        event.map(
+          didSelectDateFormat: (_DidSelectDateFormat value) {
+            _updateTypeOption(dateFormat: value.format, emit: emit);
+          },
+          didSelectTimeFormat: (_DidSelectTimeFormat value) {
+            _updateTypeOption(timeFormat: value.format, emit: emit);
+          },
+          includeTime: (_IncludeTime value) {
+            _updateTypeOption(includeTime: value.includeTime, emit: emit);
+          },
+        );
+      },
+    );
+  }
+
+  void _updateTypeOption({
+    DateFormatPB? dateFormat,
+    TimeFormatPB? timeFormat,
+    bool? includeTime,
+    required Emitter<TimestampTypeOptionState> emit,
+  }) {
+    state.typeOption.freeze();
+    final newTypeOption = state.typeOption.rebuild((typeOption) {
+      if (dateFormat != null) {
+        typeOption.dateFormat = dateFormat;
+      }
+
+      if (timeFormat != null) {
+        typeOption.timeFormat = timeFormat;
+      }
+
+      if (includeTime != null) {
+        typeOption.includeTime = includeTime;
+      }
+    });
+    emit(state.copyWith(typeOption: newTypeOption));
+  }
+}
+
+@freezed
+class TimestampTypeOptionEvent with _$TimestampTypeOptionEvent {
+  const factory TimestampTypeOptionEvent.didSelectDateFormat(
+    DateFormatPB format,
+  ) = _DidSelectDateFormat;
+  const factory TimestampTypeOptionEvent.didSelectTimeFormat(
+    TimeFormatPB format,
+  ) = _DidSelectTimeFormat;
+  const factory TimestampTypeOptionEvent.includeTime(bool includeTime) =
+      _IncludeTime;
+}
+
+@freezed
+class TimestampTypeOptionState with _$TimestampTypeOptionState {
+  const factory TimestampTypeOptionState({
+    required TimestampTypeOptionPB typeOption,
+  }) = _TimestampTypeOptionState;
+
+  factory TimestampTypeOptionState.initial(TimestampTypeOptionPB typeOption) =>
+      TimestampTypeOptionState(typeOption: typeOption);
+}

+ 13 - 1
frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart

@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
 import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
@@ -58,7 +59,7 @@ class URLTypeOptionWidgetDataParser extends TypeOptionParser<URLTypeOptionPB> {
   }
 }
 
-// Date
+// DateTime
 typedef DateTypeOptionContext = TypeOptionContext<DateTypeOptionPB>;
 
 class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
@@ -68,6 +69,17 @@ class DateTypeOptionDataParser extends TypeOptionParser<DateTypeOptionPB> {
   }
 }
 
+// LastModified and CreatedAt
+typedef TimestampTypeOptionContext = TypeOptionContext<TimestampTypeOptionPB>;
+
+class TimestampTypeOptionDataParser
+    extends TypeOptionParser<TimestampTypeOptionPB> {
+  @override
+  TimestampTypeOptionPB fromBuffer(List<int> buffer) {
+    return TimestampTypeOptionPB.fromBuffer(buffer);
+  }
+}
+
 // SingleSelect
 typedef SingleSelectTypeOptionContext
     = TypeOptionContext<SingleSelectTypeOptionPB>;

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

@@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'
 import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:protobuf/protobuf.dart' hide FieldInfo;
@@ -20,6 +21,7 @@ import 'multi_select.dart';
 import 'number.dart';
 import 'rich_text.dart';
 import 'single_select.dart';
+import 'timestamp.dart';
 import 'url.dart';
 
 typedef TypeOptionData = Uint8List;
@@ -73,8 +75,6 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
         ),
       );
     case FieldType.DateTime:
-    case FieldType.LastEditedTime:
-    case FieldType.CreatedTime:
       return DateTypeOptionWidgetBuilder(
         makeTypeOptionContextWithDataController<DateTypeOptionPB>(
           viewId: viewId,
@@ -83,6 +83,16 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
         ),
         popoverMutex,
       );
+    case FieldType.LastEditedTime:
+    case FieldType.CreatedTime:
+      return TimestampTypeOptionWidgetBuilder(
+        makeTypeOptionContextWithDataController<TimestampTypeOptionPB>(
+          viewId: viewId,
+          fieldType: fieldType,
+          dataController: dataController,
+        ),
+        popoverMutex,
+      );
     case FieldType.SingleSelect:
       return SingleSelectTypeOptionWidgetBuilder(
         makeTypeOptionContextWithDataController<SingleSelectTypeOptionPB>(
@@ -203,12 +213,16 @@ TypeOptionContext<T>
         dataParser: CheckboxTypeOptionWidgetDataParser(),
       ) as TypeOptionContext<T>;
     case FieldType.DateTime:
-    case FieldType.LastEditedTime:
-    case FieldType.CreatedTime:
       return DateTypeOptionContext(
         dataController: dataController,
         dataParser: DateTypeOptionDataParser(),
       ) as TypeOptionContext<T>;
+    case FieldType.LastEditedTime:
+    case FieldType.CreatedTime:
+      return TimestampTypeOptionContext(
+        dataController: dataController,
+        dataParser: TimestampTypeOptionDataParser(),
+      ) as TypeOptionContext<T>;
     case FieldType.SingleSelect:
       return SingleSelectTypeOptionContext(
         dataController: dataController,

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

@@ -0,0 +1,179 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart';
+import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart';
+import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
+import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'builder.dart';
+import 'date.dart';
+
+class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder {
+  final TimestampTypeOptionWidget _widget;
+
+  TimestampTypeOptionWidgetBuilder(
+    TimestampTypeOptionContext typeOptionContext,
+    PopoverMutex popoverMutex,
+  ) : _widget = TimestampTypeOptionWidget(
+          typeOptionContext: typeOptionContext,
+          popoverMutex: popoverMutex,
+        );
+
+  @override
+  Widget? build(BuildContext context) {
+    return _widget;
+  }
+}
+
+class TimestampTypeOptionWidget extends TypeOptionWidget {
+  final TimestampTypeOptionContext typeOptionContext;
+  final PopoverMutex popoverMutex;
+  const TimestampTypeOptionWidget({
+    required this.typeOptionContext,
+    required this.popoverMutex,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) =>
+          TimestampTypeOptionBloc(typeOptionContext: typeOptionContext),
+      child: BlocConsumer<TimestampTypeOptionBloc, TimestampTypeOptionState>(
+        listener: (context, state) =>
+            typeOptionContext.typeOption = state.typeOption,
+        builder: (context, state) {
+          final List<Widget> children = [
+            const TypeOptionSeparator(),
+            _renderDateFormatButton(context, state.typeOption.dateFormat),
+            _renderTimeFormatButton(context, state.typeOption.timeFormat),
+            Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 12.0),
+              child: IncludeTimeButton(
+                onChanged: (value) => context
+                    .read<TimestampTypeOptionBloc>()
+                    .add(TimestampTypeOptionEvent.includeTime(!value)),
+                value: state.typeOption.includeTime,
+              ),
+            ),
+          ];
+
+          return ListView.separated(
+            shrinkWrap: true,
+            separatorBuilder: (context, index) {
+              if (index == 0) {
+                return const SizedBox();
+              } else {
+                return VSpace(GridSize.typeOptionSeparatorHeight);
+              }
+            },
+            itemCount: children.length,
+            itemBuilder: (BuildContext context, int index) => children[index],
+          );
+        },
+      ),
+    );
+  }
+
+  Widget _renderDateFormatButton(
+    BuildContext context,
+    DateFormatPB dataFormat,
+  ) {
+    return AppFlowyPopover(
+      mutex: popoverMutex,
+      asBarrier: true,
+      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
+      offset: const Offset(8, 0),
+      constraints: BoxConstraints.loose(const Size(460, 440)),
+      popupBuilder: (popoverContext) {
+        return DateFormatList(
+          selectedFormat: dataFormat,
+          onSelected: (format) {
+            context
+                .read<TimestampTypeOptionBloc>()
+                .add(TimestampTypeOptionEvent.didSelectDateFormat(format));
+            PopoverContainer.of(popoverContext).close();
+          },
+        );
+      },
+      child: const Padding(
+        padding: EdgeInsets.symmetric(horizontal: 12.0),
+        child: DateFormatButton(),
+      ),
+    );
+  }
+
+  Widget _renderTimeFormatButton(
+    BuildContext context,
+    TimeFormatPB timeFormat,
+  ) {
+    return AppFlowyPopover(
+      mutex: popoverMutex,
+      asBarrier: true,
+      triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
+      offset: const Offset(8, 0),
+      constraints: BoxConstraints.loose(const Size(460, 440)),
+      popupBuilder: (BuildContext popoverContext) {
+        return TimeFormatList(
+          selectedFormat: timeFormat,
+          onSelected: (format) {
+            context
+                .read<TimestampTypeOptionBloc>()
+                .add(TimestampTypeOptionEvent.didSelectTimeFormat(format));
+            PopoverContainer.of(popoverContext).close();
+          },
+        );
+      },
+      child: Padding(
+        padding: const EdgeInsets.symmetric(horizontal: 12.0),
+        child: TimeFormatButton(timeFormat: timeFormat),
+      ),
+    );
+  }
+}
+
+class IncludeTimeButton extends StatelessWidget {
+  final bool value;
+  final Function(bool value) onChanged;
+  const IncludeTimeButton({
+    super.key,
+    required this.onChanged,
+    required this.value,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: GridSize.popoverItemHeight,
+      child: Padding(
+        padding: GridSize.typeOptionContentInsets,
+        child: Row(
+          children: [
+            FlowySvg(
+              FlowySvgs.clock_alarm_s,
+              color: Theme.of(context).iconTheme.color,
+            ),
+            const HSpace(6),
+            FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
+            const Spacer(),
+            Toggle(
+              value: value,
+              onChanged: onChanged,
+              style: ToggleStyle.big,
+              padding: EdgeInsets.zero,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 80 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart

@@ -0,0 +1,80 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+part 'timestamp_card_cell_bloc.freezed.dart';
+
+class TimestampCardCellBloc
+    extends Bloc<TimestampCardCellEvent, TimestampCardCellState> {
+  final TimestampCellController cellController;
+  void Function()? _onCellChangedFn;
+
+  TimestampCardCellBloc({required this.cellController})
+      : super(TimestampCardCellState.initial(cellController)) {
+    on<TimestampCardCellEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () => _startListening(),
+          didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
+            emit(
+              state.copyWith(
+                data: cellData,
+                dateStr: cellData?.dateTime ?? "",
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    await cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((data) {
+        if (!isClosed) {
+          add(TimestampCardCellEvent.didReceiveCellUpdate(data));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class TimestampCardCellEvent with _$TimestampCardCellEvent {
+  const factory TimestampCardCellEvent.initial() = _InitialCell;
+  const factory TimestampCardCellEvent.didReceiveCellUpdate(
+    TimestampCellDataPB? data,
+  ) = _DidReceiveCellUpdate;
+}
+
+@freezed
+class TimestampCardCellState with _$TimestampCardCellState {
+  const factory TimestampCardCellState({
+    required TimestampCellDataPB? data,
+    required String dateStr,
+    required FieldInfo fieldInfo,
+  }) = _TimestampCardCellState;
+
+  factory TimestampCardCellState.initial(TimestampCellController context) {
+    final cellData = context.getCellData();
+
+    return TimestampCardCellState(
+      fieldInfo: context.fieldInfo,
+      data: cellData,
+      dateStr: cellData?.dateTime ?? "",
+    );
+  }
+}

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

@@ -10,6 +10,7 @@ import 'cells/date_card_cell.dart';
 import 'cells/number_card_cell.dart';
 import 'cells/select_option_card_cell.dart';
 import 'cells/text_card_cell.dart';
+import 'cells/timestamp_card_cell.dart';
 import 'cells/url_card_cell.dart';
 
 // T represents as the Generic card data
@@ -39,13 +40,23 @@ class CardCellBuilder<CustomCardData> {
           key: key,
         );
       case FieldType.DateTime:
-      case FieldType.LastEditedTime:
-      case FieldType.CreatedTime:
         return DateCardCell<CustomCardData>(
           renderHook: renderHook?.renderHook[FieldType.DateTime],
           cellControllerBuilder: cellControllerBuilder,
           key: key,
         );
+      case FieldType.LastEditedTime:
+        return TimestampCardCell<CustomCardData>(
+          renderHook: renderHook?.renderHook[FieldType.LastEditedTime],
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
+      case FieldType.CreatedTime:
+        return TimestampCardCell<CustomCardData>(
+          renderHook: renderHook?.renderHook[FieldType.CreatedTime],
+          cellControllerBuilder: cellControllerBuilder,
+          key: key,
+        );
       case FieldType.SingleSelect:
         return SelectOptionCardCell<CustomCardData>(
           renderHook: renderHook?.renderHook[FieldType.SingleSelect],

+ 79 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/timestamp_card_cell.dart

@@ -0,0 +1,79 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../define.dart';
+import 'card_cell.dart';
+
+class TimestampCardCell<CustomCardData> extends CardCell {
+  final CellControllerBuilder cellControllerBuilder;
+  final CellRenderHook<dynamic, CustomCardData>? renderHook;
+
+  const TimestampCardCell({
+    required this.cellControllerBuilder,
+    this.renderHook,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<TimestampCardCell> createState() => _TimestampCardCellState();
+}
+
+class _TimestampCardCellState extends State<TimestampCardCell> {
+  late TimestampCardCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as TimestampCellController;
+
+    _cellBloc = TimestampCardCellBloc(cellController: cellController)
+      ..add(const TimestampCardCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<TimestampCardCellBloc, TimestampCardCellState>(
+        buildWhen: (previous, current) => previous.dateStr != current.dateStr,
+        builder: (context, state) {
+          if (state.dateStr.isEmpty) {
+            return const SizedBox.shrink();
+          }
+          final Widget? custom = widget.renderHook?.call(
+            state.data,
+            widget.cardData,
+            context,
+          );
+          if (custom != null) {
+            return custom;
+          }
+
+          return Align(
+            alignment: Alignment.centerLeft,
+            child: Padding(
+              padding: EdgeInsets.symmetric(
+                vertical: CardSizes.cardCellVPadding,
+              ),
+              child: FlowyText.regular(
+                state.dateStr,
+                fontSize: 13,
+                color: Theme.of(context).hintColor,
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+}

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

@@ -11,6 +11,7 @@ import 'cells/date_cell/date_cell.dart';
 import 'cells/number_cell/number_cell.dart';
 import 'cells/select_option_cell/select_option_cell.dart';
 import 'cells/text_cell/text_cell.dart';
+import 'cells/timestamp_cell/timestamp_cell.dart';
 import 'cells/url_cell/url_cell.dart';
 
 /// Build the cell widget in Grid style.
@@ -41,14 +42,12 @@ class GridCellBuilder {
           cellControllerBuilder: cellControllerBuilder,
           key: key,
           style: style,
-          fieldType: cellContext.fieldType,
         );
       case FieldType.LastEditedTime:
       case FieldType.CreatedTime:
-        return GridDateCell(
+        return GridTimestampCell(
           cellControllerBuilder: cellControllerBuilder,
           key: key,
-          editable: false,
           style: style,
           fieldType: cellContext.fieldType,
         );

+ 21 - 39
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart

@@ -1,5 +1,4 @@
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/widgets.dart';
@@ -22,20 +21,12 @@ abstract class GridCellDelegate {
 }
 
 class GridDateCell extends GridCellWidget {
-  final bool editable;
-
-  /// The [GridDateCell] is used by Field Type [FieldType.DateTime],
-  /// [FieldType.CreatedTime], [FieldType.LastEditedTime]. So it needs
-  /// to know the field type.
-  final FieldType fieldType;
   final CellControllerBuilder cellControllerBuilder;
   late final DateCellStyle? cellStyle;
 
   GridDateCell({
     GridCellStyle? style,
-    required this.fieldType,
     required this.cellControllerBuilder,
-    this.editable = true,
     Key? key,
   }) : super(key: key) {
     if (style != null) {
@@ -72,33 +63,27 @@ class _DateCellState extends GridCellState<GridDateCell> {
       value: _cellBloc,
       child: BlocBuilder<DateCellBloc, DateCellState>(
         builder: (context, state) {
-          Widget dateTextWidget = GridDateCellText(
-            dateStr: state.dateStr,
-            alignment: alignment,
+          return AppFlowyPopover(
+            controller: _popover,
+            triggerActions: PopoverTriggerFlags.none,
+            direction: PopoverDirection.bottomWithLeftAligned,
+            constraints: BoxConstraints.loose(const Size(260, 520)),
+            margin: EdgeInsets.zero,
+            child: GridDateCellText(
+              dateStr: state.dateStr,
+              alignment: alignment,
+            ),
+            popupBuilder: (BuildContext popoverContent) {
+              return DateCellEditor(
+                cellController:
+                    widget.cellControllerBuilder.build() as DateCellController,
+                onDismissed: () => widget.onCellFocus.value = false,
+              );
+            },
+            onClose: () {
+              widget.onCellFocus.value = false;
+            },
           );
-
-          // If the cell is editable, wrap it in a popover.
-          if (widget.editable) {
-            dateTextWidget = AppFlowyPopover(
-              controller: _popover,
-              triggerActions: PopoverTriggerFlags.none,
-              direction: PopoverDirection.bottomWithLeftAligned,
-              constraints: BoxConstraints.loose(const Size(260, 520)),
-              margin: EdgeInsets.zero,
-              child: dateTextWidget,
-              popupBuilder: (BuildContext popoverContent) {
-                return DateCellEditor(
-                  cellController: widget.cellControllerBuilder.build()
-                      as DateCellController,
-                  onDismissed: () => widget.onCellFocus.value = false,
-                );
-              },
-              onClose: () {
-                widget.onCellFocus.value = false;
-              },
-            );
-          }
-          return dateTextWidget;
         },
       ),
     );
@@ -113,10 +98,7 @@ class _DateCellState extends GridCellState<GridDateCell> {
   @override
   void requestBeginFocus() {
     _popover.show();
-
-    if (widget.editable) {
-      widget.onCellFocus.value = true;
-    }
+    widget.onCellFocus.value = true;
   }
 
   @override

+ 6 - 26
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart

@@ -2,8 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
 import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart';
-import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
-import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
@@ -241,30 +240,11 @@ class _IncludeTimeButton extends StatelessWidget {
       builder: (context, includeTime) {
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 12.0),
-          child: SizedBox(
-            height: GridSize.popoverItemHeight,
-            child: Padding(
-              padding: GridSize.typeOptionContentInsets,
-              child: Row(
-                children: [
-                  FlowySvg(
-                    FlowySvgs.clock_alarm_s,
-                    color: Theme.of(context).iconTheme.color,
-                  ),
-                  const HSpace(6),
-                  FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
-                  const Spacer(),
-                  Toggle(
-                    value: includeTime,
-                    onChanged: (value) => context
-                        .read<DateCellCalendarBloc>()
-                        .add(DateCellCalendarEvent.setIncludeTime(!value)),
-                    style: ToggleStyle.big,
-                    padding: EdgeInsets.zero,
-                  ),
-                ],
-              ),
-            ),
+          child: IncludeTimeButton(
+            onChanged: (value) => context
+                .read<DateCellCalendarBloc>()
+                .add(DateCellCalendarEvent.setIncludeTime(!value)),
+            value: includeTime,
           ),
         );
       },

+ 109 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart

@@ -0,0 +1,109 @@
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class TimestampCellStyle extends GridCellStyle {
+  Alignment alignment;
+
+  TimestampCellStyle({this.alignment = Alignment.center});
+}
+
+class GridTimestampCell extends GridCellWidget {
+  /// The [GridTimestampCell] is used by both [FieldType.CreatedTime]
+  /// and [FieldType.LastEditedTime]. So it needs to know the field type.
+  final FieldType fieldType;
+  final CellControllerBuilder cellControllerBuilder;
+  late final TimestampCellStyle? cellStyle;
+
+  GridTimestampCell({
+    GridCellStyle? style,
+    required this.fieldType,
+    required this.cellControllerBuilder,
+    Key? key,
+  }) : super(key: key) {
+    if (style != null) {
+      cellStyle = (style as TimestampCellStyle);
+    } else {
+      cellStyle = null;
+    }
+  }
+
+  @override
+  GridCellState<GridTimestampCell> createState() => _TimestampCellState();
+}
+
+class _TimestampCellState extends GridCellState<GridTimestampCell> {
+  late TimestampCellBloc _cellBloc;
+
+  @override
+  void initState() {
+    final cellController =
+        widget.cellControllerBuilder.build() as TimestampCellController;
+    _cellBloc = TimestampCellBloc(cellController: cellController)
+      ..add(const TimestampCellEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final alignment = widget.cellStyle != null
+        ? widget.cellStyle!.alignment
+        : Alignment.centerLeft;
+    return BlocProvider.value(
+      value: _cellBloc,
+      child: BlocBuilder<TimestampCellBloc, TimestampCellState>(
+        builder: (context, state) {
+          return GridTimestampCellText(
+            dateStr: state.dateStr,
+            alignment: alignment,
+          );
+        },
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    _cellBloc.close();
+    super.dispose();
+  }
+
+  @override
+  String? onCopy() => _cellBloc.state.dateStr;
+
+  @override
+  void requestBeginFocus() {
+    return;
+  }
+}
+
+class GridTimestampCellText extends StatelessWidget {
+  final String dateStr;
+  final Alignment alignment;
+  const GridTimestampCellText({
+    required this.dateStr,
+    required this.alignment,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox.expand(
+      child: Align(
+        alignment: alignment,
+        child: Padding(
+          padding: GridSize.cellContentInsets,
+          child: FlowyText.medium(
+            dateStr,
+            maxLines: null,
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 80 - 0
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart

@@ -0,0 +1,80 @@
+import 'dart:async';
+
+import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database_view/application/field/field_info.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'timestamp_cell_bloc.freezed.dart';
+
+class TimestampCellBloc extends Bloc<TimestampCellEvent, TimestampCellState> {
+  final TimestampCellController cellController;
+  void Function()? _onCellChangedFn;
+
+  TimestampCellBloc({required this.cellController})
+      : super(TimestampCellState.initial(cellController)) {
+    on<TimestampCellEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () => _startListening(),
+          didReceiveCellUpdate: (TimestampCellDataPB? cellData) {
+            emit(
+              state.copyWith(
+                data: cellData,
+                dateStr: cellData?.dateTime ?? "",
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onCellChangedFn != null) {
+      cellController.removeListener(_onCellChangedFn!);
+      _onCellChangedFn = null;
+    }
+    await cellController.dispose();
+    return super.close();
+  }
+
+  void _startListening() {
+    _onCellChangedFn = cellController.startListening(
+      onCellChanged: ((data) {
+        if (!isClosed) {
+          add(TimestampCellEvent.didReceiveCellUpdate(data));
+        }
+      }),
+    );
+  }
+}
+
+@freezed
+class TimestampCellEvent with _$TimestampCellEvent {
+  const factory TimestampCellEvent.initial() = _InitialCell;
+  const factory TimestampCellEvent.didReceiveCellUpdate(
+    TimestampCellDataPB? data,
+  ) = _DidReceiveCellUpdate;
+}
+
+@freezed
+class TimestampCellState with _$TimestampCellState {
+  const factory TimestampCellState({
+    required TimestampCellDataPB? data,
+    required String dateStr,
+    required FieldInfo fieldInfo,
+  }) = _TimestampCellState;
+
+  factory TimestampCellState.initial(TimestampCellController context) {
+    final cellData = context.getCellData();
+
+    return TimestampCellState(
+      fieldInfo: context.fieldInfo,
+      data: cellData,
+      dateStr: cellData?.dateTime ?? "",
+    );
+  }
+}

+ 5 - 1
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart

@@ -18,6 +18,7 @@ import 'cell_builder.dart';
 import 'cells/date_cell/date_cell.dart';
 import 'cells/select_option_cell/select_option_cell.dart';
 import 'cells/text_cell/text_cell.dart';
+import 'cells/timestamp_cell/timestamp_cell.dart';
 import 'cells/url_cell/url_cell.dart';
 
 /// Display the row properties in a list. Only use this widget in the
@@ -156,9 +157,12 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
     case FieldType.Checkbox:
       return null;
     case FieldType.DateTime:
+      return DateCellStyle(
+        alignment: Alignment.centerLeft,
+      );
     case FieldType.LastEditedTime:
     case FieldType.CreatedTime:
-      return DateCellStyle(
+      return TimestampCellStyle(
         alignment: Alignment.centerLeft,
       );
     case FieldType.MultiSelect:

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

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

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

+ 50 - 0
frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs

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

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

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

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

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

+ 58 - 65
frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs

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

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

@@ -83,42 +83,19 @@ impl From<&DateCellDataPB> for DateCellData {
   }
 }
 
-/// Wrapper for DateCellData that also contains the field type.
-/// Handy struct to use when you need to convert a DateCellData to a Cell.
-pub struct DateCellDataWrapper {
-  data: DateCellData,
-  field_type: FieldType,
-}
-
-impl From<(FieldType, DateCellData)> for DateCellDataWrapper {
-  fn from((field_type, data): (FieldType, DateCellData)) -> Self {
-    Self { data, field_type }
-  }
-}
-
-impl From<DateCellDataWrapper> for Cell {
-  fn from(wrapper: DateCellDataWrapper) -> Self {
-    let (field_type, data) = (wrapper.field_type, wrapper.data);
-    let timestamp_string = match data.timestamp {
+impl From<&DateCellData> for Cell {
+  fn from(cell_data: &DateCellData) -> Self {
+    let timestamp_string = match cell_data.timestamp {
       Some(timestamp) => timestamp.to_string(),
       None => "".to_owned(),
     };
-    // Most of the case, don't use these keys in other places. Otherwise, we should define
-    // constants for them.
-    new_cell_builder(field_type)
+    new_cell_builder(FieldType::DateTime)
       .insert_str_value(CELL_DATA, timestamp_string)
-      .insert_bool_value("include_time", data.include_time)
+      .insert_bool_value("include_time", cell_data.include_time)
       .build()
   }
 }
 
-impl From<DateCellData> for Cell {
-  fn from(data: DateCellData) -> Self {
-    let data: DateCellDataWrapper = (FieldType::DateTime, data).into();
-    Cell::from(data)
-  }
-}
-
 impl<'de> serde::Deserialize<'de> for DateCellData {
   fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
   where

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

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

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

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

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

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

+ 6 - 0
frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs

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

+ 205 - 0
frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs

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

+ 69 - 0
frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs

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

+ 15 - 6
frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs

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

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

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

+ 2 - 3
frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs

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

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

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

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

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

+ 33 - 13
frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs

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

+ 22 - 9
frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs

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

+ 25 - 12
frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs

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

+ 7 - 7
frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs

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