mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2025-12-12 07:34:28 +00:00
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:
parent
1b9b2a5f8d
commit
cf56e20be9
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
),
|
||||
|
||||
@ -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),
|
||||
],
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user