فهرست منبع

chore: update data & time format

appflowy 3 سال پیش
والد
کامیت
c6edd1a6da

+ 3 - 3
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/context_builder.dart

@@ -27,7 +27,7 @@ class GridCellContextBuilder {
           gridCell: _gridCell,
           cellCache: _cellCache,
           cellDataLoader: DateCellDataLoader(gridCell: _gridCell),
-          cellDataPersistence: NumberCellDataPersistence(gridCell: _gridCell),
+          cellDataPersistence: DateCellDataPersistence(gridCell: _gridCell),
         );
       case FieldType.Number:
         return GridCellContext(
@@ -120,7 +120,7 @@ class _GridCellContext<T, D> extends Equatable {
       _onFieldChangedFn = () {
         _loadData();
       };
-      cellCache.addListener(_cacheKey, _onFieldChangedFn!);
+      cellCache.addFieldListener(_cacheKey, _onFieldChangedFn!);
     }
 
     onCellChangedFn() {
@@ -172,7 +172,7 @@ class _GridCellContext<T, D> extends Equatable {
     _delayOperation?.cancel();
 
     if (_onFieldChangedFn != null) {
-      cellCache.removeListener(_cacheKey, _onFieldChangedFn!);
+      cellCache.removeFieldListener(_cacheKey, _onFieldChangedFn!);
       _onFieldChangedFn = null;
     }
   }

+ 11 - 11
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_cache.dart

@@ -30,7 +30,7 @@ class GridCellCache {
   final GridCellFieldDelegate fieldDelegate;
 
   /// fieldId: {objectId: callback}
-  final Map<String, Map<String, List<VoidCallback>>> _listenerByFieldId = {};
+  final Map<String, Map<String, List<VoidCallback>>> _fieldListenerByFieldId = {};
 
   /// fieldId: {cacheKey: cacheData}
   final Map<String, Map<String, dynamic>> _cellDataByFieldId = {};
@@ -40,7 +40,7 @@ class GridCellCache {
   }) {
     fieldDelegate.onFieldChanged((fieldId) {
       _cellDataByFieldId.remove(fieldId);
-      final map = _listenerByFieldId[fieldId];
+      final map = _fieldListenerByFieldId[fieldId];
       if (map != null) {
         for (final callbacks in map.values) {
           for (final callback in callbacks) {
@@ -51,24 +51,24 @@ class GridCellCache {
     });
   }
 
-  void addListener(GridCellCacheKey cacheKey, VoidCallback callback) {
-    var map = _listenerByFieldId[cacheKey.fieldId];
+  void addFieldListener(GridCellCacheKey cacheKey, VoidCallback onFieldChanged) {
+    var map = _fieldListenerByFieldId[cacheKey.fieldId];
     if (map == null) {
-      _listenerByFieldId[cacheKey.fieldId] = {};
-      map = _listenerByFieldId[cacheKey.fieldId];
-      map![cacheKey.objectId] = [callback];
+      _fieldListenerByFieldId[cacheKey.fieldId] = {};
+      map = _fieldListenerByFieldId[cacheKey.fieldId];
+      map![cacheKey.objectId] = [onFieldChanged];
     } else {
       var objects = map[cacheKey.objectId];
       if (objects == null) {
-        map[cacheKey.objectId] = [callback];
+        map[cacheKey.objectId] = [onFieldChanged];
       } else {
-        objects.add(callback);
+        objects.add(onFieldChanged);
       }
     }
   }
 
-  void removeListener(GridCellCacheKey cacheKey, VoidCallback fn) {
-    var callbacks = _listenerByFieldId[cacheKey.fieldId]?[cacheKey.objectId];
+  void removeFieldListener(GridCellCacheKey cacheKey, VoidCallback fn) {
+    var callbacks = _fieldListenerByFieldId[cacheKey.fieldId]?[cacheKey.objectId];
     final index = callbacks?.indexWhere((callback) => callback == fn);
     if (index != null && index != -1) {
       callbacks?.removeAt(index);

+ 3 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_loader.dart

@@ -61,12 +61,13 @@ class CellDataLoader extends _GridCellDataLoader<Cell> {
 
 class DateCellDataLoader extends _GridCellDataLoader<DateCellData> {
   final GridCell gridCell;
+  final GridCellDataConfig _config;
   DateCellDataLoader({
     required this.gridCell,
-  });
+  }) : _config = DefaultCellDataConfig(reloadOnFieldChanged: true);
 
   @override
-  GridCellDataConfig get config => DefaultCellDataConfig();
+  GridCellDataConfig get config => _config;
 
   @override
   Future<DateCellData?> loadData() {

+ 3 - 6
frontend/app_flowy/lib/workspace/application/grid/cell/cell_service/data_persistence.dart

@@ -35,9 +35,9 @@ class DateCellPersistenceData with _$DateCellPersistenceData {
   const factory DateCellPersistenceData({required DateTime date, String? time}) = _DateCellPersistenceData;
 }
 
-class NumberCellDataPersistence implements _GridCellDataPersistence<DateCellPersistenceData> {
+class DateCellDataPersistence implements _GridCellDataPersistence<DateCellPersistenceData> {
   final GridCell gridCell;
-  NumberCellDataPersistence({
+  DateCellDataPersistence({
     required this.gridCell,
   });
 
@@ -47,10 +47,7 @@ class NumberCellDataPersistence implements _GridCellDataPersistence<DateCellPers
 
     final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
     payload.date = date;
-
-    if (data.time != null) {
-      payload.time = data.time!;
-    }
+    payload.time = data.time ?? "";
 
     return GridEventUpdateDateCell(payload).send().then((result) {
       return result.fold(

+ 37 - 15
frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart

@@ -1,6 +1,6 @@
 import 'package:app_flowy/workspace/application/grid/field/field_service.dart';
 import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
@@ -25,8 +25,8 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
       (event, emit) async {
         await event.when(
           initial: () async => _startListening(),
-          selectDay: (date) {
-            _updateDateData(emit, date: date);
+          selectDay: (date) async {
+            await _updateDateData(emit, date: date, time: state.time);
           },
           setCalFormat: (format) {
             emit(state.copyWith(format: format));
@@ -44,22 +44,19 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
           setTimeFormat: (timeFormat) async {
             await _updateTypeOption(emit, timeFormat: timeFormat);
           },
-          setTime: (time) {
-            _updateDateData(emit, time: time);
+          setTime: (time) async {
+            await _updateDateData(emit, time: time);
           },
         );
       },
     );
   }
 
-  void _updateDateData(Emitter<DateCalState> emit, {DateTime? date, String? time}) {
-    state.dateData.fold(
+  Future<void> _updateDateData(Emitter<DateCalState> emit, {DateTime? date, String? time}) {
+    final DateCellPersistenceData newDateData = state.dateData.fold(
       () {
         var newDateData = DateCellPersistenceData(date: date ?? DateTime.now());
-        if (time != null) {
-          newDateData = newDateData.copyWith(time: time);
-        }
-        emit(state.copyWith(dateData: Some(newDateData)));
+        return newDateData.copyWith(time: time);
       },
       (dateData) {
         var newDateData = dateData;
@@ -70,9 +67,34 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
         if (newDateData.time != time) {
           newDateData = newDateData.copyWith(time: time);
         }
+        return newDateData;
+      },
+    );
 
-        if (newDateData != dateData) {
-          emit(state.copyWith(dateData: Some(newDateData)));
+    return _saveDateData(emit, newDateData);
+  }
+
+  Future<void> _saveDateData(Emitter<DateCalState> emit, DateCellPersistenceData newDateData) async {
+    if (state.dateData == Some(newDateData)) {
+      return;
+    }
+
+    final result = await cellContext.saveCellData(newDateData);
+    result.fold(
+      () => emit(state.copyWith(
+        dateData: Some(newDateData),
+        timeFormatError: none(),
+      )),
+      (err) {
+        switch (ErrorCode.valueOf(err.code)!) {
+          case ErrorCode.InvalidDateTimeFormat:
+            emit(state.copyWith(
+              dateData: Some(newDateData),
+              timeFormatError: Some(err.toString()),
+            ));
+            break;
+          default:
+            Log.error(err);
         }
       },
     );
@@ -152,7 +174,7 @@ class DateCalState with _$DateCalState {
     required CalendarFormat format,
     required DateTime focusedDay,
     required String time,
-    required Option<FlowyError> inputTimeError,
+    required Option<String> timeFormatError,
     required Option<DateCellPersistenceData> dateData,
   }) = _DateCalState;
 
@@ -175,7 +197,7 @@ class DateCalState with _$DateCalState {
       focusedDay: DateTime.now(),
       dateData: dateData,
       time: time,
-      inputTimeError: none(),
+      timeFormatError: none(),
     );
   }
 }

+ 0 - 2
frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart

@@ -16,7 +16,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
       (event, emit) async {
         event.when(
           initial: () => _startListening(),
-          selectDate: (DateCellPersistenceData value) => cellContext.saveCellData(value),
           didReceiveCellUpdate: (DateCellData value) => emit(state.copyWith(data: Some(value))),
           didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)),
         );
@@ -48,7 +47,6 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
 @freezed
 class DateCellEvent with _$DateCellEvent {
   const factory DateCellEvent.initial() = _InitialCell;
-  const factory DateCellEvent.selectDate(DateCellPersistenceData data) = _SelectDay;
   const factory DateCellEvent.didReceiveCellUpdate(DateCellData data) = _DidReceiveCellUpdate;
   const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate;
 }

+ 94 - 110
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/calendar.dart

@@ -32,7 +32,6 @@ class CellCalendar with FlowyOverlayDelegate {
   Future<void> show(
     BuildContext context, {
     required GridDateCellContext cellContext,
-    required void Function(DateCellPersistenceData) onSelected,
   }) async {
     CellCalendar.remove(context);
 
@@ -49,7 +48,6 @@ class CellCalendar with FlowyOverlayDelegate {
         // }
 
         final calendar = _CellCalendarWidget(
-          onSelected: onSelected,
           cellContext: cellContext,
           dateTypeOption: typeOptionData,
         );
@@ -88,10 +86,8 @@ class CellCalendar with FlowyOverlayDelegate {
 class _CellCalendarWidget extends StatelessWidget {
   final GridDateCellContext cellContext;
   final DateTypeOption dateTypeOption;
-  final void Function(DateCellPersistenceData) onSelected;
 
   const _CellCalendarWidget({
-    required this.onSelected,
     required this.cellContext,
     required this.dateTypeOption,
     Key? key,
@@ -108,40 +104,16 @@ class _CellCalendarWidget extends StatelessWidget {
           cellContext: cellContext,
         )..add(const DateCalEvent.initial());
       },
-      child: BlocConsumer<DateCalBloc, DateCalState>(
-        listener: (context, state) {
-          state.dateData.fold(
-            () => null,
-            (dateData) => onSelected(dateData),
-          );
-        },
-        listenWhen: (p, c) => p.dateData != c.dateData,
+      child: BlocBuilder<DateCalBloc, DateCalState>(
+        buildWhen: (p, c) => false,
         builder: (context, state) {
-          List<Widget> children = [];
-
-          children.addAll([
-            _buildCalendar(state, theme, context),
-            const VSpace(10),
-          ]);
-
-          if (state.dateTypeOption.includeTime) {
-            children.addAll([
-              _TimeTextField(
-                text: state.time,
-                errorText: state.inputTimeError.fold(() => "", (error) => error.toString()),
-                onEditingComplete: (text) {
-                  context.read<DateCalBloc>().add(DateCalEvent.setTime(text));
-                },
-              ),
-            ]);
-          }
-
-          children.addAll([
+          List<Widget> children = [
+            _buildCalendar(theme, context),
+            _TimeTextField(bloc: context.read<DateCalBloc>()),
             Divider(height: 1, color: theme.shader5),
             const _IncludeTimeButton(),
-          ]);
-
-          children.add(const _DateTypeOptionButton());
+            const _DateTypeOptionButton()
+          ];
 
           return ListView.separated(
             shrinkWrap: true,
@@ -159,56 +131,60 @@ class _CellCalendarWidget extends StatelessWidget {
     );
   }
 
-  TableCalendar<dynamic> _buildCalendar(DateCalState state, AppTheme theme, BuildContext context) {
-    return TableCalendar(
-      firstDay: kFirstDay,
-      lastDay: kLastDay,
-      focusedDay: state.focusedDay,
-      rowHeight: 40,
-      calendarFormat: state.format,
-      headerStyle: HeaderStyle(
-        formatButtonVisible: false,
-        titleCentered: true,
-        leftChevronMargin: EdgeInsets.zero,
-        leftChevronPadding: EdgeInsets.zero,
-        leftChevronIcon: svgWidget("home/arrow_left"),
-        rightChevronPadding: EdgeInsets.zero,
-        rightChevronMargin: EdgeInsets.zero,
-        rightChevronIcon: svgWidget("home/arrow_right"),
-      ),
-      calendarStyle: CalendarStyle(
-        selectedDecoration: BoxDecoration(
-          color: theme.main1,
-          shape: BoxShape.circle,
-        ),
-        todayDecoration: BoxDecoration(
-          color: theme.shader4,
-          shape: BoxShape.circle,
-        ),
-        selectedTextStyle: TextStyle(
-          color: theme.surface,
-          fontSize: 14.0,
-        ),
-        todayTextStyle: TextStyle(
-          color: theme.surface,
-          fontSize: 14.0,
-        ),
-      ),
-      selectedDayPredicate: (day) {
-        return state.dateData.fold(
-          () => false,
-          (dateData) => isSameDay(dateData.date, day),
+  Widget _buildCalendar(AppTheme theme, BuildContext context) {
+    return BlocBuilder<DateCalBloc, DateCalState>(
+      builder: (context, state) {
+        return TableCalendar(
+          firstDay: kFirstDay,
+          lastDay: kLastDay,
+          focusedDay: state.focusedDay,
+          rowHeight: 40,
+          calendarFormat: state.format,
+          headerStyle: HeaderStyle(
+            formatButtonVisible: false,
+            titleCentered: true,
+            leftChevronMargin: EdgeInsets.zero,
+            leftChevronPadding: EdgeInsets.zero,
+            leftChevronIcon: svgWidget("home/arrow_left"),
+            rightChevronPadding: EdgeInsets.zero,
+            rightChevronMargin: EdgeInsets.zero,
+            rightChevronIcon: svgWidget("home/arrow_right"),
+          ),
+          calendarStyle: CalendarStyle(
+            selectedDecoration: BoxDecoration(
+              color: theme.main1,
+              shape: BoxShape.circle,
+            ),
+            todayDecoration: BoxDecoration(
+              color: theme.shader4,
+              shape: BoxShape.circle,
+            ),
+            selectedTextStyle: TextStyle(
+              color: theme.surface,
+              fontSize: 14.0,
+            ),
+            todayTextStyle: TextStyle(
+              color: theme.surface,
+              fontSize: 14.0,
+            ),
+          ),
+          selectedDayPredicate: (day) {
+            return state.dateData.fold(
+              () => false,
+              (dateData) => isSameDay(dateData.date, day),
+            );
+          },
+          onDaySelected: (selectedDay, focusedDay) {
+            context.read<DateCalBloc>().add(DateCalEvent.selectDay(selectedDay));
+          },
+          onFormatChanged: (format) {
+            context.read<DateCalBloc>().add(DateCalEvent.setCalFormat(format));
+          },
+          onPageChanged: (focusedDay) {
+            context.read<DateCalBloc>().add(DateCalEvent.setFocusedDay(focusedDay));
+          },
         );
       },
-      onDaySelected: (selectedDay, focusedDay) {
-        context.read<DateCalBloc>().add(DateCalEvent.selectDay(selectedDay));
-      },
-      onFormatChanged: (format) {
-        context.read<DateCalBloc>().add(DateCalEvent.setCalFormat(format));
-      },
-      onPageChanged: (focusedDay) {
-        context.read<DateCalBloc>().add(DateCalEvent.setFocusedDay(focusedDay));
-      },
     );
   }
 }
@@ -246,14 +222,10 @@ class _IncludeTimeButton extends StatelessWidget {
 }
 
 class _TimeTextField extends StatefulWidget {
-  final String errorText;
-  final String text;
-  final void Function(String) onEditingComplete;
+  final DateCalBloc bloc;
   const _TimeTextField({
+    required this.bloc,
     Key? key,
-    required this.text,
-    required this.errorText,
-    required this.onEditingComplete,
   }) : super(key: key);
 
   @override
@@ -267,33 +239,45 @@ class _TimeTextFieldState extends State<_TimeTextField> {
   @override
   void initState() {
     _focusNode = FocusNode();
-    _controller = TextEditingController(text: widget.text);
-    _focusNode.addListener(() {
-      if (mounted) {
-        widget.onEditingComplete(_controller.text);
-      }
-    });
+    _controller = TextEditingController(text: widget.bloc.state.time);
+    if (widget.bloc.state.dateTypeOption.includeTime) {
+      _focusNode.addListener(() {
+        if (mounted) {
+          widget.bloc.add(DateCalEvent.setTime(_controller.text));
+        }
+      });
+    }
+
     super.initState();
   }
 
   @override
   Widget build(BuildContext context) {
     final theme = context.watch<AppTheme>();
-    return Padding(
-      padding: kMargin,
-      child: RoundedInputField(
-        height: 40,
-        focusNode: _focusNode,
-        controller: _controller,
-        style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
-        normalBorderColor: theme.shader4,
-        errorBorderColor: theme.red,
-        cursorColor: theme.main1,
-        errorText: widget.errorText,
-        onEditingComplete: (value) {
-          widget.onEditingComplete(value);
-        },
-      ),
+    return BlocBuilder<DateCalBloc, DateCalState>(
+      builder: (context, state) {
+        if (state.dateTypeOption.includeTime) {
+          return Padding(
+            padding: kMargin,
+            child: RoundedInputField(
+              height: 40,
+              focusNode: _focusNode,
+              controller: _controller,
+              style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
+              normalBorderColor: theme.shader4,
+              errorBorderColor: theme.red,
+              focusBorderColor: theme.main1,
+              cursorColor: theme.main1,
+              errorText: state.timeFormatError.fold(() => "", (error) => error),
+              onEditingComplete: (value) {
+                widget.bloc.add(DateCalEvent.setTime(value));
+              },
+            ),
+          );
+        } else {
+          return const SizedBox();
+        }
+      },
     );
   }
 

+ 0 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart

@@ -81,7 +81,6 @@ class _DateCellState extends State<DateCell> {
     calendar.show(
       context,
       cellContext: bloc.cellContext.clone(),
-      onSelected: (data) => bloc.add(DateCellEvent.selectDate(data)),
     );
   }
 

+ 3 - 1
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-error-code/code.pbenum.dart

@@ -52,7 +52,8 @@ class ErrorCode extends $pb.ProtobufEnum {
   static const ErrorCode FieldNotExists = ErrorCode._(443, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'FieldNotExists');
   static const ErrorCode FieldInvalidOperation = ErrorCode._(444, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'FieldInvalidOperation');
   static const ErrorCode TypeOptionDataIsEmpty = ErrorCode._(450, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'TypeOptionDataIsEmpty');
-  static const ErrorCode InvalidData = ErrorCode._(500, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'InvalidData');
+  static const ErrorCode InvalidDateTimeFormat = ErrorCode._(500, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'InvalidDateTimeFormat');
+  static const ErrorCode InvalidData = ErrorCode._(1000, const $core.bool.fromEnvironment('protobuf.omit_enum_names') ? '' : 'InvalidData');
 
   static const $core.List<ErrorCode> values = <ErrorCode> [
     Internal,
@@ -97,6 +98,7 @@ class ErrorCode extends $pb.ProtobufEnum {
     FieldNotExists,
     FieldInvalidOperation,
     TypeOptionDataIsEmpty,
+    InvalidDateTimeFormat,
     InvalidData,
   ];
 

+ 3 - 2
frontend/app_flowy/packages/flowy_sdk/lib/protobuf/flowy-error-code/code.pbjson.dart

@@ -54,9 +54,10 @@ const ErrorCode$json = const {
     const {'1': 'FieldNotExists', '2': 443},
     const {'1': 'FieldInvalidOperation', '2': 444},
     const {'1': 'TypeOptionDataIsEmpty', '2': 450},
-    const {'1': 'InvalidData', '2': 500},
+    const {'1': 'InvalidDateTimeFormat', '2': 500},
+    const {'1': 'InvalidData', '2': 1000},
   ],
 };
 
 /// Descriptor for `ErrorCode`. Decode as a `google.protobuf.EnumDescriptorProto`.
-final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode('CglFcnJvckNvZGUSDAoISW50ZXJuYWwQABIUChBVc2VyVW5hdXRob3JpemVkEAISEgoOUmVjb3JkTm90Rm91bmQQAxIRCg1Vc2VySWRJc0VtcHR5EAQSGAoUV29ya3NwYWNlTmFtZUludmFsaWQQZBIWChJXb3Jrc3BhY2VJZEludmFsaWQQZRIYChRBcHBDb2xvclN0eWxlSW52YWxpZBBmEhgKFFdvcmtzcGFjZURlc2NUb29Mb25nEGcSGAoUV29ya3NwYWNlTmFtZVRvb0xvbmcQaBIQCgxBcHBJZEludmFsaWQQbhISCg5BcHBOYW1lSW52YWxpZBBvEhMKD1ZpZXdOYW1lSW52YWxpZBB4EhgKFFZpZXdUaHVtYm5haWxJbnZhbGlkEHkSEQoNVmlld0lkSW52YWxpZBB6EhMKD1ZpZXdEZXNjVG9vTG9uZxB7EhMKD1ZpZXdEYXRhSW52YWxpZBB8EhMKD1ZpZXdOYW1lVG9vTG9uZxB9EhEKDENvbm5lY3RFcnJvchDIARIRCgxFbWFpbElzRW1wdHkQrAISFwoSRW1haWxGb3JtYXRJbnZhbGlkEK0CEhcKEkVtYWlsQWxyZWFkeUV4aXN0cxCuAhIUCg9QYXNzd29yZElzRW1wdHkQrwISFAoPUGFzc3dvcmRUb29Mb25nELACEiUKIFBhc3N3b3JkQ29udGFpbnNGb3JiaWRDaGFyYWN0ZXJzELECEhoKFVBhc3N3b3JkRm9ybWF0SW52YWxpZBCyAhIVChBQYXNzd29yZE5vdE1hdGNoELMCEhQKD1VzZXJOYW1lVG9vTG9uZxC0AhInCiJVc2VyTmFtZUNvbnRhaW5Gb3JiaWRkZW5DaGFyYWN0ZXJzELUCEhQKD1VzZXJOYW1lSXNFbXB0eRC2AhISCg1Vc2VySWRJbnZhbGlkELcCEhEKDFVzZXJOb3RFeGlzdBC4AhIQCgtUZXh0VG9vTG9uZxCQAxISCg1HcmlkSWRJc0VtcHR5EJoDEhMKDkJsb2NrSWRJc0VtcHR5EKQDEhEKDFJvd0lkSXNFbXB0eRCuAxIUCg9PcHRpb25JZElzRW1wdHkQrwMSEwoORmllbGRJZElzRW1wdHkQuAMSFgoRRmllbGREb2VzTm90RXhpc3QQuQMSHAoXU2VsZWN0T3B0aW9uTmFtZUlzRW1wdHkQugMSEwoORmllbGROb3RFeGlzdHMQuwMSGgoVRmllbGRJbnZhbGlkT3BlcmF0aW9uELwDEhoKFVR5cGVPcHRpb25EYXRhSXNFbXB0eRDCAxIQCgtJbnZhbGlkRGF0YRD0Aw==');
+final $typed_data.Uint8List errorCodeDescriptor = $convert.base64Decode('CglFcnJvckNvZGUSDAoISW50ZXJuYWwQABIUChBVc2VyVW5hdXRob3JpemVkEAISEgoOUmVjb3JkTm90Rm91bmQQAxIRCg1Vc2VySWRJc0VtcHR5EAQSGAoUV29ya3NwYWNlTmFtZUludmFsaWQQZBIWChJXb3Jrc3BhY2VJZEludmFsaWQQZRIYChRBcHBDb2xvclN0eWxlSW52YWxpZBBmEhgKFFdvcmtzcGFjZURlc2NUb29Mb25nEGcSGAoUV29ya3NwYWNlTmFtZVRvb0xvbmcQaBIQCgxBcHBJZEludmFsaWQQbhISCg5BcHBOYW1lSW52YWxpZBBvEhMKD1ZpZXdOYW1lSW52YWxpZBB4EhgKFFZpZXdUaHVtYm5haWxJbnZhbGlkEHkSEQoNVmlld0lkSW52YWxpZBB6EhMKD1ZpZXdEZXNjVG9vTG9uZxB7EhMKD1ZpZXdEYXRhSW52YWxpZBB8EhMKD1ZpZXdOYW1lVG9vTG9uZxB9EhEKDENvbm5lY3RFcnJvchDIARIRCgxFbWFpbElzRW1wdHkQrAISFwoSRW1haWxGb3JtYXRJbnZhbGlkEK0CEhcKEkVtYWlsQWxyZWFkeUV4aXN0cxCuAhIUCg9QYXNzd29yZElzRW1wdHkQrwISFAoPUGFzc3dvcmRUb29Mb25nELACEiUKIFBhc3N3b3JkQ29udGFpbnNGb3JiaWRDaGFyYWN0ZXJzELECEhoKFVBhc3N3b3JkRm9ybWF0SW52YWxpZBCyAhIVChBQYXNzd29yZE5vdE1hdGNoELMCEhQKD1VzZXJOYW1lVG9vTG9uZxC0AhInCiJVc2VyTmFtZUNvbnRhaW5Gb3JiaWRkZW5DaGFyYWN0ZXJzELUCEhQKD1VzZXJOYW1lSXNFbXB0eRC2AhISCg1Vc2VySWRJbnZhbGlkELcCEhEKDFVzZXJOb3RFeGlzdBC4AhIQCgtUZXh0VG9vTG9uZxCQAxISCg1HcmlkSWRJc0VtcHR5EJoDEhMKDkJsb2NrSWRJc0VtcHR5EKQDEhEKDFJvd0lkSXNFbXB0eRCuAxIUCg9PcHRpb25JZElzRW1wdHkQrwMSEwoORmllbGRJZElzRW1wdHkQuAMSFgoRRmllbGREb2VzTm90RXhpc3QQuQMSHAoXU2VsZWN0T3B0aW9uTmFtZUlzRW1wdHkQugMSEwoORmllbGROb3RFeGlzdHMQuwMSGgoVRmllbGRJbnZhbGlkT3BlcmF0aW9uELwDEhoKFVR5cGVPcHRpb25EYXRhSXNFbXB0eRDCAxIaChVJbnZhbGlkRGF0ZVRpbWVGb3JtYXQQ9AMSEAoLSW52YWxpZERhdGEQ6Ac=');

+ 111 - 54
frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs

@@ -5,6 +5,7 @@ use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellD
 use bytes::Bytes;
 use chrono::format::strftime::StrftimeItems;
 use chrono::NaiveDateTime;
+use diesel::types::Time;
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult};
 use flowy_grid_data_model::entities::{
@@ -30,37 +31,28 @@ pub struct DateTypeOption {
 impl_type_option!(DateTypeOption, FieldType::DateTime);
 
 impl DateTypeOption {
-    fn today_desc_from_timestamp(&self, timestamp: i64) -> String {
+    fn today_desc_from_timestamp(&self, timestamp: i64, time: &Option<String>) -> String {
         let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0);
-        self.today_desc_from_native(native)
+        self.today_desc_from_native(native, time)
     }
 
     #[allow(dead_code)]
-    fn today_desc_from_str(&self, s: String) -> String {
-        match NaiveDateTime::parse_from_str(&s, &self.fmt_str()) {
-            Ok(native) => self.today_desc_from_native(native),
+    fn today_desc_from_str(&self, s: String, time: &Option<String>) -> String {
+        match NaiveDateTime::parse_from_str(&s, &self.date_fmt(time)) {
+            Ok(native) => self.today_desc_from_native(native, time),
             Err(_) => "".to_owned(),
         }
     }
 
-    fn today_desc_from_native(&self, native: chrono::NaiveDateTime) -> String {
+    fn today_desc_from_native(&self, native: chrono::NaiveDateTime, time: &Option<String>) -> String {
         let utc = self.utc_date_time_from_native(native);
         // let china_timezone = FixedOffset::east(8 * 3600);
         // let a = utc.with_timezone(&china_timezone);
-        let output = format!("{}", utc.format_with_items(StrftimeItems::new(&self.fmt_str())));
+        let fmt = self.date_fmt(time);
+        let output = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt)));
         output
     }
 
-    fn timestamp_from_str(&self, s: &str) -> FlowyResult<i64> {
-        match NaiveDateTime::parse_from_str(s, &self.fmt_str()) {
-            Ok(native) => {
-                let utc = self.utc_date_time_from_native(native);
-                Ok(utc.timestamp())
-            }
-            Err(_) => Err(ErrorCode::InvalidData.into()),
-        }
-    }
-
     fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime<chrono::Utc> {
         let native = NaiveDateTime::from_timestamp(timestamp, 0);
         self.utc_date_time_from_native(native)
@@ -70,9 +62,18 @@ impl DateTypeOption {
         chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
     }
 
-    fn fmt_str(&self) -> String {
+    fn date_fmt(&self, time: &Option<String>) -> String {
         if self.include_time {
-            format!("{} {}", self.date_format.format_str(), self.time_format.format_str())
+            match time.as_ref() {
+                None => self.date_format.format_str().to_string(),
+                Some(time_str) => {
+                    if time_str.is_empty() {
+                        self.date_format.format_str().to_string()
+                    } else {
+                        format!("{} {}", self.date_format.format_str(), self.time_format.format_str())
+                    }
+                }
+            }
         } else {
             self.date_format.format_str().to_string()
         }
@@ -90,29 +91,48 @@ impl DateTypeOption {
         }
 
         let serde_cell_data = DateCellDataSerde::from_str(&result.unwrap().data)?;
-        let time = serde_cell_data.time;
+        let date = self.decode_cell_data_from_timestamp(&serde_cell_data).content;
+        let time = serde_cell_data.time.unwrap_or("".to_owned());
         let timestamp = serde_cell_data.timestamp;
-        let date = self.decode_cell_data_from_timestamp(serde_cell_data.timestamp).content;
 
         return Ok(DateCellData { date, time, timestamp });
     }
 
-    fn decode_cell_data_from_timestamp(&self, timestamp: i64) -> DecodedCellData {
-        if timestamp == 0 {
+    fn decode_cell_data_from_timestamp(&self, serde_cell_data: &DateCellDataSerde) -> DecodedCellData {
+        if serde_cell_data.timestamp == 0 {
             return DecodedCellData::default();
         }
 
-        let cell_content = self.today_desc_from_timestamp(timestamp);
-        return DecodedCellData::new(timestamp.to_string(), cell_content);
+        let cell_content = self.today_desc_from_timestamp(serde_cell_data.timestamp, &serde_cell_data.time);
+        return DecodedCellData::new(serde_cell_data.timestamp.to_string(), cell_content);
     }
 
-    fn timestamp_from_utc_with_time(&self, utc: &chrono::DateTime<chrono::Utc>, time: &str) -> FlowyResult<i64> {
+    fn timestamp_from_utc_with_time(
+        &self,
+        utc: &chrono::DateTime<chrono::Utc>,
+        time: &Option<String>,
+    ) -> FlowyResult<i64> {
         let mut date_str = format!(
             "{}",
             utc.format_with_items(StrftimeItems::new(self.date_format.format_str()))
         );
-        date_str = date_str.add(&time);
-        self.timestamp_from_str(&date_str)
+
+        if let Some(time_str) = time.as_ref() {
+            if !time_str.is_empty() {
+                date_str = date_str.add(&time_str);
+            }
+        }
+        let fmt = self.date_fmt(time);
+        match NaiveDateTime::parse_from_str(&date_str, &fmt) {
+            Ok(native) => {
+                let utc = self.utc_date_time_from_native(native);
+                Ok(utc.timestamp())
+            }
+            Err(_e) => {
+                let msg = format!("Parse {} with format: {} failed", date_str, fmt);
+                Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg))
+            }
+        }
     }
 }
 
@@ -127,7 +147,7 @@ impl CellDataOperation for DateTypeOption {
                 return DecodedCellData::default();
             }
             return match DateCellDataSerde::from_str(&type_option_cell_data.data) {
-                Ok(serde_cell_data) => self.decode_cell_data_from_timestamp(serde_cell_data.timestamp),
+                Ok(serde_cell_data) => self.decode_cell_data_from_timestamp(&serde_cell_data),
                 Err(_) => DecodedCellData::default(),
             };
         }
@@ -145,12 +165,12 @@ impl CellDataOperation for DateTypeOption {
             None => DateCellDataSerde::default(),
             Some(date_timestamp) => match (self.include_time, content_changeset.time) {
                 (true, Some(time)) => {
-                    let time = time.to_uppercase();
+                    let time = Some(time.trim().to_uppercase());
                     let utc = self.utc_date_time_from_timestamp(date_timestamp);
                     let timestamp = self.timestamp_from_utc_with_time(&utc, &time)?;
-                    DateCellDataSerde { timestamp, time }
+                    DateCellDataSerde::new(timestamp, time, &self.time_format)
                 }
-                _ => DateCellDataSerde::from_timestamp(date_timestamp),
+                _ => DateCellDataSerde::from_timestamp(date_timestamp, &self.time_format),
             },
         };
 
@@ -281,14 +301,21 @@ pub struct DateCellData {
 #[derive(Default, Serialize, Deserialize)]
 pub struct DateCellDataSerde {
     pub timestamp: i64,
-    pub time: String,
+    pub time: Option<String>,
 }
 
 impl DateCellDataSerde {
-    fn from_timestamp(timestamp: i64) -> Self {
+    fn new(timestamp: i64, time: Option<String>, time_format: &TimeFormat) -> Self {
+        Self {
+            timestamp,
+            time: Some(time.unwrap_or(default_time_str(time_format))),
+        }
+    }
+
+    fn from_timestamp(timestamp: i64, time_format: &TimeFormat) -> Self {
         Self {
             timestamp,
-            time: "".to_string(),
+            time: Some(default_time_str(time_format)),
         }
     }
 
@@ -301,6 +328,13 @@ impl DateCellDataSerde {
     }
 }
 
+fn default_time_str(time_format: &TimeFormat) -> String {
+    match time_format {
+        TimeFormat::TwelveHour => "12:00 AM".to_string(),
+        TimeFormat::TwentyFourHour => "00:00".to_string(),
+    }
+}
+
 #[derive(Clone, Debug, Default, ProtoBuf)]
 pub struct DateChangesetPayload {
     #[pb(index = 1)]
@@ -439,7 +473,7 @@ mod tests {
                 TimeFormat::TwentyFourHour => {
                     assert_eq!(
                         "Mar 14,2022".to_owned(),
-                        type_option.today_desc_from_timestamp(1647251762)
+                        type_option.today_desc_from_timestamp(1647251762, &None)
                     );
                     assert_eq!(
                         "Mar 14,2022".to_owned(),
@@ -449,7 +483,7 @@ mod tests {
                 TimeFormat::TwelveHour => {
                     assert_eq!(
                         "Mar 14,2022".to_owned(),
-                        type_option.today_desc_from_timestamp(1647251762)
+                        type_option.today_desc_from_timestamp(1647251762, &None)
                     );
                     assert_eq!(
                         "Mar 14,2022".to_owned(),
@@ -469,24 +503,47 @@ mod tests {
             type_option.include_time = true;
             match time_format {
                 TimeFormat::TwentyFourHour => {
-                    assert_eq!(
-                        "May 27,2022 00:00".to_owned(),
-                        type_option.today_desc_from_timestamp(1653609600)
-                    );
-                    assert_eq!(
-                        "May 27,2022 00:00".to_owned(),
-                        type_option.decode_cell_data(data(1653609600), &field_meta).content
-                    );
+                    let changeset = DateCellContentChangeset {
+                        date: Some(1653609600.to_string()),
+                        time: None,
+                    };
+                    let result = type_option.apply_changeset(changeset, None).unwrap();
+                    let content = type_option.decode_cell_data(result, &field_meta).content;
+                    assert_eq!("May 27,2022 00:00".to_owned(), content);
+
+                    let changeset = DateCellContentChangeset {
+                        date: Some(1653609600.to_string()),
+                        time: Some("23:00".to_owned()),
+                    };
+
+                    let result = type_option.apply_changeset(changeset, None).unwrap();
+                    let content = type_option.decode_cell_data(result, &field_meta).content;
+                    assert_eq!("May 27,2022 23:00".to_owned(), content);
                 }
                 TimeFormat::TwelveHour => {
-                    assert_eq!(
-                        "May 27,2022 12:00 AM".to_owned(),
-                        type_option.today_desc_from_timestamp(1653609600)
-                    );
-                    assert_eq!(
-                        "May 27,2022 12:00 AM".to_owned(),
-                        type_option.decode_cell_data(data(1653609600), &field_meta).content
-                    );
+                    let changeset = DateCellContentChangeset {
+                        date: Some(1653609600.to_string()),
+                        time: None,
+                    };
+                    let result = type_option.apply_changeset(changeset, None).unwrap();
+                    let content = type_option.decode_cell_data(result, &field_meta).content;
+                    assert_eq!("May 27,2022 12:00 AM".to_owned(), content);
+
+                    let changeset = DateCellContentChangeset {
+                        date: Some(1653609600.to_string()),
+                        time: Some("".to_owned()),
+                    };
+                    let result = type_option.apply_changeset(changeset, None).unwrap();
+                    let content = type_option.decode_cell_data(result, &field_meta).content;
+                    assert_eq!("May 27,2022".to_owned(), content);
+
+                    let changeset = DateCellContentChangeset {
+                        date: Some(1653609600.to_string()),
+                        time: Some("11:23 pm".to_owned()),
+                    };
+                    let result = type_option.apply_changeset(changeset, None).unwrap();
+                    let content = type_option.decode_cell_data(result, &field_meta).content;
+                    assert_eq!("May 27,2022 11:23 PM".to_owned(), content);
                 }
             }
         }
@@ -559,7 +616,7 @@ mod tests {
     fn data(s: i64) -> String {
         let json = serde_json::to_string(&DateCellDataSerde {
             timestamp: s,
-            time: "".to_string(),
+            time: None,
         })
         .unwrap();
         TypeOptionCellData::new(&json, FieldType::DateTime).json()

+ 4 - 1
shared-lib/flowy-error-code/src/code.rs

@@ -111,8 +111,11 @@ pub enum ErrorCode {
     #[display(fmt = "Field's type option data should not be empty")]
     TypeOptionDataIsEmpty = 450,
 
+    #[display(fmt = "Invalid date time format")]
+    InvalidDateTimeFormat = 500,
+
     #[display(fmt = "Invalid data")]
-    InvalidData = 500,
+    InvalidData = 1000,
 }
 
 impl ErrorCode {

+ 9 - 5
shared-lib/flowy-error-code/src/protobuf/model/code.rs

@@ -67,7 +67,8 @@ pub enum ErrorCode {
     FieldNotExists = 443,
     FieldInvalidOperation = 444,
     TypeOptionDataIsEmpty = 450,
-    InvalidData = 500,
+    InvalidDateTimeFormat = 500,
+    InvalidData = 1000,
 }
 
 impl ::protobuf::ProtobufEnum for ErrorCode {
@@ -119,7 +120,8 @@ impl ::protobuf::ProtobufEnum for ErrorCode {
             443 => ::std::option::Option::Some(ErrorCode::FieldNotExists),
             444 => ::std::option::Option::Some(ErrorCode::FieldInvalidOperation),
             450 => ::std::option::Option::Some(ErrorCode::TypeOptionDataIsEmpty),
-            500 => ::std::option::Option::Some(ErrorCode::InvalidData),
+            500 => ::std::option::Option::Some(ErrorCode::InvalidDateTimeFormat),
+            1000 => ::std::option::Option::Some(ErrorCode::InvalidData),
             _ => ::std::option::Option::None
         }
     }
@@ -168,6 +170,7 @@ impl ::protobuf::ProtobufEnum for ErrorCode {
             ErrorCode::FieldNotExists,
             ErrorCode::FieldInvalidOperation,
             ErrorCode::TypeOptionDataIsEmpty,
+            ErrorCode::InvalidDateTimeFormat,
             ErrorCode::InvalidData,
         ];
         values
@@ -197,7 +200,7 @@ impl ::protobuf::reflect::ProtobufValue for ErrorCode {
 }
 
 static file_descriptor_proto_data: &'static [u8] = b"\
-    \n\ncode.proto*\xe5\x07\n\tErrorCode\x12\x0c\n\x08Internal\x10\0\x12\x14\
+    \n\ncode.proto*\x81\x08\n\tErrorCode\x12\x0c\n\x08Internal\x10\0\x12\x14\
     \n\x10UserUnauthorized\x10\x02\x12\x12\n\x0eRecordNotFound\x10\x03\x12\
     \x11\n\rUserIdIsEmpty\x10\x04\x12\x18\n\x14WorkspaceNameInvalid\x10d\x12\
     \x16\n\x12WorkspaceIdInvalid\x10e\x12\x18\n\x14AppColorStyleInvalid\x10f\
@@ -220,8 +223,9 @@ static file_descriptor_proto_data: &'static [u8] = b"\
     \x12\x13\n\x0eFieldIdIsEmpty\x10\xb8\x03\x12\x16\n\x11FieldDoesNotExist\
     \x10\xb9\x03\x12\x1c\n\x17SelectOptionNameIsEmpty\x10\xba\x03\x12\x13\n\
     \x0eFieldNotExists\x10\xbb\x03\x12\x1a\n\x15FieldInvalidOperation\x10\
-    \xbc\x03\x12\x1a\n\x15TypeOptionDataIsEmpty\x10\xc2\x03\x12\x10\n\x0bInv\
-    alidData\x10\xf4\x03b\x06proto3\
+    \xbc\x03\x12\x1a\n\x15TypeOptionDataIsEmpty\x10\xc2\x03\x12\x1a\n\x15Inv\
+    alidDateTimeFormat\x10\xf4\x03\x12\x10\n\x0bInvalidData\x10\xe8\x07b\x06\
+    proto3\
 ";
 
 static file_descriptor_proto_lazy: ::protobuf::rt::LazyV2<::protobuf::descriptor::FileDescriptorProto> = ::protobuf::rt::LazyV2::INIT;

+ 2 - 1
shared-lib/flowy-error-code/src/protobuf/proto/code.proto

@@ -43,5 +43,6 @@ enum ErrorCode {
     FieldNotExists = 443;
     FieldInvalidOperation = 444;
     TypeOptionDataIsEmpty = 450;
-    InvalidData = 500;
+    InvalidDateTimeFormat = 500;
+    InvalidData = 1000;
 }