From 124d435f093acb07e9a05c3903e83c7d5490e23c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:58:15 +0800 Subject: [PATCH] feat: add end date to time cell data (#3369) * feat: add end date to time cell data * feat: implement ui for end time * test: add date to text test * chore: clippy warnings * fix: unexpected time parsing * fix: fix the date logic when toggling end date --------- Co-authored-by: nathan --- .../integration_test/database_cell_test.dart | 2 +- .../util/database_test_op.dart | 13 +- .../application/cell/date_cell_service.dart | 13 +- .../card/bloc/date_card_cell_bloc.dart | 15 +- .../row/cells/date_cell/date_cal_bloc.dart | 167 ++++++++++++----- .../row/cells/date_cell/date_cell.dart | 2 +- .../row/cells/date_cell/date_cell_bloc.dart | 13 +- .../row/cells/date_cell/date_editor.dart | 152 +++++++++++++--- .../lib/style_widget/text_field.dart | 7 +- frontend/resources/translations/en.json | 1 + .../type_option_entities/date_entities.rs | 23 ++- .../flowy-database2/src/event_handler.rs | 3 + .../src/services/cell/cell_operation.rs | 3 +- .../date_type_option/date_tests.rs | 172 +++++++++++++++--- .../date_type_option/date_type_option.rs | 83 +++++++-- .../date_type_option_entities.rs | 39 +++- .../text_type_option/text_tests.rs | 26 +++ .../group/controller_impls/date_controller.rs | 6 + .../tests/database/database_editor.rs | 2 +- .../tests/database/field_test/util.rs | 3 + .../tests/database/local_test/test.rs | 8 +- 21 files changed, 629 insertions(+), 124 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/database_cell_test.dart index 63b30f5c18..cf40bb1575 100644 --- a/frontend/appflowy_flutter/integration_test/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_cell_test.dart @@ -185,7 +185,7 @@ void main() { await tester.pumpAndSettle(); }); - testWidgets('edit time cell', (tester) async { + testWidgets('edit date time cell', (tester) async { await tester.initializeAppFlowy(); await tester.tapGoButton(); diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 685af4184b..4bed821b42 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -25,6 +25,7 @@ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/ import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_list.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/date.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/create_sort_list.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/sort/order_panel.dart'; @@ -330,7 +331,17 @@ extension AppFlowyDatabaseTest on WidgetTester { } Future toggleIncludeTime() async { - final findDateEditor = find.byType(DateCellEditor); + final findDateEditor = find.byType(IncludeTimeButton); + final findToggle = find.byType(Toggle); + final finder = find.descendant( + of: findDateEditor, + matching: findToggle, + ); + await tapButton(finder); + } + + Future toggleDateRange() async { + final findDateEditor = find.byType(EndTimeButton); final findToggle = find.byType(Toggle); final finder = find.descendant( of: findDateEditor, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/date_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/date_cell_service.dart index 736fd09ed9..1528906590 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/date_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/date_cell_service.dart @@ -20,11 +20,15 @@ final class DateCellBackendService { Future> update({ DateTime? date, String? time, + DateTime? endDate, + String? endTime, required includeTime, + required isRange, }) { final payload = DateChangesetPB.create() ..cellId = cellId - ..includeTime = includeTime; + ..includeTime = includeTime + ..isRange = isRange; if (date != null) { final dateTimestamp = date.millisecondsSinceEpoch ~/ 1000; @@ -33,6 +37,13 @@ final class DateCellBackendService { if (time != null) { payload.time = time; } + if (endDate != null) { + final dateTimestamp = endDate.millisecondsSinceEpoch ~/ 1000; + payload.endDate = Int64(dateTimestamp); + } + if (endTime != null) { + payload.endTime = endTime; + } return DatabaseEventUpdateDateCell(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart index 226710be06..d9f451ce8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/date_card_cell_bloc.dart @@ -80,10 +80,19 @@ class DateCardCellState with _$DateCardCellState { String _dateStrFromCellData(DateCellDataPB? cellData) { String dateStr = ""; if (cellData != null) { - if (cellData.includeTime) { - dateStr = '${cellData.date} ${cellData.time}'; + if (cellData.isRange) { + if (cellData.includeTime) { + dateStr = + "${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}"; + } else { + dateStr = "${cellData.date} → ${cellData.endDate}"; + } } else { - dateStr = cellData.date; + 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_cal_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cal_bloc.dart index b26e8e019d..174252d006 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 @@ -10,10 +10,10 @@ import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:easy_localization/easy_localization.dart' show StringTranslateExtension; +import 'package:flowy_infra/time/duration.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart'; -import 'package:table_calendar/table_calendar.dart'; part 'date_cal_bloc.freezed.dart'; @@ -38,40 +38,64 @@ class DateCellCalendarBloc await event.when( initial: () async => _startListening(), didReceiveCellUpdate: (DateCellDataPB? cellData) { - final (dateTime, time, includeTime) = + final (dateTime, endDateTime, time, endTime, includeTime, isRange) = _dateDataFromCellData(cellData); emit( state.copyWith( dateTime: dateTime, time: time, + endDateTime: endDateTime, + endTime: endTime, includeTime: includeTime, + isRange: isRange, + startDay: isRange ? dateTime : null, + endDay: isRange ? endDateTime : null, ), ); }, - didReceiveTimeFormatError: (String? timeFormatError) { - emit(state.copyWith(timeFormatError: timeFormatError)); + didReceiveTimeFormatError: + (String? parseTimeError, String? parseEndTimeError) { + emit( + state.copyWith( + parseTimeError: parseTimeError, + parseEndTimeError: parseEndTimeError, + ), + ); }, selectDay: (date) async { + if (state.isRange) { + return; + } await _updateDateData(date: date); }, setIncludeTime: (includeTime) async { await _updateDateData(includeTime: includeTime); }, + setIsRange: (isRange) async { + await _updateDateData(isRange: isRange); + }, setTime: (time) async { await _updateDateData(time: time); }, + selectDateRange: (DateTime? start, DateTime? end) async { + if (end == null) { + emit(state.copyWith(startDay: start, endDay: null)); + } else { + await _updateDateData( + date: start!.toLocal().date, + endDate: end.toLocal().date, + ); + } + }, + setEndTime: (String endTime) async { + await _updateDateData(endTime: endTime); + }, setDateFormat: (dateFormat) async { await _updateTypeOption(emit, dateFormat: dateFormat); }, setTimeFormat: (timeFormat) async { await _updateTypeOption(emit, timeFormat: timeFormat); }, - setCalFormat: (format) { - emit(state.copyWith(format: format)); - }, - setFocusedDay: (focusedDay) { - emit(state.copyWith(focusedDay: focusedDay)); - }, clearDate: () async { await _clearDate(); }, @@ -83,39 +107,66 @@ class DateCellCalendarBloc Future _updateDateData({ DateTime? date, String? time, + DateTime? endDate, + String? endTime, bool? includeTime, + bool? isRange, }) async { // make sure that not both date and time are updated at the same time assert( - date == null && time == null || - date == null && time != null || - date != null && time == null, + !(date != null && time != null) || !(endDate != null && endTime != null), ); + + // if not updating the time, use the old time in the state final String? newTime = time ?? state.time; - DateTime? newDate = _utcToLocalAndAddCurrentTime(date); + DateTime? newDate; if (time != null && time.isNotEmpty) { newDate = state.dateTime ?? DateTime.now(); + } else { + newDate = _utcToLocalAndAddCurrentTime(date); + } + + // if not updating the time, use the old time in the state + final String? newEndTime = endTime ?? state.endTime; + DateTime? newEndDate; + if (endTime != null && endTime.isNotEmpty) { + newEndDate = state.endDateTime ?? DateTime.now(); + } else { + newEndDate = _utcToLocalAndAddCurrentTime(endDate); } final result = await _dateCellBackendService.update( date: newDate, time: newTime, + endDate: newEndDate, + endTime: newEndTime, includeTime: includeTime ?? state.includeTime, + isRange: isRange ?? state.isRange, ); result.fold( (_) { - if (!isClosed && state.timeFormatError != null) { - add(const DateCellCalendarEvent.didReceiveTimeFormatError(null)); + if (!isClosed && + (state.parseEndTimeError != null || state.parseTimeError != null)) { + add( + const DateCellCalendarEvent.didReceiveTimeFormatError(null, null), + ); } }, (err) { switch (err.code) { case ErrorCode.InvalidDateTimeFormat: - if (isClosed) return; + if (isClosed) { + return; + } + // to determine which textfield should show error + final (startError, endError) = newDate != null + ? (timeFormatPrompt(err), null) + : (null, timeFormatPrompt(err)); add( DateCellCalendarEvent.didReceiveTimeFormatError( - timeFormatPrompt(err), + startError, + endError, ), ); break; @@ -130,9 +181,13 @@ class DateCellCalendarBloc final result = await _dateCellBackendService.clear(); result.fold( (_) { - if (!isClosed) { - add(const DateCellCalendarEvent.didReceiveTimeFormatError(null)); + if (isClosed) { + return; } + + add( + const DateCellCalendarEvent.didReceiveTimeFormatError(null, null), + ); }, (err) => Log.error(err), ); @@ -157,18 +212,13 @@ class DateCellCalendarBloc } String timeFormatPrompt(FlowyError error) { - String msg = "${LocaleKeys.grid_field_invalidTimeFormat.tr()}."; - switch (state.dateTypeOptionPB.timeFormat) { - case TimeFormatPB.TwelveHour: - msg = "$msg e.g. 01:00 PM"; - break; - case TimeFormatPB.TwentyFourHour: - msg = "$msg e.g. 13:00"; - break; - default: - break; - } - return msg; + return switch (state.dateTypeOptionPB.timeFormat) { + TimeFormatPB.TwelveHour => + "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 01:00 PM", + TimeFormatPB.TwentyFourHour => + "${LocaleKeys.grid_field_invalidTimeFormat.tr()}. e.g. 13:00", + _ => "", + }; } @override @@ -235,19 +285,21 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent { DateCellDataPB? data, ) = _DidReceiveCellUpdate; const factory DateCellCalendarEvent.didReceiveTimeFormatError( - String? timeformatError, + String? parseTimeError, + String? parseEndTimeError, ) = _DidReceiveTimeFormatError; - // table calendar's UI settings - const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay; - const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) = - _CalendarFormat; - // date cell data is modified const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay; + const factory DateCellCalendarEvent.selectDateRange( + DateTime? start, + DateTime? end, + ) = _SelectDateRange; const factory DateCellCalendarEvent.setTime(String time) = _Time; + const factory DateCellCalendarEvent.setEndTime(String endTime) = _EndTime; const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) = _IncludeTime; + const factory DateCellCalendarEvent.setIsRange(bool isRange) = _IsRange; // date field type options are modified const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) = @@ -262,12 +314,16 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent { class DateCellCalendarState with _$DateCellCalendarState { const factory DateCellCalendarState({ required DateTypeOptionPB dateTypeOptionPB, - required CalendarFormat format, - required DateTime focusedDay, + required DateTime? startDay, + required DateTime? endDay, required DateTime? dateTime, + required DateTime? endDateTime, required String? time, + required String? endTime, required bool includeTime, - required String? timeFormatError, + required bool isRange, + required String? parseTimeError, + required String? parseEndTimeError, required String timeHintText, }) = _DateCellCalendarState; @@ -275,15 +331,20 @@ class DateCellCalendarState with _$DateCellCalendarState { DateTypeOptionPB dateTypeOptionPB, DateCellDataPB? cellData, ) { - final (dateTime, time, includeTime) = _dateDataFromCellData(cellData); + final (dateTime, endDateTime, time, endTime, includeTime, isRange) = + _dateDataFromCellData(cellData); return DateCellCalendarState( dateTypeOptionPB: dateTypeOptionPB, - format: CalendarFormat.month, - focusedDay: DateTime.now(), + startDay: isRange ? dateTime : null, + endDay: isRange ? endDateTime : null, dateTime: dateTime, + endDateTime: endDateTime, time: time, + endTime: endTime, includeTime: includeTime, - timeFormatError: null, + isRange: isRange, + parseTimeError: null, + parseEndTimeError: null, timeHintText: _timeHintText(dateTypeOptionPB), ); } @@ -300,21 +361,31 @@ String _timeHintText(DateTypeOptionPB typeOption) { } } -(DateTime?, String?, bool) _dateDataFromCellData(DateCellDataPB? cellData) { +(DateTime?, DateTime?, String?, String?, bool, bool) _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 (null, null, false); + return (null, null, null, null, false, false); } DateTime? dateTime; String? time; + DateTime? endDateTime; + String? endTime; if (cellData.hasTimestamp()) { final timestamp = cellData.timestamp * 1000; dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt()); time = cellData.time; + if (cellData.hasEndTimestamp()) { + final endTimestamp = cellData.endTimestamp * 1000; + endDateTime = DateTime.fromMillisecondsSinceEpoch(endTimestamp.toInt()); + endTime = cellData.endTime; + } } final bool includeTime = cellData.includeTime; + final bool isRange = cellData.isRange; - return (dateTime, time, includeTime); + return (dateTime, endDateTime, time, endTime, includeTime, isRange); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart index 041d5073e7..f5d88256d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart @@ -74,7 +74,7 @@ class _DateCellState extends GridCellState { controller: _popover, triggerActions: PopoverTriggerFlags.none, direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(260, 520)), + constraints: BoxConstraints.loose(const Size(260, 620)), margin: EdgeInsets.zero, child: GridDateCellText( dateStr: state.dateStr, 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 e967ebbb81..0627f029be 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,8 +79,19 @@ class DateCellState with _$DateCellState { } String _dateStrFromCellData(DateCellDataPB? cellData) { + if (cellData == null || !cellData.hasTimestamp()) { + return ""; + } + String dateStr = ""; - if (cellData != null) { + if (cellData.isRange) { + if (cellData.includeTime) { + dateStr = + "${cellData.date} ${cellData.time} → ${cellData.endDate} ${cellData.endTime}"; + } else { + dateStr = "${cellData.date} → ${cellData.endDate}"; + } + } else { if (cellData.includeTime) { dateStr = "${cellData.date} ${cellData.time}"; } else { 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 f7088e1731..d301dcf751 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 @@ -3,6 +3,8 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; @@ -111,18 +113,31 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { duration: const Duration(milliseconds: 300), child: state.includeTime ? _TimeTextField( + isEndTime: false, timeStr: state.time, popoverMutex: popoverMutex, ) : const SizedBox.shrink(), ), + if (state.includeTime && state.isRange) const VSpace(8.0), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: state.includeTime && state.isRange + ? _TimeTextField( + isEndTime: true, + timeStr: state.endTime, + popoverMutex: popoverMutex, + ) + : const SizedBox.shrink(), + ), const DatePicker(), - const VSpace(8.0), - const TypeOptionSeparator(spacing: 8.0), + const TypeOptionSeparator(spacing: 12.0), + const EndTimeButton(), + const VSpace(4.0), const _IncludeTimeButton(), const TypeOptionSeparator(spacing: 8.0), DateTypeOptionButton(popoverMutex: popoverMutex), - VSpace(GridSize.typeOptionSeparatorHeight), + const VSpace(4.0), const ClearDateButton(), ]; @@ -145,9 +160,17 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> { } } -class DatePicker extends StatelessWidget { +class DatePicker extends StatefulWidget { const DatePicker({super.key}); + @override + State createState() => _DatePickerState(); +} + +class _DatePickerState extends State { + DateTime _focusedDay = DateTime.now(); + CalendarFormat _calendarFormat = CalendarFormat.month; + @override Widget build(BuildContext context) { return BlocBuilder( @@ -162,10 +185,15 @@ class DatePicker extends StatelessWidget { child: TableCalendar( firstDay: kFirstDay, lastDay: kLastDay, - focusedDay: state.focusedDay, + focusedDay: _focusedDay, rowHeight: 26.0 + 7.0, - calendarFormat: state.format, + calendarFormat: _calendarFormat, daysOfWeekHeight: 17.0 + 8.0, + rangeSelectionMode: state.isRange + ? RangeSelectionMode.enforced + : RangeSelectionMode.disabled, + rangeStartDay: state.isRange ? state.startDay : null, + rangeEndDay: state.isRange ? state.endDay : null, headerStyle: HeaderStyle( formatButtonVisible: false, titleCentered: true, @@ -198,15 +226,29 @@ class DatePicker extends StatelessWidget { ), weekendDecoration: boxDecoration, outsideDecoration: boxDecoration, + rangeStartDecoration: boxDecoration.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + rangeEndDecoration: boxDecoration.copyWith( + color: Theme.of(context).colorScheme.primary, + ), defaultTextStyle: textStyle, weekendTextStyle: textStyle, selectedTextStyle: textStyle.copyWith( color: Theme.of(context).colorScheme.surface, ), + rangeStartTextStyle: textStyle.copyWith( + color: Theme.of(context).colorScheme.surface, + ), + rangeEndTextStyle: textStyle.copyWith( + color: Theme.of(context).colorScheme.surface, + ), todayTextStyle: textStyle, outsideTextStyle: textStyle.copyWith( color: Theme.of(context).disabledColor, ), + rangeHighlightColor: + Theme.of(context).colorScheme.secondaryContainer, ), calendarBuilders: CalendarBuilders( dowBuilder: (context, day) { @@ -223,22 +265,24 @@ class DatePicker extends StatelessWidget { ); }, ), - selectedDayPredicate: (day) => isSameDay(state.dateTime, day), + selectedDayPredicate: (day) => + state.isRange ? false : isSameDay(state.dateTime, day), onDaySelected: (selectedDay, focusedDay) { context.read().add( DateCellCalendarEvent.selectDay(selectedDay.toLocal().date), ); }, - onFormatChanged: (format) { - context - .read() - .add(DateCellCalendarEvent.setCalFormat(format)); - }, - onPageChanged: (focusedDay) { - context - .read() - .add(DateCellCalendarEvent.setFocusedDay(focusedDay)); + onRangeSelected: (start, end, focusedDay) { + context.read().add( + DateCellCalendarEvent.selectDateRange(start, end), + ); }, + onFormatChanged: (calendarFormat) => setState(() { + _calendarFormat = calendarFormat; + }), + onPageChanged: (focusedDay) => setState(() { + _focusedDay = focusedDay; + }), ), ); }, @@ -268,13 +312,57 @@ class _IncludeTimeButton extends StatelessWidget { } } +@visibleForTesting +class EndTimeButton extends StatelessWidget { + const EndTimeButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) => state.isRange, + builder: (context, isRange) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: GridSize.typeOptionContentInsets, + child: Row( + children: [ + FlowySvg( + FlowySvgs.date_s, + color: Theme.of(context).iconTheme.color, + ), + const HSpace(6), + FlowyText.medium(LocaleKeys.grid_field_isRange.tr()), + const Spacer(), + Toggle( + value: isRange, + onChanged: (value) => context + .read() + .add(DateCellCalendarEvent.setIsRange(!value)), + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], + ), + ), + ), + ); + }, + ); + } +} + class _TimeTextField extends StatefulWidget { + final bool isEndTime; final String? timeStr; final PopoverMutex popoverMutex; const _TimeTextField({ required this.timeStr, required this.popoverMutex, + required this.isEndTime, Key? key, }) : super(key: key); @@ -309,21 +397,41 @@ class _TimeTextFieldState extends State<_TimeTextField> { @override Widget build(BuildContext context) { return BlocConsumer( - listener: (context, state) => _textController.text = state.time ?? "", + listener: (context, state) { + if (widget.isEndTime) { + _textController.text = state.endTime ?? ""; + } else { + _textController.text = state.time ?? ""; + } + }, builder: (context, state) { + String text = ""; + if (!widget.isEndTime && state.time != null) { + text = state.time!; + } else if (state.endTime != null) { + text = state.endTime!; + } return Padding( padding: const EdgeInsets.symmetric(horizontal: 18.0), child: FlowyTextField( - text: state.time ?? "", + text: text, focusNode: _focusNode, controller: _textController, submitOnLeave: true, hintText: state.timeHintText, - errorText: state.timeFormatError, + errorText: widget.isEndTime + ? state.parseEndTimeError + : state.parseTimeError, onSubmitted: (timeStr) { - context - .read() - .add(DateCellCalendarEvent.setTime(timeStr)); + if (widget.isEndTime) { + context + .read() + .add(DateCellCalendarEvent.setEndTime(timeStr)); + } else { + context + .read() + .add(DateCellCalendarEvent.setTime(timeStr)); + } }, ), ); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 7bf05d64e0..b444a91d24 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -107,7 +107,8 @@ class FlowyTextFieldState extends State { maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, style: Theme.of(context).textTheme.bodySmall, decoration: InputDecoration( - constraints: const BoxConstraints(maxHeight: 32), + constraints: BoxConstraints( + maxHeight: widget.errorText?.isEmpty ?? true ? 32 : 58), contentPadding: const EdgeInsets.symmetric(horizontal: 12), enabledBorder: OutlineInputBorder( borderSide: BorderSide( @@ -119,6 +120,10 @@ class FlowyTextFieldState extends State { isDense: false, hintText: widget.hintText, errorText: widget.errorText, + errorStyle: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: Theme.of(context).colorScheme.error), hintStyle: Theme.of(context) .textTheme .bodySmall! diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a52e28f669..e4fa3d5d1b 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -424,6 +424,7 @@ "numberFormat": "Number format", "dateFormat": "Date format", "includeTime": "Include time", + "isRange": "End date", "dateFormatFriendly": "Month Day, Year", "dateFormatISO": "Year-Month-Day", "dateFormatLocal": "Month/Day/Year", 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 bed6cfc5f8..a3106fbec3 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 @@ -19,7 +19,19 @@ pub struct DateCellDataPB { pub timestamp: i64, #[pb(index = 4)] + pub end_date: String, + + #[pb(index = 5)] + pub end_time: String, + + #[pb(index = 6)] + pub end_timestamp: i64, + + #[pb(index = 7)] pub include_time: bool, + + #[pb(index = 8)] + pub is_range: bool, } #[derive(Clone, Debug, Default, ProtoBuf)] @@ -34,9 +46,18 @@ pub struct DateChangesetPB { pub time: Option, #[pb(index = 4, one_of)] - pub include_time: Option, + pub end_date: Option, #[pb(index = 5, one_of)] + pub end_time: Option, + + #[pb(index = 6, one_of)] + pub include_time: Option, + + #[pb(index = 7, one_of)] + pub is_range: Option, + + #[pb(index = 8, one_of)] pub clear_flag: Option, } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index b16b1ab851..7129193c7c 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -641,7 +641,10 @@ pub(crate) async fn update_date_cell_handler( let cell_changeset = DateCellChangeset { date: data.date, time: data.time, + end_date: data.end_date, + end_time: data.end_time, include_time: data.include_time, + is_range: data.is_range, clear_flag: data.clear_flag, }; let database_editor = manager.get_database_with_view_id(&cell_id.view_id).await?; 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 0f49104709..b53f2f458b 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 @@ -210,9 +210,8 @@ pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell { pub fn insert_date_cell(timestamp: i64, include_time: Option, field: &Field) -> Cell { let cell_data = serde_json::to_string(&DateCellChangeset { date: Some(timestamp), - time: None, include_time, - clear_flag: None, + ..Default::default() }) .unwrap(); apply_cell_changeset(cell_data, None, field, None).unwrap() 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 91606a9fbc..a1d9c52824 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 @@ -27,7 +27,7 @@ mod tests { date: Some(1647251762), time: None, include_time: None, - clear_flag: None, + ..Default::default() }, None, "Mar 14, 2022", @@ -41,7 +41,7 @@ mod tests { date: Some(1647251762), time: None, include_time: None, - clear_flag: None, + ..Default::default() }, None, "2022/03/14", @@ -55,7 +55,7 @@ mod tests { date: Some(1647251762), time: None, include_time: None, - clear_flag: None, + ..Default::default() }, None, "2022-03-14", @@ -69,7 +69,7 @@ mod tests { date: Some(1647251762), time: None, include_time: None, - clear_flag: None, + ..Default::default() }, None, "03/14/2022", @@ -83,7 +83,7 @@ mod tests { date: Some(1647251762), time: None, include_time: None, - clear_flag: None, + ..Default::default() }, None, "14/03/2022", @@ -109,7 +109,7 @@ mod tests { date: Some(1653609600), time: None, include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 00:00", @@ -121,7 +121,7 @@ mod tests { date: Some(1653609600), time: Some("9:00".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 09:00", @@ -133,7 +133,7 @@ mod tests { date: Some(1653609600), time: Some("23:00".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 23:00", @@ -147,7 +147,7 @@ mod tests { date: Some(1653609600), time: None, include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 12:00 AM", @@ -159,7 +159,7 @@ mod tests { date: Some(1653609600), time: Some("9:00 AM".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 09:00 AM", @@ -171,7 +171,7 @@ mod tests { date: Some(1653609600), time: Some("11:23 pm".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 11:23 PM", @@ -195,7 +195,7 @@ mod tests { date: Some(1653609600), time: Some("1:".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 01:00", @@ -216,7 +216,7 @@ mod tests { date: Some(1653609600), time: Some("".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 01:00", @@ -235,7 +235,7 @@ mod tests { date: Some(1653609600), time: Some("00:00".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 00:00", @@ -256,7 +256,7 @@ mod tests { date: Some(1653609600), time: Some("1:00 am".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 01:00 AM", @@ -280,7 +280,7 @@ mod tests { date: Some(1653609600), time: Some("20:00".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, None, "May 27, 2022 08:00 PM", @@ -329,7 +329,7 @@ mod tests { date: Some(1700006400), time: Some("08:00".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, ); assert_date( @@ -339,7 +339,7 @@ mod tests { date: Some(1701302400), time: None, include_time: None, - clear_flag: None, + ..Default::default() }, Some(old_cell_data), "Nov 30, 2023 08:00", @@ -357,7 +357,7 @@ mod tests { date: Some(1700006400), time: Some("08:00".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, ); assert_date( @@ -367,7 +367,7 @@ mod tests { date: None, time: Some("14:00".to_owned()), include_time: None, - clear_flag: None, + ..Default::default() }, Some(old_cell_data), "Nov 15, 2023 14:00", @@ -385,7 +385,7 @@ mod tests { date: Some(1700006400), time: Some("08:00".to_owned()), include_time: Some(true), - clear_flag: None, + ..Default::default() }, ); assert_date( @@ -396,12 +396,142 @@ mod tests { time: None, include_time: Some(true), clear_flag: Some(true), + ..Default::default() }, Some(old_cell_data), "", ); } + #[test] + fn end_date_time_test() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + end_date: Some(1653782400), + include_time: Some(false), + is_range: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 → May 29, 2022", + ); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("20:00".to_owned()), + end_date: Some(1653782400), + end_time: Some("08:00".to_owned()), + include_time: Some(true), + is_range: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 20:00 → May 29, 2022 08:00", + ); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: Some(1653609600), + time: Some("20:00".to_owned()), + end_date: Some(1653782400), + include_time: Some(true), + is_range: Some(true), + ..Default::default() + }, + None, + "May 27, 2022 20:00 → May 29, 2022 00:00", + ); + } + + #[test] + fn turn_on_date_range() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some(1653609600), + time: Some("08:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + is_range: Some(true), + ..Default::default() + }, + Some(old_cell_data), + "May 27, 2022 08:00 → May 27, 2022 08:00", + ); + } + + #[test] + fn add_an_end_time() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + let old_cell_data = initialize_date_cell( + &type_option, + DateCellChangeset { + date: Some(1653609600), + time: Some("08:00".to_owned()), + include_time: Some(true), + ..Default::default() + }, + ); + assert_date( + &type_option, + &field, + DateCellChangeset { + date: None, + time: None, + end_date: Some(1700006400), + end_time: Some("16:00".to_owned()), + include_time: Some(true), + is_range: Some(true), + ..Default::default() + }, + Some(old_cell_data), + "May 27, 2022 08:00 → Nov 15, 2023 16:00", + ); + } + + #[test] + #[should_panic] + fn end_date_with_no_start_date() { + let type_option = DateTypeOption::test(); + let field = FieldBuilder::from_field_type(FieldType::DateTime).build(); + + assert_date( + &type_option, + &field, + DateCellChangeset { + date: None, + end_date: Some(1653782400), + include_time: Some(false), + is_range: Some(true), + ..Default::default() + }, + None, + "→ May 29, 2022", + ); + } + fn assert_date( type_option: &DateTypeOption, field: &Field, 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 fe17e92143..773ddaf2e0 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 @@ -70,15 +70,24 @@ impl TypeOptionCellDataSerde for DateTypeOption { &self, cell_data: ::CellData, ) -> ::CellProtobufType { - let timestamp = cell_data.timestamp; let include_time = cell_data.include_time; + let is_range = cell_data.is_range; + + let timestamp = cell_data.timestamp; let (date, time) = self.formatted_date_time_from_timestamp(×tamp); + let end_timestamp = cell_data.end_timestamp; + let (end_date, end_time) = self.formatted_date_time_from_timestamp(&end_timestamp); + DateCellDataPB { date, time, timestamp: timestamp.unwrap_or_default(), + end_date, + end_time, + end_timestamp: end_timestamp.unwrap_or_default(), include_time, + is_range, } } @@ -135,6 +144,8 @@ impl DateTypeOption { } } + /// combine the changeset_timestamp and parsed_time if provided. if + /// changeset_timestamp is None, fallback to previous_timestamp fn timestamp_from_parsed_time_previous_and_new_timestamp( &self, parsed_time: Option, @@ -142,7 +153,7 @@ impl DateTypeOption { changeset_timestamp: Option, ) -> Option { if let Some(time) = parsed_time { - // a valid time is provided, so we replace the time component of old + // a valid time is provided, so we replace the time component of old timestamp // (or new timestamp if provided) with it. let utc_date = changeset_timestamp .or(previous_timestamp) @@ -206,13 +217,30 @@ impl CellDataDecoder for DateTypeOption { } fn stringify_cell_data(&self, cell_data: ::CellData) -> String { - let timestamp = cell_data.timestamp; let include_time = cell_data.include_time; - let (date_string, time_string) = self.formatted_date_time_from_timestamp(×tamp); - if include_time && timestamp.is_some() { - format!("{} {}", date_string, time_string) + let timestamp = cell_data.timestamp; + let is_range = cell_data.is_range; + + let (date, time) = self.formatted_date_time_from_timestamp(×tamp); + + if is_range { + let (end_date, end_time) = match cell_data.end_timestamp { + Some(timestamp) => self.formatted_date_time_from_timestamp(&Some(timestamp)), + None => (date.clone(), time.clone()), + }; + if include_time && timestamp.is_some() { + format!("{} {} → {} {}", date, time, end_date, end_time) + .trim() + .to_string() + } else if timestamp.is_some() { + format!("{} → {}", date, end_date).trim().to_string() + } else { + "".to_string() + } + } else if include_time { + format!("{} {}", date, time).trim().to_string() } else { - date_string + date } } @@ -229,25 +257,33 @@ impl CellDataChangeset for DateTypeOption { cell: Option, ) -> FlowyResult<(Cell, ::CellData)> { // old date cell data - let (previous_timestamp, include_time) = match cell { + let (previous_timestamp, previous_end_timestamp, include_time, is_range) = match cell { Some(cell) => { let cell_data = DateCellData::from(&cell); - (cell_data.timestamp, cell_data.include_time) + ( + cell_data.timestamp, + cell_data.end_timestamp, + cell_data.include_time, + cell_data.is_range, + ) }, - None => (None, false), + None => (None, None, false, false), }; if changeset.clear_flag == Some(true) { let cell_data = DateCellData { timestamp: None, + end_timestamp: None, include_time, + is_range, }; return Ok((Cell::from(&cell_data), cell_data)); } - // update include_time if necessary + // update include_time and is_range if necessary let include_time = changeset.include_time.unwrap_or(include_time); + let is_range = changeset.is_range.unwrap_or(is_range); // Calculate the timestamp in the time zone specified in type option. If // a new timestamp is included in the changeset without an accompanying @@ -255,17 +291,38 @@ impl CellDataChangeset for DateTypeOption { // order to change the day without changing the time, the old time string // should be passed in as well. - let parsed_time = self.naive_time_from_time_string(include_time, changeset.time)?; + // parse the time string, which is in the local timezone + let parsed_start_time = self.naive_time_from_time_string(include_time, changeset.time)?; let timestamp = self.timestamp_from_parsed_time_previous_and_new_timestamp( - parsed_time, + parsed_start_time, previous_timestamp, changeset.date, ); + let end_timestamp = + if is_range && changeset.end_date.is_none() && previous_end_timestamp.is_none() { + // just toggled is_range so no passed in or existing end time data + timestamp + } else if is_range { + // parse the changeset's end time data or fallback to previous version + let parsed_end_time = self.naive_time_from_time_string(include_time, changeset.end_time)?; + + self.timestamp_from_parsed_time_previous_and_new_timestamp( + parsed_end_time, + previous_end_timestamp, + changeset.end_date, + ) + } else { + // clear the end time data + None + }; + let cell_data = DateCellData { timestamp, + end_timestamp, include_time, + is_range, }; Ok((Cell::from(&cell_data), 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 ab55be3b36..6d36e22758 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 @@ -20,7 +20,10 @@ use crate::services::field::{TypeOptionCellData, CELL_DATA}; pub struct DateCellChangeset { pub date: Option, pub time: Option, + pub end_date: Option, + pub end_time: Option, pub include_time: Option, + pub is_range: Option, pub clear_flag: Option, } @@ -42,15 +45,20 @@ impl ToCellChangeset for DateCellChangeset { #[derive(Default, Clone, Debug, Serialize)] pub struct DateCellData { pub timestamp: Option, + pub end_timestamp: Option, #[serde(default)] pub include_time: bool, + #[serde(default)] + pub is_range: bool, } impl DateCellData { - pub fn new(timestamp: i64, include_time: bool) -> Self { + pub fn new(timestamp: i64, include_time: bool, is_range: bool) -> Self { Self { timestamp: Some(timestamp), + end_timestamp: None, include_time, + is_range, } } } @@ -66,10 +74,16 @@ impl From<&Cell> for DateCellData { let timestamp = cell .get_str_value(CELL_DATA) .and_then(|data| data.parse::().ok()); + let end_timestamp = cell + .get_str_value("end_timestamp") + .and_then(|data| data.parse::().ok()); let include_time = cell.get_bool_value("include_time").unwrap_or_default(); + let is_range = cell.get_bool_value("is_range").unwrap_or_default(); Self { timestamp, + end_timestamp, include_time, + is_range, } } } @@ -78,7 +92,9 @@ impl From<&DateCellDataPB> for DateCellData { fn from(data: &DateCellDataPB) -> Self { Self { timestamp: Some(data.timestamp), + end_timestamp: Some(data.end_timestamp), include_time: data.include_time, + is_range: data.is_range, } } } @@ -89,9 +105,17 @@ impl From<&DateCellData> for Cell { Some(timestamp) => timestamp.to_string(), None => "".to_owned(), }; + let end_timestamp_string = match cell_data.end_timestamp { + Some(timestamp) => timestamp.to_string(), + None => "".to_owned(), + }; + // Most of the case, don't use these keys in other places. Otherwise, we should define + // constants for them. new_cell_builder(FieldType::DateTime) .insert_str_value(CELL_DATA, timestamp_string) + .insert_str_value("end_timestamp", end_timestamp_string) .insert_bool_value("include_time", cell_data.include_time) + .insert_bool_value("is_range", cell_data.is_range) .build() } } @@ -118,7 +142,9 @@ impl<'de> serde::Deserialize<'de> for DateCellData { { Ok(DateCellData { timestamp: Some(value), + end_timestamp: None, include_time: false, + is_range: false, }) } @@ -134,25 +160,36 @@ impl<'de> serde::Deserialize<'de> for DateCellData { M: serde::de::MapAccess<'de>, { let mut timestamp: Option = None; + let mut end_timestamp: Option = None; let mut include_time: Option = None; + let mut is_range: Option = None; while let Some(key) = map.next_key()? { match key { "timestamp" => { timestamp = map.next_value()?; }, + "end_timestamp" => { + end_timestamp = map.next_value()?; + }, "include_time" => { include_time = map.next_value()?; }, + "is_range" => { + is_range = map.next_value()?; + }, _ => {}, } } let include_time = include_time.unwrap_or_default(); + let is_range = is_range.unwrap_or_default(); Ok(DateCellData { timestamp, + end_timestamp, include_time, + is_range, }) } } 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 001c67e612..60e956c98a 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 @@ -26,13 +26,39 @@ mod tests { let data = DateCellData { timestamp: Some(1647251762), + end_timestamp: None, include_time: true, + is_range: false, }; assert_eq!( stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field), "Mar 14, 2022 09:56" ); + + let data = DateCellData { + timestamp: Some(1647251762), + end_timestamp: Some(1648533809), + include_time: true, + is_range: false, + }; + + assert_eq!( + stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field), + "Mar 14, 2022 09:56" + ); + + let data = DateCellData { + timestamp: Some(1647251762), + end_timestamp: Some(1648533809), + include_time: true, + is_range: true, + }; + + assert_eq!( + stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field), + "Mar 14, 2022 09:56 → Mar 29, 2022 06:03" + ); } fn to_text_cell(s: String) -> Cell { diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index b905b1453b..3c64e4ff26 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -476,6 +476,7 @@ mod tests { let mar_14_2022_cd = DateCellData { timestamp: Some(mar_14_2022.timestamp()), include_time: false, + ..Default::default() }; let today = offset::Local::now(); let three_days_before = today.checked_add_signed(Duration::days(-3)).unwrap(); @@ -497,6 +498,7 @@ mod tests { cell_data: DateCellData { timestamp: Some(today.timestamp()), include_time: false, + ..Default::default() }, type_option: &local_date_type_option, setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(), @@ -507,6 +509,7 @@ mod tests { cell_data: DateCellData { timestamp: Some(three_days_before.timestamp()), include_time: false, + ..Default::default() }, type_option: &local_date_type_option, setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(), @@ -533,6 +536,7 @@ mod tests { .timestamp(), ), include_time: false, + ..Default::default() }, type_option: &local_date_type_option, setting_content: r#"{"condition": 2, "hide_empty": false}"#.to_string(), @@ -557,6 +561,7 @@ mod tests { cell_data: DateCellData { timestamp: Some(1685715999), include_time: false, + ..Default::default() }, type_option: &default_date_type_option, setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), @@ -567,6 +572,7 @@ mod tests { cell_data: DateCellData { timestamp: Some(1685802386), include_time: false, + ..Default::default() }, type_option: &default_date_type_option, setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), 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 8c7b1a0646..79a56e29fe 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -331,7 +331,7 @@ impl<'a> TestRowBuilder<'a> { date: Some(data), time, include_time, - clear_flag: None, + ..Default::default() }) .unwrap(); let date_field = self.field_with_type(field_type); 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 383c8bab5e..6820318b3d 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 @@ -102,7 +102,10 @@ pub fn make_date_cell_string(timestamp: i64) -> String { serde_json::to_string(&DateCellChangeset { date: Some(timestamp), time: None, + end_date: None, + end_time: None, include_time: Some(false), + is_range: Some(false), clear_flag: None, }) .unwrap() diff --git a/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs b/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs index 43787ec921..34348cac5b 100644 --- a/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs +++ b/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs @@ -528,9 +528,7 @@ async fn update_date_cell_event_test() { .update_date_cell(DateChangesetPB { cell_id: cell_path, date: Some(timestamp), - time: None, - include_time: None, - clear_flag: None, + ..Default::default() }) .await; assert!(error.is_none()); @@ -892,9 +890,7 @@ async fn create_calendar_event_test() { row_id: row.id, }, date: Some(timestamp()), - time: None, - include_time: None, - clear_flag: None, + ..Default::default() }) .await; assert!(error.is_none());