feat: rewrite date logic (#2390)

* chore: remove unused fields

* chore: rewrite date logic

* chore: apply suggestions from Alex

* chore: add space in date format

* chore: re-add error handling in apply-changeset
This commit is contained in:
Richard Shiue
2023-05-09 22:37:20 +08:00
committed by GitHub
parent ba8cbe170c
commit 376f2d887d
21 changed files with 765 additions and 411 deletions

View File

@ -29,7 +29,7 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
@freezed
class DateCellData with _$DateCellData {
const factory DateCellData({
required DateTime date,
DateTime? dateTime,
String? time,
required bool includeTime,
}) = _DateCellData;
@ -45,19 +45,14 @@ class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
Future<Option<FlowyError>> save(DateCellData data) {
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
// This is a bit of a hack. This converts the data.date which is in
// UTC to Local but actually changes the timestamp instead of just
// changing the isUtc flag
final dateTime = DateTime(data.date.year, data.date.month, data.date.day);
final date = (dateTime.millisecondsSinceEpoch ~/ 1000).toString();
payload.date = date;
payload.isUtc = data.date.isUtc;
payload.includeTime = data.includeTime;
if (data.dateTime != null) {
final date = (data.dateTime!.millisecondsSinceEpoch ~/ 1000).toString();
payload.date = date;
}
if (data.time != null) {
payload.time = data.time!;
}
payload.includeTime = data.includeTime;
return DatabaseEventUpdateDateCell(payload).send().then((result) {
return result.fold(

View File

@ -342,10 +342,9 @@ class RowDataBuilder {
_cellDataByFieldId[fieldInfo.field.id] = num.toString();
}
/// The date should use the UTC timezone. Becuase the backend uses UTC timezone to format the time string.
void insertDate(FieldInfo fieldInfo, DateTime date) {
assert(fieldInfo.fieldType == FieldType.DateTime);
final timestamp = (date.toUtc().millisecondsSinceEpoch ~/ 1000);
final timestamp = date.millisecondsSinceEpoch ~/ 1000;
_cellDataByFieldId[fieldInfo.field.id] = timestamp.toString();
}

View File

@ -53,10 +53,6 @@ class DateTypeOptionBloc
if (timeFormat != null) {
typeOption.timeFormat = timeFormat;
}
if (includeTime != null) {
typeOption.includeTime = includeTime;
}
});
}
}

View File

@ -12,7 +12,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:table_calendar/table_calendar.dart';
import 'dart:async';
import 'package:dartz/dartz.dart';
import 'package:protobuf/protobuf.dart';
part 'date_cal_bloc.freezed.dart';
@ -31,45 +30,39 @@ class DateCellCalendarBloc
(event, emit) async {
await event.when(
initial: () async => _startListening(),
selectDay: (date) async {
await _updateDateData(emit, date: date, time: state.time);
},
setCalFormat: (format) {
emit(state.copyWith(format: format));
},
setFocusedDay: (focusedDay) {
emit(state.copyWith(focusedDay: focusedDay));
},
didReceiveCellUpdate: (DateCellDataPB? cellData) {
final dateCellData = calDataFromCellData(cellData);
final time = dateCellData.foldRight(
"",
(dateData, previous) => dateData.time ?? '',
final dateData = _dateDataFromCellData(cellData);
emit(
state.copyWith(
dateTime: dateData.dateTime,
time: dateData.time,
includeTime: dateData.includeTime,
),
);
emit(state.copyWith(dateCellData: dateCellData, time: time));
},
didReceiveTimeFormatError: (String? timeFormatError) {
emit(state.copyWith(timeFormatError: timeFormatError));
},
selectDay: (date) async {
await _updateDateData(emit, date: date);
},
setIncludeTime: (includeTime) async {
await _updateDateData(emit, includeTime: includeTime);
},
setTime: (time) async {
await _updateDateData(emit, time: time);
},
setDateFormat: (dateFormat) async {
await _updateTypeOption(emit, dateFormat: dateFormat);
},
setTimeFormat: (timeFormat) async {
await _updateTypeOption(emit, timeFormat: timeFormat);
},
setTime: (time) async {
if (state.dateCellData.isSome()) {
await _updateDateData(emit, time: time);
}
setCalFormat: (format) {
emit(state.copyWith(format: format));
},
didUpdateCalData:
(Option<DateCellData> data, Option<String> timeFormatError) {
emit(
state.copyWith(
dateCellData: data,
timeFormatError: timeFormatError,
),
);
setFocusedDay: (focusedDay) {
emit(state.copyWith(focusedDay: focusedDay));
},
);
},
@ -81,65 +74,44 @@ class DateCellCalendarBloc
DateTime? date,
String? time,
bool? includeTime,
}) {
final DateCellData newDateData = state.dateCellData.fold(
() => DateCellData(
date: date ?? DateTime.now(),
time: time,
includeTime: includeTime ?? false,
),
(dateData) {
var newDateData = dateData;
if (date != null && !isSameDay(newDateData.date, date)) {
newDateData = newDateData.copyWith(date: date);
}
}) async {
// make sure date and time are not updated together from the UI
assert(
date == null && time == null ||
date == null && time != null ||
date != null && time == null,
);
String? newTime = time ?? state.time;
if (newDateData.time != time) {
newDateData = newDateData.copyWith(time: time);
}
DateTime? newDate = date;
if (time != null && time.isNotEmpty) {
newDate = state.dateTime ?? DateTime.now();
}
if (includeTime != null && newDateData.includeTime != includeTime) {
newDateData = newDateData.copyWith(includeTime: includeTime);
}
return newDateData;
},
final DateCellData newDateData = DateCellData(
dateTime: newDate,
time: newTime,
includeTime: includeTime ?? state.includeTime,
);
return _saveDateData(emit, newDateData);
}
Future<void> _saveDateData(
Emitter<DateCellCalendarState> emit,
DateCellData newCalData,
) async {
if (state.dateCellData == Some(newCalData)) {
return;
}
updateCalData(
Option<DateCellData> dateCellData,
Option<String> timeFormatError,
) {
if (!isClosed) {
add(
DateCellCalendarEvent.didUpdateCalData(
dateCellData,
timeFormatError,
),
);
}
}
cellController.saveCellData(
newCalData,
newDateData,
onFinish: (result) {
result.fold(
() => updateCalData(Some(newCalData), none()),
() {
if (!isClosed && state.timeFormatError != null) {
add(const DateCellCalendarEvent.didReceiveTimeFormatError(null));
}
},
(err) {
switch (ErrorCode.valueOf(err.code)!) {
case ErrorCode.InvalidDateTimeFormat:
updateCalData(state.dateCellData, Some(timeFormatPrompt(err)));
if (isClosed) return;
add(
DateCellCalendarEvent.didReceiveTimeFormatError(
timeFormatPrompt(err),
),
);
break;
default:
Log.error(err);
@ -221,25 +193,33 @@ class DateCellCalendarBloc
@freezed
class DateCellCalendarEvent with _$DateCellCalendarEvent {
// initial event
const factory DateCellCalendarEvent.initial() = _Initial;
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
// notification that cell is updated in the backend
const factory DateCellCalendarEvent.didReceiveCellUpdate(
DateCellDataPB? data,
) = _DidReceiveCellUpdate;
const factory DateCellCalendarEvent.didReceiveTimeFormatError(
String? timeformatError,
) = _DidReceiveTimeFormatError;
// table calendar's UI settings
const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay;
const factory DateCellCalendarEvent.setCalFormat(CalendarFormat format) =
_CalendarFormat;
const factory DateCellCalendarEvent.setFocusedDay(DateTime day) = _FocusedDay;
// date cell data is modified
const factory DateCellCalendarEvent.selectDay(DateTime day) = _SelectDay;
const factory DateCellCalendarEvent.setTime(String time) = _Time;
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
_IncludeTime;
// date field type options are modified
const factory DateCellCalendarEvent.setTimeFormat(TimeFormatPB timeFormat) =
_TimeFormat;
const factory DateCellCalendarEvent.setDateFormat(DateFormatPB dateFormat) =
_DateFormat;
const factory DateCellCalendarEvent.setIncludeTime(bool includeTime) =
_IncludeTime;
const factory DateCellCalendarEvent.setTime(String time) = _Time;
const factory DateCellCalendarEvent.didReceiveCellUpdate(
DateCellDataPB? data,
) = _DidReceiveCellUpdate;
const factory DateCellCalendarEvent.didUpdateCalData(
Option<DateCellData> data,
Option<String> timeFormatError,
) = _DidUpdateCalData;
}
@freezed
@ -248,9 +228,10 @@ class DateCellCalendarState with _$DateCellCalendarState {
required DateTypeOptionPB dateTypeOptionPB,
required CalendarFormat format,
required DateTime focusedDay,
required Option<String> timeFormatError,
required Option<DateCellData> dateCellData,
required DateTime? dateTime,
required String? time,
required bool includeTime,
required String? timeFormatError,
required String timeHintText,
}) = _DateCellCalendarState;
@ -258,16 +239,15 @@ class DateCellCalendarState with _$DateCellCalendarState {
DateTypeOptionPB dateTypeOptionPB,
DateCellDataPB? cellData,
) {
Option<DateCellData> dateCellData = calDataFromCellData(cellData);
final time =
dateCellData.foldRight("", (dateData, previous) => dateData.time ?? '');
final dateData = _dateDataFromCellData(cellData);
return DateCellCalendarState(
dateTypeOptionPB: dateTypeOptionPB,
format: CalendarFormat.month,
focusedDay: DateTime.now(),
time: time,
dateCellData: dateCellData,
timeFormatError: none(),
dateTime: dateData.dateTime,
time: dateData.time,
includeTime: dateData.includeTime,
timeFormatError: null,
timeHintText: _timeHintText(dateTypeOptionPB),
);
}
@ -284,27 +264,21 @@ String _timeHintText(DateTypeOptionPB typeOption) {
}
}
Option<DateCellData> calDataFromCellData(DateCellDataPB? cellData) {
String? time = timeFromCellData(cellData);
Option<DateCellData> dateData = none();
if (cellData != null) {
DateCellData _dateDataFromCellData(DateCellDataPB? cellData) {
// a null DateCellDataPB may be returned, indicating that all the fields are
// at their default values: empty strings and false booleans
if (cellData == null) {
return const DateCellData(includeTime: false);
}
DateTime? dateTime;
String? time;
if (cellData.hasTimestamp()) {
final timestamp = cellData.timestamp * 1000;
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
dateData = Some(
DateCellData(
date: date,
time: time,
includeTime: cellData.includeTime,
),
);
dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
time = cellData.time;
}
return dateData;
}
bool includeTime = cellData.includeTime;
String? timeFromCellData(DateCellDataPB? cellData) {
if (cellData == null || !cellData.hasTime()) {
return null;
}
return cellData.time;
return DateCellData(dateTime: dateTime, time: time, includeTime: includeTime);
}

View File

@ -79,7 +79,11 @@ class DateCellState with _$DateCellState {
String _dateStrFromCellData(DateCellDataPB? cellData) {
String dateStr = "";
if (cellData != null) {
dateStr = "${cellData.date} ${cellData.time}";
if (cellData.includeTime) {
dateStr = "${cellData.date} ${cellData.time}";
} else {
dateStr = cellData.date;
}
}
return dateStr;
}

View File

@ -10,8 +10,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/time/duration.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
import 'package:flutter/material.dart';
@ -89,41 +89,35 @@ class _CellCalendarWidget extends StatefulWidget {
class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
late PopoverMutex popoverMutex;
late DateCellCalendarBloc bloc;
@override
void initState() {
popoverMutex = PopoverMutex();
bloc = DateCellCalendarBloc(
dateTypeOptionPB: widget.dateTypeOptionPB,
cellData: widget.cellContext.getCellData(),
cellController: widget.cellContext,
)..add(const DateCellCalendarEvent.initial());
super.initState();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
return BlocProvider(
create: (context) => DateCellCalendarBloc(
dateTypeOptionPB: widget.dateTypeOptionPB,
cellData: widget.cellContext.getCellData(),
cellController: widget.cellContext,
)..add(const DateCellCalendarEvent.initial()),
child: BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
buildWhen: (p, c) => p != c,
builder: (context, state) {
bool includeTime = state.dateCellData
.fold(() => false, (dateData) => dateData.includeTime);
List<Widget> children = [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: _buildCalendar(context),
),
if (includeTime) ...[
const VSpace(12.0),
_TimeTextField(
bloc: context.read<DateCellCalendarBloc>(),
popoverMutex: popoverMutex,
),
],
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: state.includeTime
? _TimeTextField(popoverMutex: popoverMutex)
: const SizedBox(),
),
const TypeOptionSeparator(spacing: 12.0),
const _IncludeTimeButton(),
const TypeOptionSeparator(spacing: 12.0),
@ -144,7 +138,6 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
@override
void dispose() {
bloc.close();
popoverMutex.dispose();
super.dispose();
}
@ -208,16 +201,11 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
outsideTextStyle:
textStyle.textColor(Theme.of(context).disabledColor),
),
selectedDayPredicate: (day) {
return state.dateCellData.fold(
() => false,
(dateData) => isSameDay(dateData.date, day),
);
},
selectedDayPredicate: (day) => isSameDay(state.dateTime, day),
onDaySelected: (selectedDay, focusedDay) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.selectDay(selectedDay));
context.read<DateCellCalendarBloc>().add(
DateCellCalendarEvent.selectDay(selectedDay.toLocal().date),
);
},
onFormatChanged: (format) {
context
@ -241,10 +229,7 @@ class _IncludeTimeButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
selector: (state) => state.dateCellData.fold(
() => false,
(dateData) => dateData.includeTime,
),
selector: (state) => state.includeTime,
builder: (context, includeTime) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
@ -258,7 +243,7 @@ class _IncludeTimeButton extends StatelessWidget {
"grid/clock",
color: Theme.of(context).iconTheme.color,
),
const HSpace(4),
const HSpace(6),
FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()),
const Spacer(),
Toggle(
@ -280,11 +265,9 @@ class _IncludeTimeButton extends StatelessWidget {
}
class _TimeTextField extends StatefulWidget {
final DateCellCalendarBloc bloc;
final PopoverMutex popoverMutex;
const _TimeTextField({
required this.bloc,
required this.popoverMutex,
Key? key,
}) : super(key: key);
@ -295,18 +278,10 @@ class _TimeTextField extends StatefulWidget {
class _TimeTextFieldState extends State<_TimeTextField> {
late final FocusNode _focusNode;
late final TextEditingController _controller;
@override
void initState() {
_focusNode = FocusNode();
_controller = TextEditingController(text: widget.bloc.state.time);
_focusNode.addListener(() {
if (mounted) {
widget.bloc.add(DateCellCalendarEvent.setTime(_controller.text));
}
});
_focusNode.addListener(() {
if (_focusNode.hasFocus) {
@ -325,38 +300,31 @@ class _TimeTextFieldState extends State<_TimeTextField> {
@override
Widget build(BuildContext context) {
_controller.text = widget.bloc.state.time ?? "";
_controller.selection =
TextSelection.collapsed(offset: _controller.text.length);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: RoundedInputField(
height: GridSize.popoverItemHeight,
focusNode: _focusNode,
autoFocus: true,
hintText: widget.bloc.state.timeHintText,
controller: _controller,
style: Theme.of(context).textTheme.bodyMedium!,
normalBorderColor: Theme.of(context).colorScheme.outline,
errorBorderColor: Theme.of(context).colorScheme.error,
focusBorderColor: Theme.of(context).colorScheme.primary,
cursorColor: Theme.of(context).colorScheme.primary,
errorText: widget.bloc.state.timeFormatError
.fold(() => "", (error) => error),
onEditingComplete: (value) =>
widget.bloc.add(DateCellCalendarEvent.setTime(value)),
),
),
return BlocBuilder<DateCellCalendarBloc, DateCellCalendarState>(
builder: (context, state) {
return Column(
children: [
const VSpace(12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: FlowyTextField(
text: state.time ?? "",
focusNode: _focusNode,
submitOnLeave: true,
hintText: state.timeHintText,
errorText: state.timeFormatError,
onSubmitted: (timeString) {
context
.read<DateCellCalendarBloc>()
.add(DateCellCalendarEvent.setTime(timeString));
},
),
),
],
);
},
);
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
}
class _DateTypeOptionButton extends StatelessWidget {