chore: launch review 0.7.3 (#6698)

* refactor: date picker

* chore: provide guidance to users while using date picker

* fix: row card icon alignment

* fix: untitled database views

* chore: hide hint text while choosing date range

* test: fix widget test

* chore: use current time when toggling include time

* chore: move autofill date logic to date picker

* test: add tests

* chore: also apply to mention date block

* test: fix integration tests

* chore: fix a date picker edge case

* fix: unmatching border radii
This commit is contained in:
Richard Shiue 2024-11-04 05:11:56 +03:00 committed by GitHub
parent 1b9b2a5f8d
commit cf56e20be9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 856 additions and 638 deletions

View File

@ -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);

View File

@ -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,

View File

@ -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;

View File

@ -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<MobileDateCellEditScreen> {
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());

View File

@ -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<void> _updateIsRange(bool isRange) async {
final dateTime = state.dateTime == null ? DateTime.now().withoutTime : null;
final endDateTime = dateTime;
Future<void> _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<void> _updateIncludeTime(bool includeTime) async {
final dateTime = state.dateTime ?? DateTime.now().withoutTime;
final endDateTime = state.isRange ? state.endDateTime ?? dateTime : null;
Future<void> _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;

View File

@ -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,

View File

@ -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<DateFilterEditor> {
child: SingleFilterBlocSelector<DateTimeFilter>(
filterId: widget.filterId,
builder: (context, filter, field) {
return AppFlowyDatePicker(
return DesktopAppFlowyDatePicker(
isRange: isRange,
includeTime: false,
dateFormat: DateFormatPB.Friendly,

View File

@ -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,

View File

@ -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,
),

View File

@ -146,9 +146,12 @@ class _TextCellState extends State<TextCardCell> {
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<TextCardCell> {
return BlocBuilder<TextCellBloc, TextCellState>(
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),
],
);

View File

@ -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 {

View File

@ -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,

View File

@ -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<DateCellEditor> {
child: BlocBuilder<DateCellEditorBloc, DateCellEditorState>(
builder: (context, state) {
final dateCellBloc = context.read<DateCellEditorBloc>();
return AppFlowyDatePicker(
return DesktopAppFlowyDatePicker(
dateTime: state.dateTime,
endDateTime: state.endDateTime,
dateFormat: state.dateTypeOptionPB.dateFormat,
@ -77,11 +77,19 @@ class _DateCellEditor extends State<DateCellEditor> {
],
),
],
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));

View File

@ -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<MentionDateBlock> {
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<MentionDateBlock> {
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<MentionDateBlock> {
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,

View File

@ -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<T extends AppFlowyDatePicker>
extends State<T> {
// 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;
}
});
}
}

View File

@ -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<Widget> 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<OptionGroup> options;
@override
State<AppFlowyDatePicker> createState() => AppFlowyDatePickerState();
State<AppFlowyDatePicker> createState() => DesktopAppFlowyDatePickerState();
}
@visibleForTesting
class AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
// 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<DesktopAppFlowyDatePicker> {
final isTabPressedNotifier = ValueNotifier<bool>(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<AppFlowyDatePicker> {
isTabPressed: isTabPressedNotifier,
refreshTextController: refreshStartTextFieldNotifier,
onSubmitted: onDateTimeInputSubmitted,
showHint: true,
),
if (isRange) ...[
const VSpace(8),
@ -164,6 +95,7 @@ class AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
isTabPressed: isTabPressedNotifier,
refreshTextController: refreshEndTextFieldNotifier,
onSubmitted: onEndDateTimeInputSubmitted,
showHint: isRange && !(dateTime != null && endDateTime == null),
),
],
const VSpace(14),
@ -203,16 +135,7 @@ class AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
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<AppFlowyDatePicker> {
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<AppFlowyDatePicker> {
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<AppFlowyDatePicker> {
dateTime = value;
startDateTime = value;
endDateTime = end;
focusedDateTime = getNewFocusedDay(value);
});
} else {
widget.onDaySelected?.call(value);
@ -408,12 +253,17 @@ class AppFlowyDatePickerState extends State<AppFlowyDatePicker> {
}
}
@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<AppFlowyDatePicker> {
});
}
}
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 {

View File

@ -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<MobileAppFlowyDatePicker> createState() =>
_MobileAppFlowyDatePickerState();
}
class _MobileAppFlowyDatePickerState extends State<MobileAppFlowyDatePicker> {
// 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<MobileAppFlowyDatePicker> {
@override
Widget build(BuildContext context) {
return Column(
@ -115,8 +57,8 @@ class _MobileAppFlowyDatePickerState extends State<MobileAppFlowyDatePicker> {
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<MobileAppFlowyDatePicker> {
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<MobileAppFlowyDatePicker> {
],
);
}
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<Widget> 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);
}
}

View File

@ -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,

View File

@ -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<bool>? isTabPressed;
final RefreshDateTimeTextFieldController? refreshTextController;
final bool showHint;
@override
State<DateTimeTextField> createState() => _DateTimeTextFieldState();
@ -48,6 +50,9 @@ class _DateTimeTextFieldState extends State<DateTimeTextField> {
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<DateTimeTextField> {
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<DateTimeTextField> {
}
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<DateTimeTextField> {
@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<DateTimeTextField> {
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<DateTimeTextField> {
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<DateTimeTextField> {
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<DateTimeTextField> {
);
}
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),
);
}
}

View File

@ -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();

View File

@ -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<AppFlowyDatePickerState>(find.byType(AppFlowyDatePicker));
tester.state<DesktopAppFlowyDatePickerState>(
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);
});
});
}