From 376f2d887df35134dc0b118ff49ad85429b089c5 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 9 May 2023 22:37:20 +0800 Subject: [PATCH] feat: rewrite date logic (#2390) * chore: remove unused fields * chore: rewrite date logic * chore: apply suggestions from Alex * chore: add space in date format * chore: re-add error handling in apply-changeset --- .../cell/cell_data_persistence.dart | 17 +- .../application/database_controller.dart | 3 +- .../field/type_option/date_bloc.dart | 4 - .../row/cells/date_cell/date_cal_bloc.dart | 206 ++++----- .../row/cells/date_cell/date_cell_bloc.dart | 6 +- .../row/cells/date_cell/date_editor.dart | 116 ++--- .../lib/plugins/trash/src/trash_cell.dart | 6 +- frontend/rust-lib/Cargo.lock | 67 ++- frontend/rust-lib/flowy-database2/Cargo.toml | 1 + .../type_option_entities/date_entities.rs | 12 +- .../flowy-database2/src/event_handler.rs | 12 +- .../src/services/cell/cell_operation.rs | 26 +- .../src/services/database/database_editor.rs | 49 +- .../date_type_option/date_tests.rs | 423 ++++++++++++++---- .../date_type_option/date_type_option.rs | 190 +++++--- .../date_type_option_entities.rs | 27 +- .../text_type_option/text_tests.rs | 5 +- .../tests/database/database_editor.rs | 2 +- .../tests/database/field_test/util.rs | 2 +- .../database/mock_data/board_mock_data.rs | 1 - .../database/mock_data/grid_mock_data.rs | 1 - 21 files changed, 765 insertions(+), 411 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart index dd5e56e08c..687a37871e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_persistence.dart @@ -29,7 +29,7 @@ class TextCellDataPersistence implements CellDataPersistence { @freezed class DateCellData with _$DateCellData { const factory DateCellData({ - required DateTime date, + DateTime? dateTime, String? time, required bool includeTime, }) = _DateCellData; @@ -45,19 +45,14 @@ class DateCellDataPersistence implements CellDataPersistence { Future> save(DateCellData data) { var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId); - // This is a bit of a hack. This converts the data.date which is in - // UTC to Local but actually changes the timestamp instead of just - // changing the isUtc flag - final dateTime = DateTime(data.date.year, data.date.month, data.date.day); - - final date = (dateTime.millisecondsSinceEpoch ~/ 1000).toString(); - payload.date = date; - payload.isUtc = data.date.isUtc; - payload.includeTime = data.includeTime; - + if (data.dateTime != null) { + final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString(); + payload.date = date; + } if (data.time != null) { payload.time = data.time!; } + payload.includeTime = data.includeTime; return DatabaseEventUpdateDateCell(payload).send().then((result) { return result.fold( diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart index 6f25c147bb..4824d68a74 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/database_controller.dart @@ -342,10 +342,9 @@ class RowDataBuilder { _cellDataByFieldId[fieldInfo.field.id] = num.toString(); } - /// The date should use the UTC timezone. Becuase the backend uses UTC timezone to format the time string. void insertDate(FieldInfo fieldInfo, DateTime date) { assert(fieldInfo.fieldType == FieldType.DateTime); - final timestamp = (date.toUtc().millisecondsSinceEpoch ~/ 1000); + final timestamp = date.millisecondsSinceEpoch ~/ 1000; _cellDataByFieldId[fieldInfo.field.id] = timestamp.toString(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart index fd1c69036c..637557c75e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart @@ -53,10 +53,6 @@ class DateTypeOptionBloc if (timeFormat != null) { typeOption.timeFormat = timeFormat; } - - if (includeTime != null) { - typeOption.includeTime = includeTime; - } }); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index 0b1e1c826e..05dd462902 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart @@ -12,7 +12,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:table_calendar/table_calendar.dart'; import 'dart:async'; -import 'package:dartz/dartz.dart'; import 'package:protobuf/protobuf.dart'; part 'date_cal_bloc.freezed.dart'; @@ -31,45 +30,39 @@ class DateCellCalendarBloc (event, emit) async { await event.when( initial: () async => _startListening(), - selectDay: (date) async { - await _updateDateData(emit, date: date, time: state.time); - }, - setCalFormat: (format) { - emit(state.copyWith(format: format)); - }, - setFocusedDay: (focusedDay) { - emit(state.copyWith(focusedDay: focusedDay)); - }, didReceiveCellUpdate: (DateCellDataPB? cellData) { - final dateCellData = calDataFromCellData(cellData); - final time = dateCellData.foldRight( - "", - (dateData, previous) => dateData.time ?? '', + final dateData = _dateDataFromCellData(cellData); + emit( + state.copyWith( + dateTime: dateData.dateTime, + time: dateData.time, + includeTime: dateData.includeTime, + ), ); - emit(state.copyWith(dateCellData: dateCellData, time: time)); + }, + didReceiveTimeFormatError: (String? timeFormatError) { + emit(state.copyWith(timeFormatError: timeFormatError)); + }, + selectDay: (date) async { + await _updateDateData(emit, date: date); }, setIncludeTime: (includeTime) async { await _updateDateData(emit, includeTime: includeTime); }, + setTime: (time) async { + await _updateDateData(emit, time: time); + }, setDateFormat: (dateFormat) async { await _updateTypeOption(emit, dateFormat: dateFormat); }, setTimeFormat: (timeFormat) async { await _updateTypeOption(emit, timeFormat: timeFormat); }, - setTime: (time) async { - if (state.dateCellData.isSome()) { - await _updateDateData(emit, time: time); - } + setCalFormat: (format) { + emit(state.copyWith(format: format)); }, - didUpdateCalData: - (Option data, Option timeFormatError) { - emit( - state.copyWith( - dateCellData: data, - timeFormatError: timeFormatError, - ), - ); + setFocusedDay: (focusedDay) { + emit(state.copyWith(focusedDay: focusedDay)); }, ); }, @@ -81,65 +74,44 @@ class DateCellCalendarBloc DateTime? date, String? time, bool? includeTime, - }) { - final DateCellData newDateData = state.dateCellData.fold( - () => DateCellData( - date: date ?? DateTime.now(), - time: time, - includeTime: includeTime ?? false, - ), - (dateData) { - var newDateData = dateData; - if (date != null && !isSameDay(newDateData.date, date)) { - newDateData = newDateData.copyWith(date: date); - } + }) async { + // make sure date and time are not updated together from the UI + assert( + date == null && time == null || + date == null && time != null || + date != null && time == null, + ); + String? newTime = time ?? state.time; - if (newDateData.time != time) { - newDateData = newDateData.copyWith(time: time); - } + DateTime? newDate = date; + if (time != null && time.isNotEmpty) { + newDate = state.dateTime ?? DateTime.now(); + } - if (includeTime != null && newDateData.includeTime != includeTime) { - newDateData = newDateData.copyWith(includeTime: includeTime); - } - - return newDateData; - }, + final DateCellData newDateData = DateCellData( + dateTime: newDate, + time: newTime, + includeTime: includeTime ?? state.includeTime, ); - return _saveDateData(emit, newDateData); - } - - Future _saveDateData( - Emitter emit, - DateCellData newCalData, - ) async { - if (state.dateCellData == Some(newCalData)) { - return; - } - - updateCalData( - Option dateCellData, - Option timeFormatError, - ) { - if (!isClosed) { - add( - DateCellCalendarEvent.didUpdateCalData( - dateCellData, - timeFormatError, - ), - ); - } - } - cellController.saveCellData( - newCalData, + newDateData, onFinish: (result) { result.fold( - () => updateCalData(Some(newCalData), none()), + () { + if (!isClosed && state.timeFormatError != null) { + add(const DateCellCalendarEvent.didReceiveTimeFormatError(null)); + } + }, (err) { switch (ErrorCode.valueOf(err.code)!) { case ErrorCode.InvalidDateTimeFormat: - updateCalData(state.dateCellData, Some(timeFormatPrompt(err))); + if (isClosed) return; + add( + DateCellCalendarEvent.didReceiveTimeFormatError( + timeFormatPrompt(err), + ), + ); break; default: Log.error(err); @@ -221,25 +193,33 @@ class DateCellCalendarBloc @freezed class DateCellCalendarEvent with _$DateCellCalendarEvent { + // initial event const factory DateCellCalendarEvent.initial() = _Initial; - const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay; + + // notification that cell is updated in the backend + const factory DateCellCalendarEvent.didReceiveCellUpdate( + DateCellDataPB? data, + ) = _DidReceiveCellUpdate; + const factory DateCellCalendarEvent.didReceiveTimeFormatError( + String? timeformatError, + ) = _DidReceiveTimeFormatError; + + // table calendar's UI settings + const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay; const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) = _CalendarFormat; - const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay; + + // date cell data is modified + const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay; + const factory DateCellCalendarEvent.setTime(String time) = _Time; + const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) = + _IncludeTime; + + // date field type options are modified const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) = _TimeFormat; const factory DateCellCalendarEvent.setDateFormat(DateFormatPB dateFormat) = _DateFormat; - const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) = - _IncludeTime; - const factory DateCellCalendarEvent.setTime(String time) = _Time; - const factory DateCellCalendarEvent.didReceiveCellUpdate( - DateCellDataPB? data, - ) = _DidReceiveCellUpdate; - const factory DateCellCalendarEvent.didUpdateCalData( - Option data, - Option timeFormatError, - ) = _DidUpdateCalData; } @freezed @@ -248,9 +228,10 @@ class DateCellCalendarState with _$DateCellCalendarState { required DateTypeOptionPB dateTypeOptionPB, required CalendarFormat format, required DateTime focusedDay, - required Option timeFormatError, - required Option dateCellData, + required DateTime? dateTime, required String? time, + required bool includeTime, + required String? timeFormatError, required String timeHintText, }) = _DateCellCalendarState; @@ -258,16 +239,15 @@ class DateCellCalendarState with _$DateCellCalendarState { DateTypeOptionPB dateTypeOptionPB, DateCellDataPB? cellData, ) { - Option dateCellData = calDataFromCellData(cellData); - final time = - dateCellData.foldRight("", (dateData, previous) => dateData.time ?? ''); + final dateData = _dateDataFromCellData(cellData); return DateCellCalendarState( dateTypeOptionPB: dateTypeOptionPB, format: CalendarFormat.month, focusedDay: DateTime.now(), - time: time, - dateCellData: dateCellData, - timeFormatError: none(), + dateTime: dateData.dateTime, + time: dateData.time, + includeTime: dateData.includeTime, + timeFormatError: null, timeHintText: _timeHintText(dateTypeOptionPB), ); } @@ -284,27 +264,21 @@ String _timeHintText(DateTypeOptionPB typeOption) { } } -Option calDataFromCellData(DateCellDataPB? cellData) { - String? time = timeFromCellData(cellData); - Option dateData = none(); - if (cellData != null) { +DateCellData _dateDataFromCellData(DateCellDataPB? cellData) { + // a null DateCellDataPB may be returned, indicating that all the fields are + // at their default values: empty strings and false booleans + if (cellData == null) { + return const DateCellData(includeTime: false); + } + + DateTime? dateTime; + String? time; + if (cellData.hasTimestamp()) { final timestamp = cellData.timestamp * 1000; - final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); - dateData = Some( - DateCellData( - date: date, - time: time, - includeTime: cellData.includeTime, - ), - ); + dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); + time = cellData.time; } - return dateData; -} + bool includeTime = cellData.includeTime; -String? timeFromCellData(DateCellDataPB? cellData) { - if (cellData == null || !cellData.hasTime()) { - return null; - } - - return cellData.time; + return DateCellData(dateTime: dateTime, time: time, includeTime: includeTime); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart index 475a3c6b6c..fd62119997 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell_bloc.dart @@ -79,7 +79,11 @@ class DateCellState with _$DateCellState { String _dateStrFromCellData(DateCellDataPB? cellData) { String dateStr = ""; if (cellData != null) { - dateStr = "${cellData.date} ${cellData.time}"; + if (cellData.includeTime) { + dateStr = "${cellData.date} ${cellData.time}"; + } else { + dateStr = cellData.date; + } } return dateStr; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index 1bf7505f9b..584f86fbad 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -10,8 +10,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/rounded_input_field.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; import 'package:flutter/material.dart'; @@ -89,41 +89,35 @@ class _CellCalendarWidget extends StatefulWidget { class _CellCalendarWidgetState extends State<_CellCalendarWidget> { late PopoverMutex popoverMutex; - late DateCellCalendarBloc bloc; @override void initState() { popoverMutex = PopoverMutex(); - bloc = DateCellCalendarBloc( - dateTypeOptionPB: widget.dateTypeOptionPB, - cellData: widget.cellContext.getCellData(), - cellController: widget.cellContext, - )..add(const DateCellCalendarEvent.initial()); super.initState(); } @override Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, + return BlocProvider( + create: (context) => DateCellCalendarBloc( + dateTypeOptionPB: widget.dateTypeOptionPB, + cellData: widget.cellContext.getCellData(), + cellController: widget.cellContext, + )..add(const DateCellCalendarEvent.initial()), child: BlocBuilder( - buildWhen: (p, c) => p != c, builder: (context, state) { - bool includeTime = state.dateCellData - .fold(() => false, (dateData) => dateData.includeTime); List children = [ Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), child: _buildCalendar(context), ), - if (includeTime) ...[ - const VSpace(12.0), - _TimeTextField( - bloc: context.read(), - popoverMutex: popoverMutex, - ), - ], + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: state.includeTime + ? _TimeTextField(popoverMutex: popoverMutex) + : const SizedBox(), + ), const TypeOptionSeparator(spacing: 12.0), const _IncludeTimeButton(), const TypeOptionSeparator(spacing: 12.0), @@ -144,7 +138,6 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { @override void dispose() { - bloc.close(); popoverMutex.dispose(); super.dispose(); } @@ -208,16 +201,11 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { outsideTextStyle: textStyle.textColor(Theme.of(context).disabledColor), ), - selectedDayPredicate: (day) { - return state.dateCellData.fold( - () => false, - (dateData) => isSameDay(dateData.date, day), - ); - }, + selectedDayPredicate: (day) => isSameDay(state.dateTime, day), onDaySelected: (selectedDay, focusedDay) { - context - .read() - .add(DateCellCalendarEvent.selectDay(selectedDay)); + context.read().add( + DateCellCalendarEvent.selectDay(selectedDay.toLocal().date), + ); }, onFormatChanged: (format) { context @@ -241,10 +229,7 @@ class _IncludeTimeButton extends StatelessWidget { @override Widget build(BuildContext context) { return BlocSelector( - selector: (state) => state.dateCellData.fold( - () => false, - (dateData) => dateData.includeTime, - ), + selector: (state) => state.includeTime, builder: (context, includeTime) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -258,7 +243,7 @@ class _IncludeTimeButton extends StatelessWidget { "grid/clock", color: Theme.of(context).iconTheme.color, ), - const HSpace(4), + const HSpace(6), FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()), const Spacer(), Toggle( @@ -280,11 +265,9 @@ class _IncludeTimeButton extends StatelessWidget { } class _TimeTextField extends StatefulWidget { - final DateCellCalendarBloc bloc; final PopoverMutex popoverMutex; const _TimeTextField({ - required this.bloc, required this.popoverMutex, Key? key, }) : super(key: key); @@ -295,18 +278,10 @@ class _TimeTextField extends StatefulWidget { class _TimeTextFieldState extends State<_TimeTextField> { late final FocusNode _focusNode; - late final TextEditingController _controller; @override void initState() { _focusNode = FocusNode(); - _controller = TextEditingController(text: widget.bloc.state.time); - - _focusNode.addListener(() { - if (mounted) { - widget.bloc.add(DateCellCalendarEvent.setTime(_controller.text)); - } - }); _focusNode.addListener(() { if (_focusNode.hasFocus) { @@ -325,38 +300,31 @@ class _TimeTextFieldState extends State<_TimeTextField> { @override Widget build(BuildContext context) { - _controller.text = widget.bloc.state.time ?? ""; - _controller.selection = - TextSelection.collapsed(offset: _controller.text.length); - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: Padding( - padding: GridSize.typeOptionContentInsets, - child: RoundedInputField( - height: GridSize.popoverItemHeight, - focusNode: _focusNode, - autoFocus: true, - hintText: widget.bloc.state.timeHintText, - controller: _controller, - style: Theme.of(context).textTheme.bodyMedium!, - normalBorderColor: Theme.of(context).colorScheme.outline, - errorBorderColor: Theme.of(context).colorScheme.error, - focusBorderColor: Theme.of(context).colorScheme.primary, - cursorColor: Theme.of(context).colorScheme.primary, - errorText: widget.bloc.state.timeFormatError - .fold(() => "", (error) => error), - onEditingComplete: (value) => - widget.bloc.add(DateCellCalendarEvent.setTime(value)), - ), - ), + return BlocBuilder( + builder: (context, state) { + return Column( + children: [ + const VSpace(12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: FlowyTextField( + text: state.time ?? "", + focusNode: _focusNode, + submitOnLeave: true, + hintText: state.timeHintText, + errorText: state.timeFormatError, + onSubmitted: (timeString) { + context + .read() + .add(DateCellCalendarEvent.setTime(timeString)); + }, + ), + ), + ], + ); + }, ); } - - @override - void dispose() { - _focusNode.dispose(); - super.dispose(); - } } class _DateTypeOptionButton extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart index d260938769..89c124d217 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/src/trash_cell.dart @@ -58,10 +58,8 @@ class TrashCell extends StatelessWidget { String dateFormatter($fixnum.Int64 inputTimestamps) { final outputFormat = DateFormat('MM/dd/yyyy hh:mm a'); - final date = DateTime.fromMillisecondsSinceEpoch( - inputTimestamps.toInt() * 1000, - isUtc: true, - ); + final date = + DateTime.fromMillisecondsSinceEpoch(inputTimestamps.toInt() * 1000); final outputDate = outputFormat.format(date); return outputDate; } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index bec7e93ef0..53d8e47ec4 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -469,10 +469,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" dependencies = [ "chrono", - "chrono-tz-build", + "chrono-tz-build 0.0.2", "phf 0.10.1", ] +[[package]] +name = "chrono-tz" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552" +dependencies = [ + "chrono", + "chrono-tz-build 0.1.0", + "phf 0.11.1", +] + [[package]] name = "chrono-tz-build" version = "0.0.2" @@ -481,7 +492,18 @@ checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" dependencies = [ "parse-zoneinfo", "phf 0.10.1", - "phf_codegen", + "phf_codegen 0.10.0", +] + +[[package]] +name = "chrono-tz-build" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751" +dependencies = [ + "parse-zoneinfo", + "phf 0.11.1", + "phf_codegen 0.11.1", ] [[package]] @@ -1377,6 +1399,7 @@ dependencies = [ "async-stream", "bytes", "chrono", + "chrono-tz 0.8.2", "collab", "collab-database", "collab-persistence", @@ -3018,6 +3041,15 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c" +dependencies = [ + "phf_shared 0.11.1", +] + [[package]] name = "phf_codegen" version = "0.10.0" @@ -3028,6 +3060,16 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf_codegen" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770" +dependencies = [ + "phf_generator 0.11.1", + "phf_shared 0.11.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -3048,6 +3090,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf" +dependencies = [ + "phf_shared 0.11.1", + "rand 0.8.5", +] + [[package]] name = "phf_macros" version = "0.8.0" @@ -3081,6 +3133,15 @@ dependencies = [ "uncased", ] +[[package]] +name = "phf_shared" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.0.12" @@ -4090,7 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a665751302f22a03c56721e23094e4dc22b04a80f381e6737a07bf7a7c70c0" dependencies = [ "chrono", - "chrono-tz", + "chrono-tz 0.6.1", "globwalk", "humansize", "lazy_static", diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index 4cd324e652..eb6a1f4451 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -39,6 +39,7 @@ anyhow = "1.0" async-stream = "0.3.4" rayon = "1.6.1" nanoid = "0.4.0" +chrono-tz = "0.8.1" strum = "0.21" strum_macros = "0.21" diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs index 911769c102..da4da5ac46 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs @@ -20,6 +20,9 @@ pub struct DateCellDataPB { #[pb(index = 4)] pub include_time: bool, + + #[pb(index = 5)] + pub timezone_id: String, } #[derive(Clone, Debug, Default, ProtoBuf)] @@ -36,8 +39,8 @@ pub struct DateChangesetPB { #[pb(index = 4, one_of)] pub include_time: Option, - #[pb(index = 5)] - pub is_utc: bool, + #[pb(index = 5, one_of)] + pub timezone_id: Option, } // Date @@ -48,9 +51,6 @@ pub struct DateTypeOptionPB { #[pb(index = 2)] pub time_format: TimeFormatPB, - - #[pb(index = 3)] - pub include_time: bool, } impl From for DateTypeOptionPB { @@ -58,7 +58,6 @@ impl From for DateTypeOptionPB { Self { date_format: data.date_format.into(), time_format: data.time_format.into(), - include_time: data.include_time, } } } @@ -68,7 +67,6 @@ impl From for DateTypeOption { Self { date_format: data.date_format.into(), time_format: data.time_format.into(), - include_time: data.include_time, } } } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 96230faf2d..c81732d8bd 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -361,7 +361,7 @@ pub(crate) async fn update_cell_handler( ¶ms.field_id, params.cell_changeset.clone(), ) - .await; + .await?; Ok(()) } @@ -397,7 +397,7 @@ pub(crate) async fn insert_or_update_select_option_handler( RowId::from(params.row_id), params.items, ) - .await; + .await?; Ok(()) } @@ -415,7 +415,7 @@ pub(crate) async fn delete_select_option_handler( RowId::from(params.row_id), params.items, ) - .await; + .await?; Ok(()) } @@ -452,7 +452,7 @@ pub(crate) async fn update_select_option_cell_handler( ¶ms.cell_identifier.field_id, changeset, ) - .await; + .await?; Ok(()) } @@ -467,7 +467,7 @@ pub(crate) async fn update_date_cell_handler( date: data.date, time: data.time, include_time: data.include_time, - is_utc: data.is_utc, + timezone_id: data.timezone_id, }; let database_editor = manager.get_database(&cell_id.view_id).await?; database_editor @@ -477,7 +477,7 @@ pub(crate) async fn update_date_cell_handler( &cell_id.field_id, cell_changeset, ) - .await; + .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index b5b4561bc8..b020b4fc24 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use collab_database::fields::Field; use collab_database::rows::{get_field_type_from_cell, Cell, Cells}; -use flowy_error::{ErrorCode, FlowyResult}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use crate::entities::FieldType; use crate::services::cell::{CellCache, CellProtobufBlob}; @@ -63,16 +63,14 @@ pub fn apply_cell_data_changeset( cell: Option, field: &Field, cell_data_cache: Option, -) -> Cell { +) -> Result { let changeset = changeset.to_cell_changeset_str(); let field_type = FieldType::from(field.field_type); match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache) .get_type_option_cell_data_handler(&field_type) { - None => Cell::default(), - Some(handler) => handler - .handle_cell_changeset(changeset, cell, field) - .unwrap_or_default(), + None => Ok(Cell::default()), + Some(handler) => Ok(handler.handle_cell_changeset(changeset, cell, field)?), } } @@ -196,11 +194,11 @@ pub fn stringify_cell_data( } pub fn insert_text_cell(s: String, field: &Field) -> Cell { - apply_cell_data_changeset(s, None, field, None) + apply_cell_data_changeset(s, None, field, None).unwrap() } pub fn insert_number_cell(num: i64, field: &Field) -> Cell { - apply_cell_data_changeset(num.to_string(), None, field, None) + apply_cell_data_changeset(num.to_string(), None, field, None).unwrap() } pub fn insert_url_cell(url: String, field: &Field) -> Cell { @@ -214,7 +212,7 @@ pub fn insert_url_cell(url: String, field: &Field) -> Cell { _ => url, }; - apply_cell_data_changeset(url, None, field, None) + apply_cell_data_changeset(url, None, field, None).unwrap() } pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell { @@ -223,7 +221,7 @@ pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell { } else { UNCHECK.to_string() }; - apply_cell_data_changeset(s, None, field, None) + apply_cell_data_changeset(s, None, field, None).unwrap() } pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell { @@ -231,22 +229,22 @@ pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell { date: Some(timestamp.to_string()), time: None, include_time: Some(false), - is_utc: true, + timezone_id: None, }) .unwrap(); - apply_cell_data_changeset(cell_data, None, field, None) + apply_cell_data_changeset(cell_data, None, field, None).unwrap() } pub fn insert_select_option_cell(option_ids: Vec, field: &Field) -> Cell { let changeset = SelectOptionCellChangeset::from_insert_options(option_ids).to_cell_changeset_str(); - apply_cell_data_changeset(changeset, None, field, None) + apply_cell_data_changeset(changeset, None, field, None).unwrap() } pub fn delete_select_option_cell(option_ids: Vec, field: &Field) -> Cell { let changeset = SelectOptionCellChangeset::from_delete_options(option_ids).to_cell_changeset_str(); - apply_cell_data_changeset(changeset, None, field, None) + apply_cell_data_changeset(changeset, None, field, None).unwrap() } /// Deserialize the String into cell specific data type. diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 94c15897d1..64e28fd9bb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -466,20 +466,27 @@ impl DatabaseEditor { row_id: RowId, field_id: &str, cell_changeset: T, - ) -> Option<()> + ) -> FlowyResult<()> where T: ToCellChangeset, { let (field, cell) = { let database = self.database.lock(); + let field = match database.fields.get_field(field_id) { + Some(field) => Ok(field), + None => { + let msg = format!("Field with id:{} not found", &field_id); + Err(FlowyError::internal().context(msg)) + }, + }?; ( - database.fields.get_field(field_id)?, + field, database.get_cell(field_id, &row_id).map(|cell| cell.cell), ) }; let cell_changeset = cell_changeset.to_cell_changeset_str(); let new_cell = - apply_cell_data_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone())); + apply_cell_data_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone()))?; self.update_cell(view_id, row_id, field_id, new_cell).await } @@ -489,7 +496,7 @@ impl DatabaseEditor { row_id: RowId, field_id: &str, new_cell: Cell, - ) -> Option<()> { + ) -> FlowyResult<()> { let old_row = { self.database.lock().get_row(&row_id) }; self.database.lock().update_row(&row_id, |row_update| { row_update.update_cells(|cell_update| { @@ -516,7 +523,7 @@ impl DatabaseEditor { field_id: field_id.to_string(), }]) .await; - None + Ok(()) } pub async fn create_select_option( @@ -536,9 +543,15 @@ impl DatabaseEditor { field_id: &str, row_id: RowId, options: Vec, - ) -> Option<()> { - let field = self.database.lock().fields.get_field(field_id)?; - let mut type_option = select_type_option_from_field(&field).ok()?; + ) -> FlowyResult<()> { + let field = match self.database.lock().fields.get_field(field_id) { + Some(field) => Ok(field), + None => { + let msg = format!("Field with id:{} not found", &field_id); + Err(FlowyError::internal().context(msg)) + }, + }?; + let mut type_option = select_type_option_from_field(&field)?; let cell_changeset = SelectOptionCellChangeset { insert_option_ids: options.iter().map(|option| option.id.clone()).collect(), ..Default::default() @@ -557,8 +570,8 @@ impl DatabaseEditor { self .update_cell_with_changeset(view_id, row_id, field_id, cell_changeset) - .await; - None + .await?; + Ok(()) } pub async fn delete_select_options( @@ -567,9 +580,15 @@ impl DatabaseEditor { field_id: &str, row_id: RowId, options: Vec, - ) -> Option<()> { - let field = self.database.lock().fields.get_field(field_id)?; - let mut type_option = select_type_option_from_field(&field).ok()?; + ) -> FlowyResult<()> { + let field = match self.database.lock().fields.get_field(field_id) { + Some(field) => Ok(field), + None => { + let msg = format!("Field with id:{} not found", &field_id); + Err(FlowyError::internal().context(msg)) + }, + }?; + let mut type_option = select_type_option_from_field(&field)?; let cell_changeset = SelectOptionCellChangeset { delete_option_ids: options.iter().map(|option| option.id.clone()).collect(), ..Default::default() @@ -588,8 +607,8 @@ impl DatabaseEditor { self .update_cell_with_changeset(view_id, row_id, field_id, cell_changeset) - .await; - None + .await?; + Ok(()) } pub async fn get_select_options(&self, row_id: RowId, field_id: &str) -> SelectOptionCellDataPB { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs index 67886475b9..319c87285d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs @@ -20,19 +20,74 @@ mod tests { type_option.date_format = date_format; match date_format { DateFormat::Friendly => { - assert_date(&type_option, 1647251762, None, "Mar 14,2022", false, &field); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1647251762".to_owned()), + time: None, + include_time: None, + timezone_id: None, + }, + None, + "Mar 14, 2022", + ); }, DateFormat::US => { - assert_date(&type_option, 1647251762, None, "2022/03/14", false, &field); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1647251762".to_owned()), + time: None, + include_time: None, + timezone_id: None, + }, + None, + "2022/03/14", + ); }, DateFormat::ISO => { - assert_date(&type_option, 1647251762, None, "2022-03-14", false, &field); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1647251762".to_owned()), + time: None, + include_time: None, + timezone_id: None, + }, + None, + "2022-03-14", + ); }, DateFormat::Local => { - assert_date(&type_option, 1647251762, None, "03/14/2022", false, &field); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1647251762".to_owned()), + time: None, + include_time: None, + timezone_id: None, + }, + None, + "03/14/2022", + ); }, DateFormat::DayMonthYear => { - assert_date(&type_option, 1647251762, None, "14/03/2022", false, &field); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1647251762".to_owned()), + time: None, + include_time: None, + timezone_id: None, + }, + None, + "14/03/2022", + ); }, } } @@ -41,8 +96,7 @@ mod tests { #[test] fn date_type_option_different_time_format_test() { let mut type_option = DateTypeOption::default(); - let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(field_type).build(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); for time_format in TimeFormat::iter() { type_option.time_format = time_format; @@ -50,53 +104,77 @@ mod tests { TimeFormat::TwentyFourHour => { assert_date( &type_option, - 1653609600, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: None, + include_time: Some(true), + timezone_id: Some("Etc/UTC".to_owned()), + }, None, - "May 27,2022 00:00", - true, - &field_rev, + "May 27, 2022 00:00", ); assert_date( &type_option, - 1653609600, - Some("9:00".to_owned()), - "May 27,2022 09:00", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("9:00".to_owned()), + include_time: Some(true), + timezone_id: Some("Etc/UTC".to_owned()), + }, + None, + "May 27, 2022 09:00", ); assert_date( &type_option, - 1653609600, - Some("23:00".to_owned()), - "May 27,2022 23:00", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("23:00".to_owned()), + include_time: Some(true), + timezone_id: Some("Etc/UTC".to_owned()), + }, + None, + "May 27, 2022 23:00", ); }, TimeFormat::TwelveHour => { assert_date( &type_option, - 1653609600, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: None, + include_time: Some(true), + timezone_id: Some("Etc/UTC".to_owned()), + }, None, - "May 27,2022 12:00 AM", - true, - &field_rev, + "May 27, 2022 12:00 AM", ); assert_date( &type_option, - 1653609600, - Some("9:00 AM".to_owned()), - "May 27,2022 09:00 AM", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("9:00 AM".to_owned()), + include_time: Some(true), + timezone_id: None, + }, + None, + "May 27, 2022 09:00 AM", ); assert_date( &type_option, - 1653609600, - Some("11:23 pm".to_owned()), - "May 27,2022 11:23 PM", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("11:23 pm".to_owned()), + include_time: Some(true), + timezone_id: Some("Etc/UTC".to_owned()), + }, + None, + "May 27, 2022 11:23 PM", ); }, } @@ -107,38 +185,58 @@ mod tests { fn date_type_option_invalid_date_str_test() { let type_option = DateTypeOption::default(); let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(field_type).build(); - assert_date(&type_option, "abc", None, "", false, &field_rev); + let field = FieldBuilder::from_field_type(field_type).build(); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("abc".to_owned()), + time: None, + include_time: None, + timezone_id: None, + }, + None, + "", + ); } #[test] #[should_panic] fn date_type_option_invalid_include_time_str_test() { let type_option = DateTypeOption::new(); - let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); assert_date( &type_option, - 1653609600, - Some("1:".to_owned()), - "May 27,2022 01:00", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("1:".to_owned()), + include_time: Some(true), + timezone_id: None, + }, + None, + "May 27, 2022 01:00", ); } #[test] + #[should_panic] fn date_type_option_empty_include_time_str_test() { let type_option = DateTypeOption::new(); - let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); assert_date( &type_option, - 1653609600, - Some("".to_owned()), - "May 27,2022 00:00", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("".to_owned()), + include_time: Some(true), + timezone_id: None, + }, + None, + "May 27, 2022 01:00", ); } @@ -146,14 +244,18 @@ mod tests { fn date_type_midnight_include_time_str_test() { let type_option = DateTypeOption::new(); let field_type = FieldType::DateTime; - let field_rev = FieldBuilder::from_field_type(field_type).build(); + let field = FieldBuilder::from_field_type(field_type).build(); assert_date( &type_option, - 1653609600, - Some("00:00".to_owned()), - "May 27,2022 00:00", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("00:00".to_owned()), + include_time: Some(true), + timezone_id: None, + }, + None, + "May 27, 2022 00:00", ); } @@ -162,15 +264,18 @@ mod tests { #[should_panic] fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() { let type_option = DateTypeOption::new(); - let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); - + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); assert_date( &type_option, - 1653609600, - Some("1:00 am".to_owned()), - "May 27,2022 01:00 AM", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("1:00 am".to_owned()), + include_time: Some(true), + timezone_id: None, + }, + None, + "May 27, 2022 01:00 AM", ); } @@ -180,15 +285,19 @@ mod tests { fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() { let mut type_option = DateTypeOption::new(); type_option.time_format = TimeFormat::TwelveHour; - let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); assert_date( &type_option, - 1653609600, - Some("20:00".to_owned()), - "May 27,2022 08:00 PM", - true, - &field_rev, + &field, + DateCellChangeset { + date: Some("1653609600".to_owned()), + time: Some("20:00".to_owned()), + include_time: Some(true), + timezone_id: None, + }, + None, + "May 27, 2022 08:00 PM", ); } @@ -218,25 +327,170 @@ mod tests { assert_eq!(china_local_time, "03/14/2022 05:56 PM"); } - fn assert_date( + /// The time component shouldn't remain the same since the timestamp is + /// completely overwritten. To achieve the desired result, also pass in the + /// time string along with the new timestamp. + #[test] + #[should_panic] + fn update_date_keep_time() { + let type_option = DateTypeOption::new(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some("1700006400".to_owned()), + time: Some("08:00".to_owned()), + include_time: Some(true), + timezone_id: Some("Etc/UTC".to_owned()), + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1701302400".to_owned()), + time: None, + include_time: None, + timezone_id: None, + }, + Some(old_cell_data), + "Nov 30, 2023 08:00", + ); + } + + #[test] + fn update_time_keep_date() { + let type_option = DateTypeOption::new(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some("1700006400".to_owned()), + time: Some("08:00".to_owned()), + include_time: Some(true), + timezone_id: None, + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: None, + time: Some("14:00".to_owned()), + include_time: None, + timezone_id: None, + }, + Some(old_cell_data), + "Nov 15, 2023 14:00", + ); + } + + #[test] + fn timezone_no_daylight_saving_time() { + let type_option = DateTypeOption::new(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1672963200".to_owned()), + time: None, + include_time: Some(true), + timezone_id: Some("Asia/Tokyo".to_owned()), + }, + None, + "Jan 06, 2023 09:00", + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1685404800".to_owned()), + time: None, + include_time: Some(true), + timezone_id: Some("Asia/Tokyo".to_owned()), + }, + None, + "May 30, 2023 09:00", + ); + } + + #[test] + fn timezone_with_daylight_saving_time() { + let type_option = DateTypeOption::new(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1672963200".to_owned()), + time: None, + include_time: Some(true), + timezone_id: Some("Europe/Paris".to_owned()), + }, + None, + "Jan 06, 2023 01:00", + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some("1685404800".to_owned()), + time: None, + include_time: Some(true), + timezone_id: Some("Europe/Paris".to_owned()), + }, + None, + "May 30, 2023 02:00", + ); + } + + #[test] + fn change_timezone() { + let type_option = DateTypeOption::new(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some("1672963200".to_owned()), + time: None, + include_time: Some(true), + timezone_id: Some("Asia/China".to_owned()), + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: None, + time: None, + include_time: None, + timezone_id: Some("America/Los_Angeles".to_owned()), + }, + Some(old_cell_data), + "Jan 05, 2023 16:00", + ); + } + + fn assert_date( type_option: &DateTypeOption, - timestamp: T, - include_time_str: Option, - expected_str: &str, - include_time: bool, field: &Field, + changeset: DateCellChangeset, + old_cell_data: Option, + expected_str: &str, ) { - let changeset = DateCellChangeset { - date: Some(timestamp.to_string()), - time: include_time_str, - is_utc: false, - include_time: Some(include_time), - }; - let (cell, _) = type_option.apply_changeset(changeset, None).unwrap(); + let (cell, cell_data) = type_option + .apply_changeset(changeset, old_cell_data) + .unwrap(); assert_eq!( - decode_cell_data(&cell, type_option, include_time, field), - expected_str.to_owned(), + decode_cell_data(&cell, type_option, cell_data.include_time, field), + expected_str, ); } @@ -258,4 +512,9 @@ mod tests { decoded_data.date } } + + fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell { + let (cell, _) = type_option.apply_changeset(changeset, None).unwrap(); + cell + } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs index 0dd3dd471b..448e276468 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -5,20 +5,21 @@ use crate::services::field::{ TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, }; use chrono::format::strftime::StrftimeItems; -use chrono::NaiveDateTime; +use chrono::{DateTime, Local, NaiveDateTime, NaiveTime, Offset, TimeZone}; +use chrono_tz::Tz; 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 std::cmp::Ordering; +use std::str::FromStr; // Date #[derive(Clone, Debug, Default, Serialize, Deserialize)] pub struct DateTypeOption { pub date_format: DateFormat, pub time_format: TimeFormat, - pub include_time: bool, } impl TypeOption for DateTypeOption { @@ -30,7 +31,6 @@ impl TypeOption for DateTypeOption { impl From for DateTypeOption { fn from(data: TypeOptionData) -> Self { - let include_time = data.get_bool_value("include_time").unwrap_or(false); let date_format = data .get_i64_value("data_format") .map(DateFormat::from) @@ -42,7 +42,6 @@ impl From for DateTypeOption { Self { date_format, time_format, - include_time, } } } @@ -52,7 +51,6 @@ impl From for TypeOptionData { TypeOptionDataBuilder::new() .insert_i64_value("data_format", data.date_format.value()) .insert_i64_value("time_format", data.time_format.value()) - .insert_bool_value("include_time", data.include_time) .build() } } @@ -79,23 +77,26 @@ impl DateTypeOption { 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 timezone_id = cell_data.timezone_id; - let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0); - if naive.is_none() { - return DateCellDataPB::default(); - } - let naive = naive.unwrap(); - if timestamp == 0 { - return DateCellDataPB::default(); - } - let fmt = self.date_format.format_str(); - let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt))); + let (date, time) = match cell_data.timestamp { + Some(timestamp) => { + let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(); + let offset = match Tz::from_str(&timezone_id) { + Ok(timezone) => timezone.offset_from_utc_datetime(&naive).fix(), + Err(_) => Local::now().offset().clone(), + }; - let time = if include_time { - let fmt = self.time_format.format_str(); - format!("{}", naive.format_with_items(StrftimeItems::new(fmt))) - } else { - "".to_string() + let date_time = DateTime::::from_utc(naive, offset); + + let fmt = self.date_format.format_str(); + let date = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt))); + let fmt = self.time_format.format_str(); + let time = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt))); + + (date, time) + }, + None => ("".to_owned(), "".to_owned()), }; DateCellDataPB { @@ -103,32 +104,9 @@ impl DateTypeOption { time, include_time, timestamp, + timezone_id, } } - - fn timestamp_from_utc_with_time( - &self, - naive_date: &NaiveDateTime, - time_str: &Option, - ) -> FlowyResult { - if let Some(time_str) = time_str.as_ref() { - if !time_str.is_empty() { - let naive_time = chrono::NaiveTime::parse_from_str(time_str, self.time_format.format_str()); - - match naive_time { - Ok(naive_time) => { - return Ok(naive_date.date().and_time(naive_time).timestamp()); - }, - Err(_e) => { - let msg = format!("Parse {} failed", time_str); - return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg)); - }, - }; - } - } - - Ok(naive_date.timestamp()) - } } impl TypeOptionTransform for DateTypeOption {} @@ -167,39 +145,129 @@ impl CellDataChangeset for DateTypeOption { changeset: ::CellChangeset, cell: Option, ) -> FlowyResult<(Cell, ::CellData)> { - let (timestamp, include_time) = match cell { - None => (None, false), - Some(cell) => { - let cell_data = DateCellData::from(&cell); - (cell_data.timestamp, cell_data.include_time) + // old date cell data + let (timestamp, include_time, timezone_id) = match cell { + None => (None, false, "".to_owned()), + Some(type_cell_data) => { + let cell_data = DateCellData::from(&type_cell_data); + ( + cell_data.timestamp, + cell_data.include_time, + cell_data.timezone_id, + ) }, }; + // update include_time and timezone_id if present let include_time = match changeset.include_time { None => include_time, Some(include_time) => include_time, }; - let timestamp = match changeset.date_timestamp() { - None => timestamp, - Some(date_timestamp) => match (include_time, changeset.time) { - (true, Some(time)) => { - let time = Some(time.trim().to_uppercase()); - let naive = NaiveDateTime::from_timestamp_opt(date_timestamp, 0); - if let Some(naive) = naive { - Some(self.timestamp_from_utc_with_time(&naive, &time)?) - } else { - Some(date_timestamp) + let timezone_id = match changeset.timezone_id { + None => timezone_id, + Some(ref timezone_id) => timezone_id.to_owned(), + }; + + let previous_datetime = match timestamp { + Some(timestamp) => NaiveDateTime::from_timestamp_opt(timestamp, 0), + None => None, + }; + + let new_date_timestamp = changeset.date_timestamp(); + + // parse the time string, which would be 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), + }?; + + // Calculate the new timestamp, while considering the timezone. If a new + // timestamp is included in the changeset without an accompanying time + // string, the new timestamp will simply overwrite the old one. Meaning, + // in order to change the day without time in the frontend, the time string + // must also be passed. + let timestamp = match Tz::from_str(&timezone_id) { + Ok(timezone) => match parsed_time { + Some(time) => { + // a valid time is provided, so we replace the time component of old + // (or new timestamp if provided) with this. + let local_date = match new_date_timestamp { + Some(timestamp) => Some( + timezone + .from_utc_datetime(&NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap()) + .date_naive(), + ), + None => match previous_datetime { + Some(datetime) => Some(timezone.from_utc_datetime(&datetime).date_naive()), + None => None, + }, + }; + + match local_date { + Some(date) => { + let local_datetime_naive = NaiveDateTime::new(date, time); + let local_datetime = timezone.from_local_datetime(&local_datetime_naive).unwrap(); + + Some(local_datetime.timestamp()) + }, + None => None, } }, - _ => Some(date_timestamp), + None => match new_date_timestamp { + // no valid time, return old timestamp or new one if provided + Some(timestamp) => Some(timestamp), + None => timestamp, + }, + }, + Err(_) => match parsed_time { + // same logic as above, but using local time instead of timezone + Some(time) => { + let offset = Local::now().offset().clone(); + + let local_date = match new_date_timestamp { + Some(timestamp) => Some( + offset + .from_utc_datetime(&NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap()) + .date_naive(), + ), + None => match previous_datetime { + Some(datetime) => Some(offset.from_utc_datetime(&datetime).date_naive()), + None => None, + }, + }; + + match local_date { + Some(date) => { + let local_datetime = NaiveDateTime::new(date, time); + let datetime = offset.from_local_datetime(&local_datetime).unwrap(); + + Some(datetime.timestamp()) + }, + None => None, + } + }, + None => match new_date_timestamp { + Some(timestamp) => Some(timestamp), + None => timestamp, + }, }, }; let date_cell_data = DateCellData { timestamp, include_time, + timezone_id, }; - Ok((date_cell_data.clone().into(), date_cell_data)) + Ok((Cell::from(date_cell_data.clone()), date_cell_data)) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs index 5b7a9da9c5..c7b02bf329 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -22,7 +22,7 @@ pub struct DateCellChangeset { pub date: Option, pub time: Option, pub include_time: Option, - pub is_utc: bool, + pub timezone_id: Option, } impl DateCellChangeset { @@ -57,27 +57,37 @@ impl ToCellChangeset for DateCellChangeset { pub struct DateCellData { pub timestamp: Option, pub include_time: bool, + pub timezone_id: String, } impl From<&Cell> for DateCellData { fn from(cell: &Cell) -> Self { let timestamp = cell .get_str_value(CELL_DATE) - .map(|data| data.parse::().unwrap_or_default()); + .map(|data| data.parse::().ok()) + .flatten(); let include_time = cell.get_bool_value("include_time").unwrap_or_default(); + let timezone_id = cell.get_str_value("timezone_id").unwrap_or_default(); + Self { timestamp, include_time, + timezone_id, } } } impl From for Cell { fn from(data: DateCellData) -> Self { + let timestamp_string = match data.timestamp { + Some(timestamp) => timestamp.to_string(), + None => "".to_owned(), + }; new_cell_builder(FieldType::DateTime) - .insert_str_value(CELL_DATE, data.timestamp.unwrap_or_default().to_string()) + .insert_str_value(CELL_DATE, timestamp_string) .insert_bool_value("include_time", data.include_time) + .insert_str_value("timezone_id", data.timezone_id) .build() } } @@ -105,6 +115,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData { Ok(DateCellData { timestamp: Some(value), include_time: false, + timezone_id: "".to_owned(), }) } @@ -121,6 +132,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData { { let mut timestamp: Option = None; let mut include_time: Option = None; + let mut timezone_id: Option = None; while let Some(key) = map.next_key()? { match key { @@ -130,15 +142,20 @@ impl<'de> serde::Deserialize<'de> for DateCellData { "include_time" => { include_time = map.next_value()?; }, + "timezone_id" => { + timezone_id = map.next_value()?; + }, _ => {}, } } - let include_time = include_time.unwrap_or(false); + let include_time = include_time.unwrap_or_default(); + let timezone_id = timezone_id.unwrap_or_default(); Ok(DateCellData { timestamp, include_time, + timezone_id, }) } } @@ -203,7 +220,7 @@ impl DateFormat { DateFormat::Local => "%m/%d/%Y", DateFormat::US => "%Y/%m/%d", DateFormat::ISO => "%Y-%m-%d", - DateFormat::Friendly => "%b %d,%Y", + DateFormat::Friendly => "%b %d, %Y", DateFormat::DayMonthYear => "%d/%m/%Y", } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs index 00337c29f0..a7335c6729 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs @@ -21,17 +21,18 @@ mod tests { &field_type, &field ), - "Mar 14,2022" + "Mar 14, 2022" ); let data = DateCellData { timestamp: Some(1647251762), include_time: true, + timezone_id: "".to_owned(), }; assert_eq!( stringify_cell_data(&data.into(), &FieldType::RichText, &field_type, &field), - "Mar 14,2022" + "Mar 14, 2022" ); } diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index ce7e025364..02f4ad853d 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -257,8 +257,8 @@ impl TestRowBuilder { let value = serde_json::to_string(&DateCellChangeset { date: Some(data.to_string()), time: None, - is_utc: true, include_time: Some(false), + timezone_id: None, }) .unwrap(); let date_field = self.field_with_type(&FieldType::DateTime); diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs index e3d9880f0c..60152fc7fd 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs @@ -48,8 +48,8 @@ pub fn make_date_cell_string(s: &str) -> String { serde_json::to_string(&DateCellChangeset { date: Some(s.to_string()), time: None, - is_utc: true, include_time: Some(false), + timezone_id: None, }) .unwrap() } diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index 4c31b9345a..dee31caeff 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -43,7 +43,6 @@ pub fn make_test_board() -> DatabaseData { let date_type_option = DateTypeOption { date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, - include_time: false, }; let date_field = FieldBuilder::new(field_type.clone(), date_type_option) .name("Time") diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index 9254944216..83270d483c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -44,7 +44,6 @@ pub fn make_test_grid() -> DatabaseData { let date_type_option = DateTypeOption { date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, - include_time: false, }; let date_field = FieldBuilder::new(field_type.clone(), date_type_option) .name("Time")