diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart index 62e95cf6a8..ca565474ec 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart @@ -211,8 +211,7 @@ void main() { await tester.toggleIncludeTime(); // Select a date - final now = DateTime.now(); - final expected = DateTime(now.year, now.month, now.day); + DateTime now = DateTime.now(); await tester.selectDay(content: now.day); await tester.dismissCellEditor(); @@ -220,13 +219,13 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y').format(expected), + content: DateFormat('MMM dd, y').format(now), ); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); // Toggle include time - // When toggling include time, the time value is from the previous existing date time, not the current time + now = DateTime.now(); await tester.toggleIncludeTime(); await tester.dismissCellEditor(); @@ -234,7 +233,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y HH:mm').format(expected), + content: DateFormat('MMM dd, y HH:mm').format(now), ); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -249,7 +248,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('dd/MM/y HH:mm').format(expected), + content: DateFormat('dd/MM/y HH:mm').format(now), ); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); @@ -264,7 +263,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('dd/MM/y hh:mm a').format(expected), + content: DateFormat('dd/MM/y hh:mm a').format(now), ); await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart index 32acd51369..9ca6a524a6 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart @@ -292,7 +292,6 @@ void main() { await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); await tester.toggleIncludeTime(); final now = DateTime.now(); - final expected = DateTime(now.year, now.month, now.day); await tester.selectDay(content: now.day); await tester.dismissCellEditor(); @@ -300,7 +299,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y HH:mm').format(expected), + content: DateFormat('MMM dd, y HH:mm').format(now), ); // open editor and change date & time format @@ -314,7 +313,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('dd/MM/y hh:mm a').format(expected), + content: DateFormat('dd/MM/y hh:mm a').format(now), ); }); @@ -541,7 +540,6 @@ void main() { await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime); await tester.toggleIncludeTime(); final now = DateTime.now(); - final expected = DateTime(now.year, now.month, now.day); await tester.selectDay(content: now.day); await tester.dismissCellEditor(); @@ -549,7 +547,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y HH:mm').format(expected), + content: DateFormat('MMM dd, y HH:mm').format(now), ); await tester.changeFieldTypeOfFieldWithName( @@ -559,7 +557,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.RichText, - content: DateFormat('MMM dd, y HH:mm').format(expected), + content: DateFormat('MMM dd, y HH:mm').format(now), cellIndex: 1, ); @@ -583,7 +581,7 @@ void main() { tester.assertCellContent( rowIndex: 0, fieldType: FieldType.DateTime, - content: DateFormat('MMM dd, y').format(expected), + content: DateFormat('MMM dd, y').format(now), ); tester.assertCellContent( rowIndex: 1, diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart index 7d4275da36..c7fea448f7 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_date_reminder_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -58,7 +58,7 @@ void main() { // add time 11:12 final textField = find .descendant( - of: find.byType(AppFlowyDatePicker), + of: find.byType(DesktopAppFlowyDatePicker), matching: find.byType(TextField), ) .last; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart index 6c6baa00b9..8262cf6408 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/date_picker/mobile_date_picker_screen.dart @@ -5,7 +5,7 @@ import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_header.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -93,11 +93,19 @@ class _MobileDateCellEditScreenState extends State { onRangeSelected: (start, end) { dateCellBloc.add(DateCellEditorEvent.updateDateRange(start, end)); }, - onIsRangeChanged: (value) { - dateCellBloc.add(DateCellEditorEvent.setIsRange(value)); + onIsRangeChanged: (value, dateTime, endDateTime) { + dateCellBloc.add( + DateCellEditorEvent.setIsRange(value, dateTime, endDateTime), + ); }, - onIncludeTimeChanged: (value) { - dateCellBloc.add(DateCellEditorEvent.setIncludeTime(value)); + onIncludeTimeChanged: (value, dateTime, endDateTime) { + dateCellBloc.add( + DateCellEditorEvent.setIncludeTime( + value, + dateTime, + endDateTime, + ), + ); }, onClearDate: () { dateCellBloc.add(const DateCellEditorEvent.clearDate()); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart index 2dd35aba10..85583dea71 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_editor_bloc.dart @@ -137,11 +137,11 @@ class DateCellEditorBloc } await _updateDateData(date: start, endDate: end); }, - setIncludeTime: (includeTime) async { - await _updateIncludeTime(includeTime); + setIncludeTime: (includeTime, dateTime, endDateTime) async { + await _updateIncludeTime(includeTime, dateTime, endDateTime); }, - setIsRange: (isRange) async { - await _updateIsRange(isRange); + setIsRange: (isRange, dateTime, endDateTime) async { + await _updateIsRange(isRange, dateTime, endDateTime); }, setDateFormat: (DateFormatPB dateFormat) async { await _updateTypeOption(emit, dateFormat: dateFormat); @@ -209,10 +209,11 @@ class DateCellEditorBloc result.onFailure(Log.error); } - Future _updateIsRange(bool isRange) async { - final dateTime = state.dateTime == null ? DateTime.now().withoutTime : null; - final endDateTime = dateTime; - + Future _updateIsRange( + bool isRange, + DateTime? dateTime, + DateTime? endDateTime, + ) async { final result = await _dateCellBackendService.update( date: dateTime, endDate: endDateTime, @@ -221,9 +222,11 @@ class DateCellEditorBloc result.onFailure(Log.error); } - Future _updateIncludeTime(bool includeTime) async { - final dateTime = state.dateTime ?? DateTime.now().withoutTime; - final endDateTime = state.isRange ? state.endDateTime ?? dateTime : null; + Future _updateIncludeTime( + bool includeTime, + DateTime? dateTime, + DateTime? endDateTime, + ) async { final result = await _dateCellBackendService.update( date: dateTime, endDate: endDateTime, @@ -320,10 +323,17 @@ class DateCellEditorEvent with _$DateCellEditorEvent { DateTime end, ) = _UpdateDateRange; - const factory DateCellEditorEvent.setIncludeTime(bool includeTime) = - _IncludeTime; + const factory DateCellEditorEvent.setIncludeTime( + bool includeTime, + DateTime? dateTime, + DateTime? endDateTime, + ) = _IncludeTime; - const factory DateCellEditorEvent.setIsRange(bool isRange) = _SetIsRange; + const factory DateCellEditorEvent.setIsRange( + bool isRange, + DateTime? dateTime, + DateTime? endDateTime, + ) = _SetIsRange; const factory DateCellEditorEvent.setReminderOption(ReminderOption option) = _SetReminderOption; diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart index 3cd6899b09..de5648291e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_day.dart @@ -242,6 +242,7 @@ class NewEventButton extends StatelessWidget { hoverColor: AFThemeExtension.of(context).lightGreyHover, width: 22, tooltipText: LocaleKeys.calendar_newEventButtonTooltip.tr(), + radius: Corners.s6Border, decoration: BoxDecoration( border: Border.fromBorderSide( BorderSide( @@ -251,7 +252,7 @@ class NewEventButton extends StatelessWidget { width: 0.5, ), ), - borderRadius: Corners.s5Border, + borderRadius: Corners.s6Border, boxShadow: [ BoxShadow( spreadRadius: -2, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart index b9e4890a3f..a2d61cc5f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/date.dart @@ -6,7 +6,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -198,7 +198,7 @@ class _DateFilterEditorState extends State { child: SingleFilterBlocSelector( filterId: widget.filterId, builder: (context, filter, field) { - return AppFlowyDatePicker( + return DesktopAppFlowyDatePicker( isRange: isRange, includeTime: false, dateFormat: DateFormatPB.Friendly, diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index 936a59335d..116e47349c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -219,7 +219,9 @@ class TabBarItemButton extends StatelessWidget { color: color, ), text: FlowyText( - view.name, + view.name.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : view.name, lineHeight: 1.0, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart index ce0dcc8d7b..8796df14ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart @@ -1,4 +1,6 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; @@ -105,7 +107,9 @@ class _DatabaseViewSelectorButton extends StatelessWidget { const HSpace(6), Flexible( child: FlowyText.medium( - tabBar.view.name, + tabBar.view.name.isEmpty + ? LocaleKeys.document_title_placeholder.tr() + : tabBar.view.name, fontSize: 14, overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart index 8b097834d0..a3758029d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart @@ -146,9 +146,12 @@ class _TextCellState extends State { if (widget.showNotes) { return FlowyTooltip( message: LocaleKeys.board_notesTooltip.tr(), - child: FlowySvg( - FlowySvgs.notes_s, - color: Theme.of(context).hintColor, + child: Padding( + padding: const EdgeInsets.all(1.0), + child: FlowySvg( + FlowySvgs.notes_s, + color: Theme.of(context).hintColor, + ), ), ); } @@ -180,13 +183,23 @@ class _TextCellState extends State { return BlocBuilder( builder: (context, state) { final icon = _buildIcon(state); + if (icon == null) { + return textField; + } + final resolved = + widget.style.padding.resolve(Directionality.of(context)); + final padding = EdgeInsetsDirectional.only( + start: resolved.left, + top: resolved.top, + bottom: resolved.bottom, + ); return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (icon != null) ...[ - icon, - const HSpace(4.0), - ], + Container( + padding: padding, + child: icon, + ), Expanded(child: textField), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart index 6d9f08fb5b..6264fea958 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart @@ -22,7 +22,6 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) { final TextStyle textStyle = Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: 11, overflow: TextOverflow.ellipsis, - fontWeight: FontWeight.w400, ); return { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart index 1110987b95..f28cb756c8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_text_cell.dart @@ -83,7 +83,8 @@ class _IconOrEmoji extends StatelessWidget { return hasDocument ? Padding( padding: - const EdgeInsetsDirectional.only(end: 6.0), + const EdgeInsetsDirectional.only(end: 6.0) + .add(const EdgeInsets.all(1)), child: FlowySvg( FlowySvgs.notes_s, color: Theme.of(context).hintColor, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart index d85a5dfbbf..3ee7f1ef56 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/date_cell_editor.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -44,7 +44,7 @@ class _DateCellEditor extends State { child: BlocBuilder( builder: (context, state) { final dateCellBloc = context.read(); - return AppFlowyDatePicker( + return DesktopAppFlowyDatePicker( dateTime: state.dateTime, endDateTime: state.endDateTime, dateFormat: state.dateTypeOptionPB.dateFormat, @@ -77,11 +77,19 @@ class _DateCellEditor extends State { ], ), ], - onIncludeTimeChanged: (value) { - dateCellBloc.add(DateCellEditorEvent.setIncludeTime(value)); + onIncludeTimeChanged: (value, dateTime, endDateTime) { + dateCellBloc.add( + DateCellEditorEvent.setIncludeTime( + value, + dateTime, + endDateTime, + ), + ); }, - onIsRangeChanged: (value) { - dateCellBloc.add(DateCellEditorEvent.setIsRange(value)); + onIsRangeChanged: (value, dateTime, endDateTime) { + dateCellBloc.add( + DateCellEditorEvent.setIsRange(value, dateTime, endDateTime), + ); }, onDaySelected: (selectedDay) { dateCellBloc.add(DateCellEditorEvent.updateDateTime(selectedDay)); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 851d56560f..7ef29ccba4 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -9,7 +9,7 @@ import 'package:appflowy/user/application/reminder/reminder_extension.dart'; import 'package:appflowy/util/theme_extension.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/mobile_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart'; @@ -65,6 +65,12 @@ class _MentionDateBlockState extends State { late bool _includeTime = widget.includeTime; late DateTime? parsedDate = DateTime.tryParse(widget.date); + @override + void didUpdateWidget(covariant oldWidget) { + parsedDate = DateTime.tryParse(widget.date); + super.didUpdateWidget(oldWidget); + } + @override void dispose() { mutex.dispose(); @@ -98,7 +104,7 @@ class _MentionDateBlockState extends State { dateFormat: appearance.dateFormat, timeFormat: appearance.timeFormat, selectedReminderOption: widget.reminderOption, - onIncludeTimeChanged: (includeTime) { + onIncludeTimeChanged: (includeTime, dateTime, _) { _includeTime = includeTime; if (widget.reminderOption != ReminderOption.none) { @@ -107,9 +113,10 @@ class _MentionDateBlockState extends State { reminder, includeTime, ); - } else { + } else if (dateTime != null) { + parsedDate = dateTime; _updateBlock( - parsedDate!, + dateTime, includeTime: includeTime, ); } @@ -357,6 +364,7 @@ class _DatePickerBottomSheet extends StatelessWidget { MobileAppFlowyDatePicker( dateTime: parsedDate, includeTime: includeTime, + isRange: options.isRange, dateFormat: options.dateFormat.simplified, timeFormat: options.timeFormat.simplified, reminderOption: reminderOption, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart new file mode 100644 index 0000000000..f3c70fd08c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart @@ -0,0 +1,335 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter/widgets.dart'; + +import 'widgets/reminder_selector.dart'; + +typedef DaySelectedCallback = void Function(DateTime); +typedef RangeSelectedCallback = void Function(DateTime, DateTime); +typedef IsRangeChangedCallback = void Function(bool, DateTime?, DateTime?); +typedef IncludeTimeChangedCallback = void Function(bool, DateTime?, DateTime?); + +abstract class AppFlowyDatePicker extends StatefulWidget { + const AppFlowyDatePicker({ + super.key, + required this.dateTime, + this.endDateTime, + required this.includeTime, + required this.isRange, + this.reminderOption = ReminderOption.none, + required this.dateFormat, + required this.timeFormat, + this.onDaySelected, + this.onRangeSelected, + this.onIncludeTimeChanged, + this.onIsRangeChanged, + this.onReminderSelected, + }); + + final DateTime? dateTime; + final DateTime? endDateTime; + + final DateFormatPB dateFormat; + final TimeFormatPB timeFormat; + + /// Called when the date is picked, whether by submitting a date from the top + /// or by selecting a date in the calendar. Will not be called if isRange is + /// true + final DaySelectedCallback? onDaySelected; + + /// Called when a date range is picked. Will not be called if isRange is false + final RangeSelectedCallback? onRangeSelected; + + /// Whether the date picker allows inputting a time in addition to the date + final bool includeTime; + + /// Called when the include time value is changed. This callback has the side + /// effect of changing the dateTime values as well + final IncludeTimeChangedCallback? onIncludeTimeChanged; + + // Whether the date picker supports date ranges + final bool isRange; + + /// Called when the is range value is changed. This callback has the side + /// effect of changing the dateTime values as well + final IsRangeChangedCallback? onIsRangeChanged; + + final ReminderOption reminderOption; + final OnReminderSelected? onReminderSelected; +} + +abstract class AppFlowyDatePickerState + extends State { + // store date values in the state and refresh the ui upon any changes made, instead of only updating them after receiving update from backend. + late DateTime? dateTime; + late DateTime? startDateTime; + late DateTime? endDateTime; + late bool includeTime; + late bool isRange; + late ReminderOption reminderOption; + + late DateTime focusedDateTime; + PageController? pageController; + + bool justChangedIsRange = false; + + @override + void initState() { + super.initState(); + + dateTime = widget.dateTime; + startDateTime = widget.isRange ? widget.dateTime : null; + endDateTime = widget.isRange ? widget.endDateTime : null; + includeTime = widget.includeTime; + isRange = widget.isRange; + reminderOption = widget.reminderOption; + + focusedDateTime = widget.dateTime ?? DateTime.now(); + } + + @override + void didUpdateWidget(covariant oldWidget) { + dateTime = widget.dateTime; + if (widget.isRange) { + startDateTime = widget.dateTime; + endDateTime = widget.endDateTime; + } else { + startDateTime = endDateTime = null; + } + includeTime = widget.includeTime; + isRange = widget.isRange; + if (oldWidget.reminderOption != widget.reminderOption) { + reminderOption = widget.reminderOption; + } + super.didUpdateWidget(oldWidget); + } + + void onDateSelectedFromDatePicker( + DateTime? newStartDateTime, + DateTime? newEndDateTime, + ) { + if (newStartDateTime == null) { + return; + } + if (isRange) { + if (newEndDateTime == null) { + if (justChangedIsRange && dateTime != null) { + justChangedIsRange = false; + DateTime start = dateTime!; + DateTime end = combineDateTimes( + DateTime( + newStartDateTime.year, + newStartDateTime.month, + newStartDateTime.day, + ), + start, + ); + if (end.isBefore(start)) { + (start, end) = (end, start); + } + widget.onRangeSelected?.call(start, end); + setState(() { + // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. + dateTime = startDateTime = endDateTime = null; + focusedDateTime = getNewFocusedDay(newStartDateTime); + }); + } else { + final combined = combineDateTimes(newStartDateTime, dateTime); + setState(() { + dateTime = combined; + startDateTime = combined; + endDateTime = null; + focusedDateTime = getNewFocusedDay(combined); + }); + } + } else { + bool switched = false; + DateTime combinedDateTime = + combineDateTimes(newStartDateTime, dateTime); + DateTime combinedEndDateTime = + combineDateTimes(newEndDateTime, widget.endDateTime); + + if (combinedEndDateTime.isBefore(combinedDateTime)) { + (combinedDateTime, combinedEndDateTime) = + (combinedEndDateTime, combinedDateTime); + switched = true; + } + + widget.onRangeSelected?.call(combinedDateTime, combinedEndDateTime); + + setState(() { + dateTime = switched ? combinedDateTime : combinedEndDateTime; + startDateTime = combinedDateTime; + endDateTime = combinedEndDateTime; + focusedDateTime = getNewFocusedDay(newEndDateTime); + }); + } + } else { + final combinedDateTime = combineDateTimes(newStartDateTime, dateTime); + widget.onDaySelected?.call(combinedDateTime); + + setState(() { + dateTime = combinedDateTime; + focusedDateTime = getNewFocusedDay(combinedDateTime); + }); + } + } + + DateTime combineDateTimes(DateTime date, DateTime? time) { + final timeComponent = time == null + ? Duration.zero + : Duration(hours: time.hour, minutes: time.minute); + + return DateTime(date.year, date.month, date.day).add(timeComponent); + } + + void onDateTimeInputSubmitted(DateTime value) { + if (isRange) { + DateTime end = endDateTime ?? value; + if (end.isBefore(value)) { + (value, end) = (end, value); + } + + widget.onRangeSelected?.call(value, end); + + setState(() { + dateTime = value; + startDateTime = value; + endDateTime = end; + focusedDateTime = getNewFocusedDay(value); + }); + } else { + widget.onDaySelected?.call(value); + + setState(() { + dateTime = value; + focusedDateTime = getNewFocusedDay(value); + }); + } + } + + void onEndDateTimeInputSubmitted(DateTime value) { + if (isRange) { + if (endDateTime == null) { + value = combineDateTimes(value, widget.endDateTime); + } + DateTime start = startDateTime ?? value; + if (value.isBefore(start)) { + (start, value) = (value, start); + } + + widget.onRangeSelected?.call(start, value); + + if (endDateTime == null) { + // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. + setState(() { + dateTime = startDateTime = endDateTime = null; + focusedDateTime = getNewFocusedDay(value); + }); + } else { + setState(() { + dateTime = start; + startDateTime = start; + endDateTime = value; + focusedDateTime = getNewFocusedDay(value); + }); + } + } else { + widget.onDaySelected?.call(value); + + setState(() { + dateTime = value; + focusedDateTime = getNewFocusedDay(value); + }); + } + } + + DateTime getNewFocusedDay(DateTime dateTime) { + if (focusedDateTime.year != dateTime.year || + focusedDateTime.month != dateTime.month) { + return DateTime(dateTime.year, dateTime.month); + } else { + return focusedDateTime; + } + } + + void onIsRangeChanged(bool value) { + if (value) { + justChangedIsRange = true; + } + + final now = DateTime.now(); + final fillerDate = includeTime + ? DateTime(now.year, now.month, now.day, now.hour, now.minute) + : DateTime(now.year, now.month, now.day); + final newDateTime = dateTime ?? fillerDate; + + if (value) { + widget.onIsRangeChanged!.call(value, newDateTime, newDateTime); + } else { + widget.onIsRangeChanged!.call(value, null, null); + } + + setState(() { + isRange = value; + dateTime = newDateTime; + if (value) { + startDateTime = endDateTime = newDateTime; + } else { + startDateTime = endDateTime = null; + } + }); + } + + void onIncludeTimeChanged(bool value) { + late final DateTime? newDateTime; + late final DateTime? newEndDateTime; + + final now = DateTime.now(); + final fillerDate = value + ? DateTime(now.year, now.month, now.day, now.hour, now.minute) + : DateTime(now.year, now.month, now.day); + + if (value) { + // fill date if empty, add time component + newDateTime = dateTime == null + ? fillerDate + : combineDateTimes(dateTime!, fillerDate); + newEndDateTime = isRange + ? endDateTime == null + ? fillerDate + : combineDateTimes(endDateTime!, fillerDate) + : null; + } else { + // fill date if empty, remove time component + newDateTime = dateTime == null + ? fillerDate + : DateTime( + dateTime!.year, + dateTime!.month, + dateTime!.day, + ); + newEndDateTime = isRange + ? endDateTime == null + ? fillerDate + : DateTime( + endDateTime!.year, + endDateTime!.month, + endDateTime!.day, + ) + : null; + } + + widget.onIncludeTimeChanged!.call(value, newDateTime, newEndDateTime); + + setState(() { + includeTime = value; + dateTime = newDateTime ?? dateTime; + if (isRange) { + startDateTime = newDateTime ?? dateTime; + endDateTime = newEndDateTime ?? endDateTime; + } else { + startDateTime = endDateTime = null; + } + }); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart similarity index 59% rename from frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart index 58993dfe69..a89283fda4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/desktop_date_picker.dart @@ -1,11 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'appflowy_date_picker_base.dart'; import 'widgets/date_picker.dart'; import 'widgets/date_time_text_field.dart'; import 'widgets/end_time_button.dart'; @@ -17,110 +17,40 @@ class OptionGroup { final List options; } -typedef DaySelectedCallback = void Function(DateTime); -typedef RangeSelectedCallback = void Function(DateTime, DateTime); -typedef IncludeTimeChangedCallback = void Function(bool); - -class AppFlowyDatePicker extends StatefulWidget { - const AppFlowyDatePicker({ +class DesktopAppFlowyDatePicker extends AppFlowyDatePicker { + const DesktopAppFlowyDatePicker({ super.key, - required this.dateTime, - this.endDateTime, - required this.includeTime, - required this.isRange, - this.reminderOption = ReminderOption.none, - required this.dateFormat, - required this.timeFormat, + required super.dateTime, + super.endDateTime, + required super.includeTime, + required super.isRange, + super.reminderOption = ReminderOption.none, + required super.dateFormat, + required super.timeFormat, + super.onDaySelected, + super.onRangeSelected, + super.onIncludeTimeChanged, + super.onIsRangeChanged, + super.onReminderSelected, this.popoverMutex, this.options = const [], - this.onDaySelected, - this.onRangeSelected, - this.onIncludeTimeChanged, - this.onIsRangeChanged, - this.onReminderSelected, }); - final DateTime? dateTime; - final DateTime? endDateTime; - - final DateFormatPB dateFormat; - final TimeFormatPB timeFormat; - final PopoverMutex? popoverMutex; - final DaySelectedCallback? onDaySelected; - final RangeSelectedCallback? onRangeSelected; - - final bool includeTime; - final Function(bool)? onIncludeTimeChanged; - - final bool isRange; - final Function(bool)? onIsRangeChanged; - - final ReminderOption reminderOption; - final OnReminderSelected? onReminderSelected; - - /// A list of [OptionGroup] that will be rendered with proper - /// separators, each group can contain multiple options. - /// - /// __Supported on Desktop & Web__ - /// final List options; @override - State createState() => AppFlowyDatePickerState(); + State createState() => DesktopAppFlowyDatePickerState(); } @visibleForTesting -class AppFlowyDatePickerState extends State { - // store date values in the state and refresh the ui upon any changes made, instead of only updating them after receiving update from backend. - late DateTime? dateTime; - late DateTime? startDateTime; - late DateTime? endDateTime; - late bool includeTime; - late bool isRange; - late ReminderOption reminderOption; - - late DateTime focusedDateTime; - PageController? pageController; - - bool justChangedIsRange = false; - +class DesktopAppFlowyDatePickerState + extends AppFlowyDatePickerState { final isTabPressedNotifier = ValueNotifier(false); final refreshStartTextFieldNotifier = RefreshDateTimeTextFieldController(); final refreshEndTextFieldNotifier = RefreshDateTimeTextFieldController(); - @override - void initState() { - super.initState(); - - dateTime = widget.dateTime; - startDateTime = widget.isRange ? widget.dateTime : null; - endDateTime = widget.isRange ? widget.endDateTime : null; - includeTime = widget.includeTime; - isRange = widget.isRange; - reminderOption = widget.reminderOption; - - focusedDateTime = widget.dateTime ?? DateTime.now(); - } - - @override - void didUpdateWidget(covariant oldWidget) { - dateTime = widget.dateTime; - if (widget.isRange) { - startDateTime = widget.dateTime; - endDateTime = widget.endDateTime; - } else { - startDateTime = endDateTime = null; - } - includeTime = widget.includeTime; - isRange = widget.isRange; - if (oldWidget.reminderOption != widget.reminderOption) { - reminderOption = widget.reminderOption; - } - super.didUpdateWidget(oldWidget); - } - @override void dispose() { isTabPressedNotifier.dispose(); @@ -151,6 +81,7 @@ class AppFlowyDatePickerState extends State { isTabPressed: isTabPressedNotifier, refreshTextController: refreshStartTextFieldNotifier, onSubmitted: onDateTimeInputSubmitted, + showHint: true, ), if (isRange) ...[ const VSpace(8), @@ -164,6 +95,7 @@ class AppFlowyDatePickerState extends State { isTabPressed: isTabPressedNotifier, refreshTextController: refreshEndTextFieldNotifier, onSubmitted: onEndDateTimeInputSubmitted, + showHint: isRange && !(dateTime != null && endDateTime == null), ), ], const VSpace(14), @@ -203,16 +135,7 @@ class AppFlowyDatePickerState extends State { if (widget.onIsRangeChanged != null) ...[ EndTimeButton( isRange: isRange, - onChanged: (value) { - if (value) { - justChangedIsRange = true; - } - widget.onIsRangeChanged!.call(value); - if (dateTime != null && value) { - widget.onRangeSelected?.call(dateTime!, dateTime!); - } - setState(() => isRange = value); - }, + onChanged: onIsRangeChanged, ), const VSpace(4.0), ], @@ -221,10 +144,7 @@ class AppFlowyDatePickerState extends State { padding: const EdgeInsets.symmetric(horizontal: 12.0), child: IncludeTimeButton( includeTime: includeTime, - onChanged: (value) { - widget.onIncludeTimeChanged?.call(value); - setState(() => includeTime = value); - }, + onChanged: onIncludeTimeChanged, ), ), if (widget.onReminderSelected != null) ...[ @@ -306,88 +226,14 @@ class AppFlowyDatePickerState extends State { itemBuilder: (_, index) => options[index], ); - void onDateSelectedFromDatePicker( - DateTime? newStartDateTime, - DateTime? newEndDateTime, - ) { - if (newStartDateTime == null) { - return; - } - if (isRange) { - if (newEndDateTime == null) { - if (justChangedIsRange && dateTime != null) { - justChangedIsRange = false; - DateTime start = dateTime!; - DateTime end = DateTime( - newStartDateTime.year, - newStartDateTime.month, - newStartDateTime.day, - ); - if (end.isBefore(start)) { - (start, end) = (end, start); - } - widget.onRangeSelected?.call(start, end); - setState(() { - // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. - dateTime = startDateTime = endDateTime = null; - focusedDateTime = getNewFocusedDay(newStartDateTime); - }); - } else { - final combined = combineDateTimes(newStartDateTime, dateTime); - setState(() { - dateTime = combined; - startDateTime = combined; - endDateTime = null; - focusedDateTime = getNewFocusedDay(combined); - }); - } - } else { - bool switched = false; - DateTime combinedDateTime = - combineDateTimes(newStartDateTime, dateTime); - DateTime combinedEndDateTime = - combineDateTimes(newEndDateTime, widget.endDateTime); - - if (combinedEndDateTime.isBefore(combinedDateTime)) { - (combinedDateTime, combinedEndDateTime) = - (combinedEndDateTime, combinedDateTime); - switched = true; - } - - widget.onRangeSelected?.call(combinedDateTime, combinedEndDateTime); - - setState(() { - dateTime = switched ? combinedDateTime : combinedEndDateTime; - startDateTime = combinedDateTime; - endDateTime = combinedEndDateTime; - focusedDateTime = getNewFocusedDay(newEndDateTime); - }); - } - } else { - final combinedDateTime = combineDateTimes(newStartDateTime, dateTime); - widget.onDaySelected?.call(combinedDateTime); - - setState(() { - dateTime = combinedDateTime; - focusedDateTime = getNewFocusedDay(combinedDateTime); - }); - } - } - - DateTime combineDateTimes(DateTime date, DateTime? time) { - final timeComponent = time == null - ? Duration.zero - : Duration(hours: time.hour, minutes: time.minute); - - return DateTime(date.year, date.month, date.day).add(timeComponent); - } - + @override void onDateTimeInputSubmitted(DateTime value) { + refreshStartTextFieldNotifier.refresh(); if (isRange) { DateTime end = endDateTime ?? value; if (end.isBefore(value)) { (value, end) = (end, value); - refreshStartTextFieldNotifier.refresh(); + refreshEndTextFieldNotifier.refresh(); } widget.onRangeSelected?.call(value, end); @@ -396,7 +242,6 @@ class AppFlowyDatePickerState extends State { dateTime = value; startDateTime = value; endDateTime = end; - focusedDateTime = getNewFocusedDay(value); }); } else { widget.onDaySelected?.call(value); @@ -408,12 +253,17 @@ class AppFlowyDatePickerState extends State { } } + @override void onEndDateTimeInputSubmitted(DateTime value) { + refreshEndTextFieldNotifier.refresh(); if (isRange) { + if (endDateTime == null) { + value = combineDateTimes(value, widget.endDateTime); + } DateTime start = startDateTime ?? value; if (value.isBefore(start)) { (start, value) = (value, start); - refreshEndTextFieldNotifier.refresh(); + refreshStartTextFieldNotifier.refresh(); } widget.onRangeSelected?.call(start, value); @@ -441,15 +291,6 @@ class AppFlowyDatePickerState extends State { }); } } - - DateTime getNewFocusedDay(DateTime dateTime) { - if (focusedDateTime.year != dateTime.year || - focusedDateTime.month != dateTime.month) { - return DateTime(dateTime.year, dateTime.month); - } else { - return focusedDateTime; - } - } } class _GroupSeparator extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_date_picker.dart similarity index 58% rename from frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_date_picker.dart index d2d1fefae0..06c3f3975b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_appflowy_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/mobile_date_picker.dart @@ -5,7 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_she import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_option_decorate_box.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/mobile_date_editor.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; @@ -15,93 +15,35 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -class MobileAppFlowyDatePicker extends StatefulWidget { +import 'appflowy_date_picker_base.dart'; + +class MobileAppFlowyDatePicker extends AppFlowyDatePicker { const MobileAppFlowyDatePicker({ super.key, - this.dateTime, - this.endDateTime, - required this.dateFormat, - required this.timeFormat, - this.reminderOption = ReminderOption.none, - required this.includeTime, - required this.onIncludeTimeChanged, - this.isRange = false, - this.onIsRangeChanged, - this.onDaySelected, - this.onRangeSelected, + required super.dateTime, + super.endDateTime, + required super.includeTime, + required super.isRange, + super.reminderOption = ReminderOption.none, + required super.dateFormat, + required super.timeFormat, + super.onDaySelected, + super.onRangeSelected, + super.onIncludeTimeChanged, + super.onIsRangeChanged, + super.onReminderSelected, this.onClearDate, - this.onReminderSelected, }); - final DateTime? dateTime; - final DateTime? endDateTime; - - final bool isRange; - final bool includeTime; - - final TimeFormatPB timeFormat; - final DateFormatPB dateFormat; - - final ReminderOption reminderOption; - - final Function(bool)? onIncludeTimeChanged; - final Function(bool)? onIsRangeChanged; - - final DaySelectedCallback? onDaySelected; - final RangeSelectedCallback? onRangeSelected; final VoidCallback? onClearDate; - final OnReminderSelected? onReminderSelected; @override State createState() => _MobileAppFlowyDatePickerState(); } -class _MobileAppFlowyDatePickerState extends State { - // store date values in the state and refresh the ui upon any changes made, instead of only updating them after receiving update from backend. - late DateTime? dateTime; - late DateTime? startDateTime; - late DateTime? endDateTime; - late bool includeTime; - late bool isRange; - late ReminderOption reminderOption; - - late DateTime focusedDateTime; - PageController? pageController; - - bool justChangedIsRange = false; - - @override - void initState() { - super.initState(); - - dateTime = widget.dateTime; - startDateTime = widget.isRange ? widget.dateTime : null; - endDateTime = widget.isRange ? widget.endDateTime : null; - includeTime = widget.includeTime; - isRange = widget.isRange; - reminderOption = widget.reminderOption; - - focusedDateTime = widget.dateTime ?? DateTime.now(); - } - - @override - void didUpdateWidget(covariant oldWidget) { - dateTime = widget.dateTime; - if (widget.isRange) { - startDateTime = widget.dateTime; - endDateTime = widget.endDateTime; - } else { - startDateTime = endDateTime = null; - } - includeTime = widget.includeTime; - isRange = widget.isRange; - if (oldWidget.reminderOption != widget.reminderOption) { - reminderOption = widget.reminderOption; - } - super.didUpdateWidget(oldWidget); - } - +class _MobileAppFlowyDatePickerState + extends AppFlowyDatePickerState { @override Widget build(BuildContext context) { return Column( @@ -115,8 +57,8 @@ class _MobileAppFlowyDatePickerState extends State { isRange: isRange, dateFormat: widget.dateFormat, timeFormat: widget.timeFormat, - onStartTimeChanged: onStartTimeChanged, - onEndTimeChanged: onEndTimeChanged, + onStartTimeChanged: onDateTimeInputSubmitted, + onEndTimeChanged: onEndDateTimeInputSubmitted, ), ), const _Divider(), @@ -142,22 +84,13 @@ class _MobileAppFlowyDatePickerState extends State { if (widget.onIsRangeChanged != null) _IsRangeSwitch( isRange: widget.isRange, - onRangeChanged: (value) { - if (!isRange) { - justChangedIsRange = true; - } - widget.onIsRangeChanged!.call(value); - setState(() => isRange = value); - }, + onRangeChanged: onIsRangeChanged, ), if (widget.onIncludeTimeChanged != null) _IncludeTimeSwitch( showTopBorder: widget.onIsRangeChanged == null, includeTime: includeTime, - onIncludeTimeChanged: (includeTime) { - widget.onIncludeTimeChanged?.call(includeTime); - setState(() => this.includeTime = includeTime); - }, + onIncludeTimeChanged: onIncludeTimeChanged, ), if (widget.onReminderSelected != null) ...[ const _Divider(), @@ -184,149 +117,6 @@ class _MobileAppFlowyDatePickerState extends State { ], ); } - - void onDateSelectedFromDatePicker( - DateTime? newStartDateTime, - DateTime? newEndDateTime, - ) { - if (newStartDateTime == null) { - return; - } - if (isRange) { - if (newEndDateTime == null) { - if (justChangedIsRange && dateTime != null) { - justChangedIsRange = false; - DateTime start = dateTime!; - DateTime end = DateTime( - newStartDateTime.year, - newStartDateTime.month, - newStartDateTime.day, - ); - if (end.isBefore(start)) { - (start, end) = (end, start); - } - widget.onRangeSelected?.call(start, end); - setState(() { - // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. - dateTime = startDateTime = endDateTime = null; - focusedDateTime = getNewFocusedDay(newStartDateTime); - }); - } else { - final combined = combineDateTimes(newStartDateTime, dateTime); - setState(() { - dateTime = combined; - startDateTime = combined; - endDateTime = null; - focusedDateTime = getNewFocusedDay(combined); - }); - } - } else { - bool switched = false; - DateTime combinedDateTime = - combineDateTimes(newStartDateTime, dateTime); - DateTime combinedEndDateTime = - combineDateTimes(newEndDateTime, widget.endDateTime); - - if (combinedEndDateTime.isBefore(combinedDateTime)) { - (combinedDateTime, combinedEndDateTime) = - (combinedEndDateTime, combinedDateTime); - switched = true; - } - - widget.onRangeSelected?.call(combinedDateTime, combinedEndDateTime); - - setState(() { - dateTime = switched ? combinedDateTime : combinedEndDateTime; - startDateTime = combinedDateTime; - endDateTime = combinedEndDateTime; - focusedDateTime = getNewFocusedDay(newEndDateTime); - }); - } - } else { - final combinedDateTime = combineDateTimes(newStartDateTime, dateTime); - widget.onDaySelected?.call(combinedDateTime); - - setState(() { - dateTime = combinedDateTime; - focusedDateTime = getNewFocusedDay(combinedDateTime); - }); - } - } - - DateTime combineDateTimes(DateTime date, DateTime? time) { - final timeComponent = time == null - ? Duration.zero - : Duration(hours: time.hour, minutes: time.minute); - - return DateTime(date.year, date.month, date.day).add(timeComponent); - } - - void onStartTimeChanged(DateTime value) { - if (isRange) { - DateTime end = endDateTime ?? value; - if (end.isBefore(value)) { - (value, end) = (end, value); - } - - widget.onRangeSelected?.call(value, end); - - setState(() { - dateTime = value; - startDateTime = value; - endDateTime = end; - focusedDateTime = getNewFocusedDay(value); - }); - } else { - widget.onDaySelected?.call(value); - - setState(() { - dateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } - - void onEndTimeChanged(DateTime value) { - if (isRange) { - DateTime start = startDateTime ?? value; - if (value.isBefore(start)) { - (start, value) = (value, start); - } - - widget.onRangeSelected?.call(start, value); - - if (endDateTime == null) { - // hAcK: Resetting these state variables to null to reset the click counter of the table calendar widget, which doesn't expose a controller for us to do so otherwise. The parent widget needs to provide the data again so that it can be shown. - setState(() { - dateTime = startDateTime = endDateTime = null; - focusedDateTime = getNewFocusedDay(value); - }); - } else { - setState(() { - dateTime = start; - startDateTime = start; - endDateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } else { - widget.onDaySelected?.call(value); - - setState(() { - dateTime = value; - focusedDateTime = getNewFocusedDay(value); - }); - } - } - - DateTime getNewFocusedDay(DateTime dateTime) { - if (focusedDateTime.year != dateTime.year || - focusedDateTime.month != dateTime.month) { - return DateTime(dateTime.year, dateTime.month); - } else { - return focusedDateTime; - } - } } class _Divider extends StatelessWidget { @@ -495,23 +285,6 @@ class _TimePicker extends StatelessWidget { final endDateStr = getDateStr(endDateTime); final endTimeStr = getTimeStr(endDateTime); - if (dateStr.isEmpty) { - return const Divider(height: 1); - } - - if (endDateStr.isEmpty) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: _buildTime( - context, - dateStr, - timeStr, - includeTime, - false, - ), - ); - } - return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Column( @@ -524,14 +297,16 @@ class _TimePicker extends StatelessWidget { includeTime, true, ), - VSpace(8.0, color: Theme.of(context).colorScheme.surface), - _buildTime( - context, - endDateStr, - endTimeStr, - includeTime, - false, - ), + if (isRange) ...[ + VSpace(8.0, color: Theme.of(context).colorScheme.surface), + _buildTime( + context, + endDateStr, + endTimeStr, + includeTime, + false, + ), + ], ], ), ); @@ -539,41 +314,21 @@ class _TimePicker extends StatelessWidget { Widget _buildTime( BuildContext context, - String? dateStr, - String? timeStr, + String dateStr, + String timeStr, bool includeTime, bool isStartDay, ) { - if (dateStr == null) { - return const SizedBox.shrink(); - } - final List children = []; + final now = DateTime.now(); + final hintDate = DateTime(now.year, now.month, 1, 9); + if (!includeTime) { children.add( - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: GestureDetector( - onTap: () async { - final result = await _showDateTimePicker( - context, - isStartDay ? dateTime : endDateTime, - use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour, - mode: CupertinoDatePickerMode.date, - ); - handleDateTimePickerResult(result, isStartDay); - }, - child: FlowyText(dateStr), - ), - ), - ), - ); - } else { - children.addAll([ Expanded( child: GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () async { final result = await _showDateTimePicker( context, @@ -583,9 +338,39 @@ class _TimePicker extends StatelessWidget { ); handleDateTimePickerResult(result, isStartDay); }, - child: FlowyText( - dateStr, - textAlign: TextAlign.center, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 8, + ), + child: FlowyText( + dateStr.isNotEmpty ? dateStr : getDateStr(hintDate), + color: dateStr.isEmpty ? Theme.of(context).hintColor : null, + ), + ), + ), + ), + ); + } else { + children.addAll([ + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + final result = await _showDateTimePicker( + context, + isStartDay ? dateTime : endDateTime, + use24hFormat: timeFormat == TimeFormatPB.TwentyFourHour, + mode: CupertinoDatePickerMode.date, + ); + handleDateTimePickerResult(result, isStartDay); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: FlowyText( + dateStr.isNotEmpty ? dateStr : "", + textAlign: TextAlign.center, + ), ), ), ), @@ -596,6 +381,7 @@ class _TimePicker extends StatelessWidget { ), Expanded( child: GestureDetector( + behavior: HitTestBehavior.opaque, onTap: () async { final result = await _showDateTimePicker( context, @@ -605,9 +391,12 @@ class _TimePicker extends StatelessWidget { ); handleDateTimePickerResult(result, isStartDay); }, - child: FlowyText( - timeStr!, - textAlign: TextAlign.center, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: FlowyText( + timeStr.isNotEmpty ? timeStr : "", + textAlign: TextAlign.center, + ), ), ), ), @@ -623,7 +412,9 @@ class _TimePicker extends StatelessWidget { color: Theme.of(context).colorScheme.outline, ), ), - child: Row(children: children), + child: Row( + children: children, + ), ); } @@ -684,29 +475,14 @@ class _TimePicker extends StatelessWidget { if (dateTime == null) { return ""; } - final format = DateFormat( - switch (dateFormat) { - DateFormatPB.Local => 'MM/dd/y', - DateFormatPB.US => 'y/MM/dd', - DateFormatPB.ISO => 'y-MM-dd', - DateFormatPB.Friendly => 'MMM dd, y', - DateFormatPB.DayMonthYear => 'dd/MM/y', - _ => 'MMM dd, y', - }, - ); - - return format.format(dateTime); + return DateFormat(dateFormat.pattern).format(dateTime); } String getTimeStr(DateTime? dateTime) { if (dateTime == null || !includeTime) { return ""; } - final format = timeFormat == TimeFormatPB.TwelveHour - ? DateFormat.jm() - : DateFormat.Hm(); - - return format.format(dateTime); + return DateFormat(timeFormat.pattern).format(dateTime); } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart index ae8e58c468..301fd038ee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart @@ -1,4 +1,5 @@ -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart'; @@ -23,7 +24,7 @@ class DatePickerOptions { this.timeFormat = UserTimeFormatPB.TwentyFourHour, this.selectedReminderOption, this.onDaySelected, - required this.onIncludeTimeChanged, + this.onIncludeTimeChanged, this.onRangeSelected, this.onIsRangeChanged, this.onReminderSelected, @@ -40,8 +41,8 @@ class DatePickerOptions { final DaySelectedCallback? onDaySelected; final RangeSelectedCallback? onRangeSelected; - final IncludeTimeChangedCallback onIncludeTimeChanged; - final void Function(bool)? onIsRangeChanged; + final IncludeTimeChangedCallback? onIncludeTimeChanged; + final IsRangeChangedCallback? onIsRangeChanged; final OnReminderSelected? onReminderSelected; } @@ -156,11 +157,9 @@ class _AnimatedDatePicker extends StatelessWidget { Theme.of(context).colorScheme.shadow, ), constraints: BoxConstraints.loose(const Size(_datePickerWidth, 465)), - child: AppFlowyDatePicker( + child: DesktopAppFlowyDatePicker( includeTime: options.includeTime, - onIncludeTimeChanged: (includeTime) { - options.onIncludeTimeChanged.call(includeTime); - }, + onIncludeTimeChanged: options.onIncludeTimeChanged, isRange: options.isRange, onIsRangeChanged: options.onIsRangeChanged, dateFormat: options.dateFormat.simplified, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart index ad663420aa..acd50ce764 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart @@ -7,7 +7,7 @@ import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../appflowy_date_picker.dart'; +import '../desktop_date_picker.dart'; import 'date_picker.dart'; class DateTimeTextField extends StatefulWidget { @@ -21,6 +21,7 @@ class DateTimeTextField extends StatefulWidget { this.popoverMutex, this.isTabPressed, this.refreshTextController, + required this.showHint, }) : assert(includeTime && timeFormat != null || !includeTime); final DateTime? dateTime; @@ -31,6 +32,7 @@ class DateTimeTextField extends StatefulWidget { final PopoverMutex? popoverMutex; final ValueNotifier? isTabPressed; final RefreshDateTimeTextFieldController? refreshTextController; + final bool showHint; @override State createState() => _DateTimeTextFieldState(); @@ -48,6 +50,9 @@ class _DateTimeTextFieldState extends State { bool justSubmitted = false; + DateFormat get dateFormat => DateFormat(widget.dateFormat.pattern); + DateFormat get timeFormat => DateFormat(widget.timeFormat?.pattern); + @override void initState() { super.initState(); @@ -166,9 +171,6 @@ class _DateTimeTextFieldState extends State { return; } - final dateFormat = DateFormat(widget.dateFormat.pattern); - final timeFormat = DateFormat(widget.timeFormat?.pattern); - dateTextController.text = dateFormat.format(widget.dateTime!); timeTextController.text = timeFormat.format(widget.dateTime!); } @@ -196,39 +198,32 @@ class _DateTimeTextFieldState extends State { } void onTimeTextFieldSubmitted() { - final adjustedTimeStr = "Jan 01, 2000 ${timeTextController.text.trim()}"; - DateTime? dateTime = parseDateTimeStr(adjustedTimeStr); + // this happens in the middle of a date range selection + if (widget.dateTime == null) { + widget.refreshTextController?.refresh(); + statesController.update(WidgetState.error, true); + return; + } + final adjustedTimeStr = + "${dateTextController.text} ${timeTextController.text.trim()}"; + final dateTime = parseDateTimeStr(adjustedTimeStr); if (dateTime == null) { statesController.update(WidgetState.error, true); return; } statesController.update(WidgetState.error, false); - final dateComponent = widget.dateTime ?? DateTime.now(); - final timeComponent = Duration( - hours: dateTime.hour, - minutes: dateTime.minute, - seconds: dateTime.second, - ); - dateTime = DateTime( - dateComponent.year, - dateComponent.month, - dateComponent.day, - ).add(timeComponent); widget.onSubmitted?.call(dateTime); } DateTime? parseDateTimeStr(String string) { final locale = context.locale.toLanguageTag(); final parser = AnyDate.fromLocale(locale); - late DateTime? result; - try { - result = parser.parse(string); - if (result.isBefore(kFirstDay) || result.isAfter(kLastDay)) { - result = null; - } - } catch (err) { - result = null; + final result = parser.tryParse(string); + if (result == null || + result.isBefore(kFirstDay) || + result.isAfter(kLastDay)) { + return null; } return result; } @@ -248,6 +243,9 @@ class _DateTimeTextFieldState extends State { @override Widget build(BuildContext context) { + final now = DateTime.now(); + final hintDate = DateTime(now.year, now.month, 1, 9); + return Focus( focusNode: focusNode, skipTraversal: true, @@ -283,6 +281,7 @@ class _DateTimeTextFieldState extends State { style: Theme.of(context).textTheme.bodyMedium, decoration: getInputDecoration( const EdgeInsetsDirectional.fromSTEB(12, 6, 6, 6), + dateFormat.format(hintDate), ), onSubmitted: (value) { justSubmitted = true; @@ -304,6 +303,7 @@ class _DateTimeTextFieldState extends State { style: Theme.of(context).textTheme.bodyMedium, decoration: getInputDecoration( const EdgeInsetsDirectional.fromSTEB(6, 6, 12, 6), + timeFormat.format(hintDate), ), onSubmitted: (value) { justSubmitted = true; @@ -321,6 +321,7 @@ class _DateTimeTextFieldState extends State { style: Theme.of(context).textTheme.bodyMedium, decoration: getInputDecoration( const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + dateFormat.format(hintDate), ), onSubmitted: (value) { justSubmitted = true; @@ -348,12 +349,20 @@ class _DateTimeTextFieldState extends State { ); } - InputDecoration getInputDecoration(EdgeInsetsGeometry padding) { + InputDecoration getInputDecoration( + EdgeInsetsGeometry padding, + String? hintText, + ) { return InputDecoration( border: InputBorder.none, contentPadding: padding, isCollapsed: true, isDense: true, + hintText: widget.showHint ? hintText : null, + hintStyle: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).hintColor), ); } } diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart index 35c81b31c0..05f8965ab8 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/date_cell_bloc_test.dart @@ -2,8 +2,7 @@ import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:time/time.dart'; @@ -61,19 +60,18 @@ void main() { await gridResponseFuture(); final now = DateTime.now(); - final expected = DateTime(now.year, now.month, now.day); - bloc.add(const DateCellEditorEvent.setIncludeTime(true)); + bloc.add(DateCellEditorEvent.setIncludeTime(true, now, null)); await gridResponseFuture(); expect(bloc.state.includeTime, true); - expect(bloc.state.dateTime!.isAtSameMinuteAs(expected), true); + expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); expect(bloc.state.endDateTime, null); - bloc.add(const DateCellEditorEvent.setIncludeTime(false)); + bloc.add(const DateCellEditorEvent.setIncludeTime(false, null, null)); await gridResponseFuture(); expect(bloc.state.includeTime, false); - expect(bloc.state.dateTime!.isAtSameMinuteAs(expected), true); + expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); expect(bloc.state.endDateTime, null); }); @@ -97,14 +95,14 @@ void main() { expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); expect(bloc.state.endDateTime, null); - bloc.add(const DateCellEditorEvent.setIsRange(true)); + bloc.add(const DateCellEditorEvent.setIsRange(true, null, null)); await gridResponseFuture(); expect(bloc.state.isRange, true); expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); expect(bloc.state.endDateTime!.isAtSameMinuteAs(now), true); - bloc.add(const DateCellEditorEvent.setIsRange(false)); + bloc.add(const DateCellEditorEvent.setIsRange(false, null, null)); await gridResponseFuture(); expect(bloc.state.isRange, false); @@ -125,22 +123,69 @@ void main() { expect(bloc.state.endDateTime, null); final now = DateTime.now(); - final expected = DateTime(now.year, now.month, now.day); - bloc.add(const DateCellEditorEvent.setIsRange(true)); + bloc.add(DateCellEditorEvent.setIsRange(true, now, now)); await gridResponseFuture(); expect(bloc.state.isRange, true); - expect(bloc.state.dateTime!.isAtSameMinuteAs(expected), true); - expect(bloc.state.endDateTime!.isAtSameMinuteAs(expected), true); + expect(bloc.state.dateTime!.isAtSameDayAs(now), true); + expect(bloc.state.endDateTime!.isAtSameDayAs(now), true); - bloc.add(const DateCellEditorEvent.setIsRange(false)); + bloc.add(const DateCellEditorEvent.setIsRange(false, null, null)); await gridResponseFuture(); expect(bloc.state.isRange, false); - expect(bloc.state.dateTime!.isAtSameMinuteAs(expected), true); + expect(bloc.state.dateTime!.isAtSameDayAs(now), true); expect(bloc.state.endDateTime, null); }); + test('end time unexpected null', () async { + final reminderBloc = ReminderBloc(); + final bloc = DateCellEditorBloc( + cellController: cellController, + reminderBloc: reminderBloc, + ); + await gridResponseFuture(); + + expect(bloc.state.isRange, false); + expect(bloc.state.dateTime, null); + expect(bloc.state.endDateTime, null); + + final now = DateTime.now(); + // pass in unexpected null as end date time + bloc.add(DateCellEditorEvent.setIsRange(true, now, null)); + await gridResponseFuture(); + + // no changes + expect(bloc.state.isRange, false); + expect(bloc.state.dateTime, null); + expect(bloc.state.endDateTime, null); + }); + + test('end time unexpected end', () async { + final reminderBloc = ReminderBloc(); + final bloc = DateCellEditorBloc( + cellController: cellController, + reminderBloc: reminderBloc, + ); + await gridResponseFuture(); + + expect(bloc.state.isRange, false); + expect(bloc.state.dateTime, null); + expect(bloc.state.endDateTime, null); + + final now = DateTime.now(); + bloc.add(DateCellEditorEvent.setIsRange(true, now, now)); + await gridResponseFuture(); + + bloc.add(DateCellEditorEvent.setIsRange(false, now, now)); + await gridResponseFuture(); + + // no change + expect(bloc.state.isRange, true); + expect(bloc.state.dateTime!.isAtSameDayAs(now), true); + expect(bloc.state.endDateTime!.isAtSameDayAs(now), true); + }); + test('clear date', () async { final reminderBloc = ReminderBloc(); final bloc = DateCellEditorBloc( @@ -150,16 +195,15 @@ void main() { await gridResponseFuture(); final now = DateTime.now(); - final expected = DateTime(now.year, now.month, now.day); - bloc.add(const DateCellEditorEvent.setIsRange(true)); + bloc.add(DateCellEditorEvent.setIsRange(true, now, now)); await gridResponseFuture(); - bloc.add(const DateCellEditorEvent.setIncludeTime(true)); + bloc.add(DateCellEditorEvent.setIncludeTime(true, now, now)); await gridResponseFuture(); expect(bloc.state.isRange, true); expect(bloc.state.includeTime, true); - expect(bloc.state.dateTime!.isAtSameMinuteAs(expected), true); - expect(bloc.state.endDateTime!.isAtSameMinuteAs(expected), true); + expect(bloc.state.dateTime!.isAtSameMinuteAs(now), true); + expect(bloc.state.endDateTime!.isAtSameMinuteAs(now), true); bloc.add(const DateCellEditorEvent.clearDate()); await gridResponseFuture(); diff --git a/frontend/appflowy_flutter/test/widget_test/date_picker_test.dart b/frontend/appflowy_flutter/test/widget_test/date_picker_test.dart index 9521c68394..8e9e916aa7 100644 --- a/frontend/appflowy_flutter/test/widget_test/date_picker_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/date_picker_test.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart'; import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; -import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker_base.dart'; +import 'package:appflowy/workspace/presentation/widgets/date_picker/desktop_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_time_text_field.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; @@ -95,7 +96,7 @@ class _MockDatePickerState extends State<_MockDatePicker> { @override Widget build(BuildContext context) { - return AppFlowyDatePicker( + return DesktopAppFlowyDatePicker( dateTime: data.dateTime, endDateTime: data.endDateTime, includeTime: data.includeTime, @@ -115,16 +116,28 @@ class _MockDatePickerState extends State<_MockDatePicker> { data.endDateTime = end; }); }, - onIncludeTimeChanged: (value) async { + onIncludeTimeChanged: (value, dateTime, endDateTime) async { await Future.delayed(_mockDatePickerDelay); setState(() { data.includeTime = value; + if (dateTime != null) { + data.dateTime = dateTime; + } + if (endDateTime != null) { + data.endDateTime = endDateTime; + } }); }, - onIsRangeChanged: (value) async { + onIsRangeChanged: (value, dateTime, endDateTime) async { await Future.delayed(_mockDatePickerDelay); setState(() { data.isRange = value; + if (dateTime != null) { + data.dateTime = dateTime; + } + if (endDateTime != null) { + data.endDateTime = endDateTime; + } }); }, ); @@ -160,7 +173,9 @@ void main() { tester.state<_MockDatePickerState>(find.byType(_MockDatePicker)); AppFlowyDatePickerState getAfState(WidgetTester tester) => - tester.state(find.byType(AppFlowyDatePicker)); + tester.state( + find.byType(DesktopAppFlowyDatePicker), + ); group('AppFlowy date picker:', () { testWidgets('default state', (tester) async { @@ -172,7 +187,7 @@ void main() { await tester.pumpAndSettle(); - expect(find.byType(AppFlowyDatePicker), findsOneWidget); + expect(find.byType(DesktopAppFlowyDatePicker), findsOneWidget); expect( find.byWidgetPredicate( (w) => w is DateTimeTextField && w.dateTime == null, @@ -208,7 +223,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byType(AppFlowyDatePicker), findsOneWidget); + expect(find.byType(DesktopAppFlowyDatePicker), findsOneWidget); expect(find.byType(DateTimeTextField), findsNWidgets(2)); expect(find.byType(DatePicker), findsOneWidget); expect( @@ -468,26 +483,27 @@ void main() { afState = getAfState(tester); mockState = getMockState(tester); - expect(afState.dateTime, fourteenthDateTime); - expect(afState.startDateTime, null); - expect(afState.endDateTime, null); - expect(afState.justChangedIsRange, true); expect(afState.isRange, true); + expect(afState.dateTime, fourteenthDateTime); + expect(afState.startDateTime, fourteenthDateTime); + expect(afState.endDateTime, fourteenthDateTime); + expect(afState.justChangedIsRange, true); + expect(mockState.data.isRange, false); expect(mockState.data.dateTime, fourteenthDateTime); expect(mockState.data.endDateTime, null); - expect(mockState.data.isRange, false); await tester.pumpAndSettle(); afState = getAfState(tester); mockState = getMockState(tester); + expect(afState.isRange, true); expect(afState.dateTime, fourteenthDateTime); expect(afState.startDateTime, fourteenthDateTime); expect(afState.endDateTime, fourteenthDateTime); expect(afState.justChangedIsRange, true); + expect(mockState.data.isRange, true); expect(mockState.data.dateTime, fourteenthDateTime); expect(mockState.data.endDateTime, fourteenthDateTime); - expect(mockState.data.isRange, true); final twentyFirst = dayInDatePicker(21).first; await tester.tap(twentyFirst); @@ -508,13 +524,17 @@ void main() { testWidgets('include time and modify', (tester) async { final now = DateTime.now(); - final fourteenthDateTime = DateTime(now.year, now.month, 14); + final fourteenthDateTime = now.copyWith(day: 14); await tester.pumpWidget( WidgetTestApp( child: _MockDatePicker( data: _DatePickerDataStub( - dateTime: fourteenthDateTime, + dateTime: DateTime( + fourteenthDateTime.year, + fourteenthDateTime.month, + fourteenthDateTime.day, + ), endDateTime: null, includeTime: false, isRange: false, @@ -526,7 +546,8 @@ void main() { AppFlowyDatePickerState afState = getAfState(tester); _MockDatePickerState mockState = getMockState(tester); - expect(afState.dateTime, fourteenthDateTime); + expect(afState.dateTime!.isAtSameDayAs(fourteenthDateTime), true); + expect(afState.dateTime!.isAtSameMinuteAs(fourteenthDateTime), false); expect(afState.startDateTime, null); expect(afState.endDateTime, null); expect(afState.includeTime, false); @@ -541,14 +562,24 @@ void main() { afState = getAfState(tester); mockState = getMockState(tester); - expect(afState.dateTime, fourteenthDateTime); + expect(afState.dateTime!.isAtSameMinuteAs(fourteenthDateTime), true); expect(afState.includeTime, true); - expect(mockState.data.dateTime, fourteenthDateTime); + expect( + mockState.data.dateTime!.isAtSameDayAs(fourteenthDateTime), + true, + ); + expect( + mockState.data.dateTime!.isAtSameMinuteAs(fourteenthDateTime), + false, + ); expect(mockState.data.includeTime, false); await tester.pumpAndSettle(300.milliseconds); mockState = getMockState(tester); - expect(mockState.data.dateTime, fourteenthDateTime); + expect( + mockState.data.dateTime!.isAtSameMinuteAs(fourteenthDateTime), + true, + ); expect(mockState.data.includeTime, true); final timeField = find.byKey(const ValueKey('date_time_text_field_time')); @@ -598,9 +629,66 @@ void main() { expect(mockState.data.dateTime, expected); }); + testWidgets( + 'turn on include time, turn on end date, then select date range', + (tester) async { + final fourteenth = DateTime(2024, 10, 14); + + await tester.pumpWidget( + WidgetTestApp( + child: _MockDatePicker( + data: _DatePickerDataStub( + dateTime: fourteenth, + endDateTime: null, + includeTime: false, + isRange: false, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap( + find.descendant( + of: find.byType(EndTimeButton), + matching: find.byType(Toggle), + ), + ); + await tester.pumpAndSettle(); + + final now = DateTime.now(); + await tester.tap( + find.descendant( + of: find.byType(IncludeTimeButton), + matching: find.byType(Toggle), + ), + ); + await tester.pumpAndSettle(); + + final third = dayInDatePicker(21).first; + await tester.tap(third); + await tester.pumpAndSettle(); + + final afState = getAfState(tester); + final mockState = getMockState(tester); + final expectedTime = Duration(hours: now.hour, minutes: now.minute); + final expectedStart = fourteenth.add(expectedTime); + final expectedEnd = fourteenth.copyWith(day: 21).add(expectedTime); + expect(afState.justChangedIsRange, false); + expect(afState.includeTime, true); + expect(afState.isRange, true); + expect(afState.dateTime, expectedStart); + expect(afState.startDateTime, expectedStart); + expect(afState.endDateTime, expectedEnd); + expect(mockState.data.dateTime, expectedStart); + expect(mockState.data.endDateTime, expectedEnd); + expect(mockState.data.isRange, true); + }, + ); + testWidgets('edit text field causes start and end to get swapped', (tester) async { - final fourteenth = DateTime(2024, 10, 14); + final fourteenth = DateTime(2024, 10, 14, 1); await tester.pumpWidget( WidgetTestApp( @@ -608,7 +696,7 @@ void main() { data: _DatePickerDataStub( dateTime: fourteenth, endDateTime: fourteenth, - includeTime: false, + includeTime: true, isRange: true, ), ), @@ -633,7 +721,7 @@ void main() { await tester.pumpAndSettle(); await tester.pumpAndSettle(); - final bday = DateTime(2024, 11, 30); + final bday = DateTime(2024, 11, 30, 1); expect( find.descendant( @@ -660,9 +748,10 @@ void main() { expect(mockState.data.endDateTime, bday); }); - testWidgets('select start with calendar and then enter end with keyboard', + testWidgets( + 'select start date with calendar and then enter end date with keyboard', (tester) async { - final fourteenth = DateTime(2024, 10, 14); + final fourteenth = DateTime(2024, 10, 14, 1); await tester.pumpWidget( WidgetTestApp( @@ -670,7 +759,7 @@ void main() { data: _DatePickerDataStub( dateTime: fourteenth, endDateTime: fourteenth, - includeTime: false, + includeTime: true, isRange: true, ), ), @@ -682,7 +771,7 @@ void main() { await tester.tap(third); await tester.pumpAndSettle(); - final start = DateTime(2024, 10, 3); + final start = DateTime(2024, 10, 3, 1); AppFlowyDatePickerState afState = getAfState(tester); _MockDatePickerState mockState = getMockState(tester); @@ -703,7 +792,7 @@ void main() { await tester.pumpAndSettle(); await tester.pumpAndSettle(); - final end = DateTime(2024, 10, 18); + final end = DateTime(2024, 10, 18, 1); expect( find.descendant( @@ -735,7 +824,7 @@ void main() { // make sure click counter was reset final twentyFifth = dayInDatePicker(25).first; - final expected = DateTime(2024, 10, 25); + final expected = DateTime(2024, 10, 25, 1); await tester.tap(twentyFifth); await tester.pumpAndSettle(); afState = getAfState(tester); @@ -746,5 +835,79 @@ void main() { expect(mockState.data.dateTime, start); expect(mockState.data.endDateTime, end); }); + + testWidgets('same as above but enter time', (tester) async { + final fourteenth = DateTime(2024, 10, 14, 1); + + await tester.pumpWidget( + WidgetTestApp( + child: _MockDatePicker( + data: _DatePickerDataStub( + dateTime: fourteenth, + endDateTime: fourteenth, + includeTime: true, + isRange: true, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + final third = dayInDatePicker(3).first; + await tester.tap(third); + await tester.pumpAndSettle(); + + final start = DateTime(2024, 10, 3, 1); + + final dateTextField = find.descendant( + of: find.byKey(const ValueKey('end_date_time_text_field')), + matching: find.byKey(const ValueKey('date_time_text_field_time')), + ); + expect(dateTextField, findsOneWidget); + await tester.enterText(dateTextField, "15:00"); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + + expect( + find.descendant( + of: find.byKey(const ValueKey('date_time_text_field')), + matching: find.text( + DateFormat(DateFormatPB.Friendly.pattern).format(start), + ), + ), + findsOneWidget, + ); + + expect( + find.descendant( + of: find.byKey(const ValueKey('end_date_time_text_field')), + matching: find.text("15:00"), + ), + findsNothing, + ); + + AppFlowyDatePickerState afState = getAfState(tester); + _MockDatePickerState mockState = getMockState(tester); + expect(afState.dateTime, start); + expect(afState.startDateTime, start); + expect(afState.endDateTime, null); + expect(mockState.data.dateTime, fourteenth); + expect(mockState.data.endDateTime, fourteenth); + + // select for real now + final twentyFifth = dayInDatePicker(25).first; + final expected = DateTime(2024, 10, 25, 1); + await tester.tap(twentyFifth); + await tester.pumpAndSettle(); + await tester.pumpAndSettle(); + afState = getAfState(tester); + mockState = getMockState(tester); + expect(afState.dateTime, start); + expect(afState.startDateTime, start); + expect(afState.endDateTime, expected); + expect(mockState.data.dateTime, start); + expect(mockState.data.endDateTime, expected); + }); }); }