mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
parent
ba8cbe170c
commit
376f2d887d
@ -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(
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -53,10 +53,6 @@ class DateTypeOptionBloc
|
||||
if (timeFormat != null) {
|
||||
typeOption.timeFormat = timeFormat;
|
||||
}
|
||||
|
||||
if (includeTime != null) {
|
||||
typeOption.includeTime = includeTime;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -58,10 +58,8 @@ class TrashCell extends StatelessWidget {
|
||||
|
||||
String dateFormatter($fixnum.Int64 inputTimestamps) {
|
||||
final outputFormat = DateFormat('MM/dd/yyyy hh:mm a');
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(
|
||||
inputTimestamps.toInt() * 1000,
|
||||
isUtc: true,
|
||||
);
|
||||
final date =
|
||||
DateTime.fromMillisecondsSinceEpoch(inputTimestamps.toInt() * 1000);
|
||||
final outputDate = outputFormat.format(date);
|
||||
return outputDate;
|
||||
}
|
||||
|
67
frontend/rust-lib/Cargo.lock
generated
67
frontend/rust-lib/Cargo.lock
generated
@ -469,10 +469,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"chrono-tz-build 0.0.2",
|
||||
"phf 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf9cc2b23599e6d7479755f3594285efb3f74a1bdca7a7374948bc831e23a552"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build 0.1.0",
|
||||
"phf 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.0.2"
|
||||
@ -481,7 +492,18 @@ checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf 0.10.1",
|
||||
"phf_codegen",
|
||||
"phf_codegen 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf 0.11.1",
|
||||
"phf_codegen 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1377,6 +1399,7 @@ dependencies = [
|
||||
"async-stream",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"chrono-tz 0.8.2",
|
||||
"collab",
|
||||
"collab-database",
|
||||
"collab-persistence",
|
||||
@ -3018,6 +3041,15 @@ dependencies = [
|
||||
"phf_shared 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
|
||||
dependencies = [
|
||||
"phf_shared 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.10.0"
|
||||
@ -3028,6 +3060,16 @@ dependencies = [
|
||||
"phf_shared 0.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
|
||||
dependencies = [
|
||||
"phf_generator 0.11.1",
|
||||
"phf_shared 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.8.0"
|
||||
@ -3048,6 +3090,16 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
|
||||
dependencies = [
|
||||
"phf_shared 0.11.1",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.8.0"
|
||||
@ -3081,6 +3133,15 @@ dependencies = [
|
||||
"uncased",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.12"
|
||||
@ -4090,7 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95a665751302f22a03c56721e23094e4dc22b04a80f381e6737a07bf7a7c70c0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"chrono-tz 0.6.1",
|
||||
"globwalk",
|
||||
"humansize",
|
||||
"lazy_static",
|
||||
|
@ -39,6 +39,7 @@ anyhow = "1.0"
|
||||
async-stream = "0.3.4"
|
||||
rayon = "1.6.1"
|
||||
nanoid = "0.4.0"
|
||||
chrono-tz = "0.8.1"
|
||||
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
|
@ -20,6 +20,9 @@ pub struct DateCellDataPB {
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub include_time: bool,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub timezone_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
@ -36,8 +39,8 @@ pub struct DateChangesetPB {
|
||||
#[pb(index = 4, one_of)]
|
||||
pub include_time: Option<bool>,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub is_utc: bool,
|
||||
#[pb(index = 5, one_of)]
|
||||
pub timezone_id: Option<String>,
|
||||
}
|
||||
|
||||
// Date
|
||||
@ -48,9 +51,6 @@ pub struct DateTypeOptionPB {
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub time_format: TimeFormatPB,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub include_time: bool,
|
||||
}
|
||||
|
||||
impl From<DateTypeOption> for DateTypeOptionPB {
|
||||
@ -58,7 +58,6 @@ impl From<DateTypeOption> for DateTypeOptionPB {
|
||||
Self {
|
||||
date_format: data.date_format.into(),
|
||||
time_format: data.time_format.into(),
|
||||
include_time: data.include_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -68,7 +67,6 @@ impl From<DateTypeOptionPB> for DateTypeOption {
|
||||
Self {
|
||||
date_format: data.date_format.into(),
|
||||
time_format: data.time_format.into(),
|
||||
include_time: data.include_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -361,7 +361,7 @@ pub(crate) async fn update_cell_handler(
|
||||
¶ms.field_id,
|
||||
params.cell_changeset.clone(),
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -397,7 +397,7 @@ pub(crate) async fn insert_or_update_select_option_handler(
|
||||
RowId::from(params.row_id),
|
||||
params.items,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -415,7 +415,7 @@ pub(crate) async fn delete_select_option_handler(
|
||||
RowId::from(params.row_id),
|
||||
params.items,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -452,7 +452,7 @@ pub(crate) async fn update_select_option_cell_handler(
|
||||
¶ms.cell_identifier.field_id,
|
||||
changeset,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -467,7 +467,7 @@ pub(crate) async fn update_date_cell_handler(
|
||||
date: data.date,
|
||||
time: data.time,
|
||||
include_time: data.include_time,
|
||||
is_utc: data.is_utc,
|
||||
timezone_id: data.timezone_id,
|
||||
};
|
||||
let database_editor = manager.get_database(&cell_id.view_id).await?;
|
||||
database_editor
|
||||
@ -477,7 +477,7 @@ pub(crate) async fn update_date_cell_handler(
|
||||
&cell_id.field_id,
|
||||
cell_changeset,
|
||||
)
|
||||
.await;
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ use std::fmt::Debug;
|
||||
use collab_database::fields::Field;
|
||||
use collab_database::rows::{get_field_type_from_cell, Cell, Cells};
|
||||
|
||||
use flowy_error::{ErrorCode, FlowyResult};
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::{CellCache, CellProtobufBlob};
|
||||
@ -63,16 +63,14 @@ pub fn apply_cell_data_changeset<C: ToCellChangeset>(
|
||||
cell: Option<Cell>,
|
||||
field: &Field,
|
||||
cell_data_cache: Option<CellCache>,
|
||||
) -> Cell {
|
||||
) -> Result<Cell, FlowyError> {
|
||||
let changeset = changeset.to_cell_changeset_str();
|
||||
let field_type = FieldType::from(field.field_type);
|
||||
match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache)
|
||||
.get_type_option_cell_data_handler(&field_type)
|
||||
{
|
||||
None => Cell::default(),
|
||||
Some(handler) => handler
|
||||
.handle_cell_changeset(changeset, cell, field)
|
||||
.unwrap_or_default(),
|
||||
None => Ok(Cell::default()),
|
||||
Some(handler) => Ok(handler.handle_cell_changeset(changeset, cell, field)?),
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,11 +194,11 @@ pub fn stringify_cell_data(
|
||||
}
|
||||
|
||||
pub fn insert_text_cell(s: String, field: &Field) -> Cell {
|
||||
apply_cell_data_changeset(s, None, field, None)
|
||||
apply_cell_data_changeset(s, None, field, None).unwrap()
|
||||
}
|
||||
|
||||
pub fn insert_number_cell(num: i64, field: &Field) -> Cell {
|
||||
apply_cell_data_changeset(num.to_string(), None, field, None)
|
||||
apply_cell_data_changeset(num.to_string(), None, field, None).unwrap()
|
||||
}
|
||||
|
||||
pub fn insert_url_cell(url: String, field: &Field) -> Cell {
|
||||
@ -214,7 +212,7 @@ pub fn insert_url_cell(url: String, field: &Field) -> Cell {
|
||||
_ => url,
|
||||
};
|
||||
|
||||
apply_cell_data_changeset(url, None, field, None)
|
||||
apply_cell_data_changeset(url, None, field, None).unwrap()
|
||||
}
|
||||
|
||||
pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
|
||||
@ -223,7 +221,7 @@ pub fn insert_checkbox_cell(is_check: bool, field: &Field) -> Cell {
|
||||
} else {
|
||||
UNCHECK.to_string()
|
||||
};
|
||||
apply_cell_data_changeset(s, None, field, None)
|
||||
apply_cell_data_changeset(s, None, field, None).unwrap()
|
||||
}
|
||||
|
||||
pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell {
|
||||
@ -231,22 +229,22 @@ pub fn insert_date_cell(timestamp: i64, field: &Field) -> Cell {
|
||||
date: Some(timestamp.to_string()),
|
||||
time: None,
|
||||
include_time: Some(false),
|
||||
is_utc: true,
|
||||
timezone_id: None,
|
||||
})
|
||||
.unwrap();
|
||||
apply_cell_data_changeset(cell_data, None, field, None)
|
||||
apply_cell_data_changeset(cell_data, None, field, None).unwrap()
|
||||
}
|
||||
|
||||
pub fn insert_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell {
|
||||
let changeset =
|
||||
SelectOptionCellChangeset::from_insert_options(option_ids).to_cell_changeset_str();
|
||||
apply_cell_data_changeset(changeset, None, field, None)
|
||||
apply_cell_data_changeset(changeset, None, field, None).unwrap()
|
||||
}
|
||||
|
||||
pub fn delete_select_option_cell(option_ids: Vec<String>, field: &Field) -> Cell {
|
||||
let changeset =
|
||||
SelectOptionCellChangeset::from_delete_options(option_ids).to_cell_changeset_str();
|
||||
apply_cell_data_changeset(changeset, None, field, None)
|
||||
apply_cell_data_changeset(changeset, None, field, None).unwrap()
|
||||
}
|
||||
|
||||
/// Deserialize the String into cell specific data type.
|
||||
|
@ -466,20 +466,27 @@ impl DatabaseEditor {
|
||||
row_id: RowId,
|
||||
field_id: &str,
|
||||
cell_changeset: T,
|
||||
) -> Option<()>
|
||||
) -> FlowyResult<()>
|
||||
where
|
||||
T: ToCellChangeset,
|
||||
{
|
||||
let (field, cell) = {
|
||||
let database = self.database.lock();
|
||||
let field = match database.fields.get_field(field_id) {
|
||||
Some(field) => Ok(field),
|
||||
None => {
|
||||
let msg = format!("Field with id:{} not found", &field_id);
|
||||
Err(FlowyError::internal().context(msg))
|
||||
},
|
||||
}?;
|
||||
(
|
||||
database.fields.get_field(field_id)?,
|
||||
field,
|
||||
database.get_cell(field_id, &row_id).map(|cell| cell.cell),
|
||||
)
|
||||
};
|
||||
let cell_changeset = cell_changeset.to_cell_changeset_str();
|
||||
let new_cell =
|
||||
apply_cell_data_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone()));
|
||||
apply_cell_data_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone()))?;
|
||||
self.update_cell(view_id, row_id, field_id, new_cell).await
|
||||
}
|
||||
|
||||
@ -489,7 +496,7 @@ impl DatabaseEditor {
|
||||
row_id: RowId,
|
||||
field_id: &str,
|
||||
new_cell: Cell,
|
||||
) -> Option<()> {
|
||||
) -> FlowyResult<()> {
|
||||
let old_row = { self.database.lock().get_row(&row_id) };
|
||||
self.database.lock().update_row(&row_id, |row_update| {
|
||||
row_update.update_cells(|cell_update| {
|
||||
@ -516,7 +523,7 @@ impl DatabaseEditor {
|
||||
field_id: field_id.to_string(),
|
||||
}])
|
||||
.await;
|
||||
None
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_select_option(
|
||||
@ -536,9 +543,15 @@ impl DatabaseEditor {
|
||||
field_id: &str,
|
||||
row_id: RowId,
|
||||
options: Vec<SelectOptionPB>,
|
||||
) -> Option<()> {
|
||||
let field = self.database.lock().fields.get_field(field_id)?;
|
||||
let mut type_option = select_type_option_from_field(&field).ok()?;
|
||||
) -> FlowyResult<()> {
|
||||
let field = match self.database.lock().fields.get_field(field_id) {
|
||||
Some(field) => Ok(field),
|
||||
None => {
|
||||
let msg = format!("Field with id:{} not found", &field_id);
|
||||
Err(FlowyError::internal().context(msg))
|
||||
},
|
||||
}?;
|
||||
let mut type_option = select_type_option_from_field(&field)?;
|
||||
let cell_changeset = SelectOptionCellChangeset {
|
||||
insert_option_ids: options.iter().map(|option| option.id.clone()).collect(),
|
||||
..Default::default()
|
||||
@ -557,8 +570,8 @@ impl DatabaseEditor {
|
||||
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, cell_changeset)
|
||||
.await;
|
||||
None
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_select_options(
|
||||
@ -567,9 +580,15 @@ impl DatabaseEditor {
|
||||
field_id: &str,
|
||||
row_id: RowId,
|
||||
options: Vec<SelectOptionPB>,
|
||||
) -> Option<()> {
|
||||
let field = self.database.lock().fields.get_field(field_id)?;
|
||||
let mut type_option = select_type_option_from_field(&field).ok()?;
|
||||
) -> FlowyResult<()> {
|
||||
let field = match self.database.lock().fields.get_field(field_id) {
|
||||
Some(field) => Ok(field),
|
||||
None => {
|
||||
let msg = format!("Field with id:{} not found", &field_id);
|
||||
Err(FlowyError::internal().context(msg))
|
||||
},
|
||||
}?;
|
||||
let mut type_option = select_type_option_from_field(&field)?;
|
||||
let cell_changeset = SelectOptionCellChangeset {
|
||||
delete_option_ids: options.iter().map(|option| option.id.clone()).collect(),
|
||||
..Default::default()
|
||||
@ -588,8 +607,8 @@ impl DatabaseEditor {
|
||||
|
||||
self
|
||||
.update_cell_with_changeset(view_id, row_id, field_id, cell_changeset)
|
||||
.await;
|
||||
None
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_select_options(&self, row_id: RowId, field_id: &str) -> SelectOptionCellDataPB {
|
||||
|
@ -20,19 +20,74 @@ mod tests {
|
||||
type_option.date_format = date_format;
|
||||
match date_format {
|
||||
DateFormat::Friendly => {
|
||||
assert_date(&type_option, 1647251762, None, "Mar 14,2022", false, &field);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1647251762".to_owned()),
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"Mar 14, 2022",
|
||||
);
|
||||
},
|
||||
DateFormat::US => {
|
||||
assert_date(&type_option, 1647251762, None, "2022/03/14", false, &field);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1647251762".to_owned()),
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"2022/03/14",
|
||||
);
|
||||
},
|
||||
DateFormat::ISO => {
|
||||
assert_date(&type_option, 1647251762, None, "2022-03-14", false, &field);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1647251762".to_owned()),
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"2022-03-14",
|
||||
);
|
||||
},
|
||||
DateFormat::Local => {
|
||||
assert_date(&type_option, 1647251762, None, "03/14/2022", false, &field);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1647251762".to_owned()),
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"03/14/2022",
|
||||
);
|
||||
},
|
||||
DateFormat::DayMonthYear => {
|
||||
assert_date(&type_option, 1647251762, None, "14/03/2022", false, &field);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1647251762".to_owned()),
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"14/03/2022",
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -41,8 +96,7 @@ mod tests {
|
||||
#[test]
|
||||
fn date_type_option_different_time_format_test() {
|
||||
let mut type_option = DateTypeOption::default();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_rev = FieldBuilder::from_field_type(field_type).build();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
for time_format in TimeFormat::iter() {
|
||||
type_option.time_format = time_format;
|
||||
@ -50,53 +104,77 @@ mod tests {
|
||||
TimeFormat::TwentyFourHour => {
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: None,
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Etc/UTC".to_owned()),
|
||||
},
|
||||
None,
|
||||
"May 27,2022 00:00",
|
||||
true,
|
||||
&field_rev,
|
||||
"May 27, 2022 00:00",
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("9:00".to_owned()),
|
||||
"May 27,2022 09:00",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("9:00".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Etc/UTC".to_owned()),
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 09:00",
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("23:00".to_owned()),
|
||||
"May 27,2022 23:00",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("23:00".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Etc/UTC".to_owned()),
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 23:00",
|
||||
);
|
||||
},
|
||||
TimeFormat::TwelveHour => {
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: None,
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Etc/UTC".to_owned()),
|
||||
},
|
||||
None,
|
||||
"May 27,2022 12:00 AM",
|
||||
true,
|
||||
&field_rev,
|
||||
"May 27, 2022 12:00 AM",
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("9:00 AM".to_owned()),
|
||||
"May 27,2022 09:00 AM",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("9:00 AM".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 09:00 AM",
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("11:23 pm".to_owned()),
|
||||
"May 27,2022 11:23 PM",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("11:23 pm".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Etc/UTC".to_owned()),
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 11:23 PM",
|
||||
);
|
||||
},
|
||||
}
|
||||
@ -107,38 +185,58 @@ mod tests {
|
||||
fn date_type_option_invalid_date_str_test() {
|
||||
let type_option = DateTypeOption::default();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_rev = FieldBuilder::from_field_type(field_type).build();
|
||||
assert_date(&type_option, "abc", None, "", false, &field_rev);
|
||||
let field = FieldBuilder::from_field_type(field_type).build();
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("abc".to_owned()),
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn date_type_option_invalid_include_time_str_test() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("1:".to_owned()),
|
||||
"May 27,2022 01:00",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("1:".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 01:00",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn date_type_option_empty_include_time_str_test() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("".to_owned()),
|
||||
"May 27,2022 00:00",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 01:00",
|
||||
);
|
||||
}
|
||||
|
||||
@ -146,14 +244,18 @@ mod tests {
|
||||
fn date_type_midnight_include_time_str_test() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_rev = FieldBuilder::from_field_type(field_type).build();
|
||||
let field = FieldBuilder::from_field_type(field_type).build();
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("00:00".to_owned()),
|
||||
"May 27,2022 00:00",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("00:00".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 00:00",
|
||||
);
|
||||
}
|
||||
|
||||
@ -162,15 +264,18 @@ mod tests {
|
||||
#[should_panic]
|
||||
fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("1:00 am".to_owned()),
|
||||
"May 27,2022 01:00 AM",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("1:00 am".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 01:00 AM",
|
||||
);
|
||||
}
|
||||
|
||||
@ -180,15 +285,19 @@ mod tests {
|
||||
fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() {
|
||||
let mut type_option = DateTypeOption::new();
|
||||
type_option.time_format = TimeFormat::TwelveHour;
|
||||
let field_rev = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("20:00".to_owned()),
|
||||
"May 27,2022 08:00 PM",
|
||||
true,
|
||||
&field_rev,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1653609600".to_owned()),
|
||||
time: Some("20:00".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: None,
|
||||
},
|
||||
None,
|
||||
"May 27, 2022 08:00 PM",
|
||||
);
|
||||
}
|
||||
|
||||
@ -218,25 +327,170 @@ mod tests {
|
||||
assert_eq!(china_local_time, "03/14/2022 05:56 PM");
|
||||
}
|
||||
|
||||
fn assert_date<T: ToString>(
|
||||
/// The time component shouldn't remain the same since the timestamp is
|
||||
/// completely overwritten. To achieve the desired result, also pass in the
|
||||
/// time string along with the new timestamp.
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn update_date_keep_time() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
let old_cell_data = initialize_date_cell(
|
||||
&type_option,
|
||||
DateCellChangeset {
|
||||
date: Some("1700006400".to_owned()),
|
||||
time: Some("08:00".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Etc/UTC".to_owned()),
|
||||
},
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1701302400".to_owned()),
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
Some(old_cell_data),
|
||||
"Nov 30, 2023 08:00",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_time_keep_date() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
let old_cell_data = initialize_date_cell(
|
||||
&type_option,
|
||||
DateCellChangeset {
|
||||
date: Some("1700006400".to_owned()),
|
||||
time: Some("08:00".to_owned()),
|
||||
include_time: Some(true),
|
||||
timezone_id: None,
|
||||
},
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: None,
|
||||
time: Some("14:00".to_owned()),
|
||||
include_time: None,
|
||||
timezone_id: None,
|
||||
},
|
||||
Some(old_cell_data),
|
||||
"Nov 15, 2023 14:00",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timezone_no_daylight_saving_time() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1672963200".to_owned()),
|
||||
time: None,
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Asia/Tokyo".to_owned()),
|
||||
},
|
||||
None,
|
||||
"Jan 06, 2023 09:00",
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1685404800".to_owned()),
|
||||
time: None,
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Asia/Tokyo".to_owned()),
|
||||
},
|
||||
None,
|
||||
"May 30, 2023 09:00",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timezone_with_daylight_saving_time() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1672963200".to_owned()),
|
||||
time: None,
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Europe/Paris".to_owned()),
|
||||
},
|
||||
None,
|
||||
"Jan 06, 2023 01:00",
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: Some("1685404800".to_owned()),
|
||||
time: None,
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Europe/Paris".to_owned()),
|
||||
},
|
||||
None,
|
||||
"May 30, 2023 02:00",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn change_timezone() {
|
||||
let type_option = DateTypeOption::new();
|
||||
let field = FieldBuilder::from_field_type(FieldType::DateTime).build();
|
||||
|
||||
let old_cell_data = initialize_date_cell(
|
||||
&type_option,
|
||||
DateCellChangeset {
|
||||
date: Some("1672963200".to_owned()),
|
||||
time: None,
|
||||
include_time: Some(true),
|
||||
timezone_id: Some("Asia/China".to_owned()),
|
||||
},
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
&field,
|
||||
DateCellChangeset {
|
||||
date: None,
|
||||
time: None,
|
||||
include_time: None,
|
||||
timezone_id: Some("America/Los_Angeles".to_owned()),
|
||||
},
|
||||
Some(old_cell_data),
|
||||
"Jan 05, 2023 16:00",
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_date(
|
||||
type_option: &DateTypeOption,
|
||||
timestamp: T,
|
||||
include_time_str: Option<String>,
|
||||
expected_str: &str,
|
||||
include_time: bool,
|
||||
field: &Field,
|
||||
changeset: DateCellChangeset,
|
||||
old_cell_data: Option<Cell>,
|
||||
expected_str: &str,
|
||||
) {
|
||||
let changeset = DateCellChangeset {
|
||||
date: Some(timestamp.to_string()),
|
||||
time: include_time_str,
|
||||
is_utc: false,
|
||||
include_time: Some(include_time),
|
||||
};
|
||||
let (cell, _) = type_option.apply_changeset(changeset, None).unwrap();
|
||||
let (cell, cell_data) = type_option
|
||||
.apply_changeset(changeset, old_cell_data)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decode_cell_data(&cell, type_option, include_time, field),
|
||||
expected_str.to_owned(),
|
||||
decode_cell_data(&cell, type_option, cell_data.include_time, field),
|
||||
expected_str,
|
||||
);
|
||||
}
|
||||
|
||||
@ -258,4 +512,9 @@ mod tests {
|
||||
decoded_data.date
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell {
|
||||
let (cell, _) = type_option.apply_changeset(changeset, None).unwrap();
|
||||
cell
|
||||
}
|
||||
}
|
||||
|
@ -5,20 +5,21 @@ use crate::services::field::{
|
||||
TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform,
|
||||
};
|
||||
use chrono::format::strftime::StrftimeItems;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::{DateTime, Local, NaiveDateTime, NaiveTime, Offset, TimeZone};
|
||||
use chrono_tz::Tz;
|
||||
use collab::core::any_map::AnyMapExtension;
|
||||
use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder};
|
||||
use collab_database::rows::Cell;
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
use std::str::FromStr;
|
||||
|
||||
// Date
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct DateTypeOption {
|
||||
pub date_format: DateFormat,
|
||||
pub time_format: TimeFormat,
|
||||
pub include_time: bool,
|
||||
}
|
||||
|
||||
impl TypeOption for DateTypeOption {
|
||||
@ -30,7 +31,6 @@ impl TypeOption for DateTypeOption {
|
||||
|
||||
impl From<TypeOptionData> for DateTypeOption {
|
||||
fn from(data: TypeOptionData) -> Self {
|
||||
let include_time = data.get_bool_value("include_time").unwrap_or(false);
|
||||
let date_format = data
|
||||
.get_i64_value("data_format")
|
||||
.map(DateFormat::from)
|
||||
@ -42,7 +42,6 @@ impl From<TypeOptionData> for DateTypeOption {
|
||||
Self {
|
||||
date_format,
|
||||
time_format,
|
||||
include_time,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,7 +51,6 @@ impl From<DateTypeOption> for TypeOptionData {
|
||||
TypeOptionDataBuilder::new()
|
||||
.insert_i64_value("data_format", data.date_format.value())
|
||||
.insert_i64_value("time_format", data.time_format.value())
|
||||
.insert_bool_value("include_time", data.include_time)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@ -79,23 +77,26 @@ impl DateTypeOption {
|
||||
fn today_desc_from_timestamp(&self, cell_data: DateCellData) -> DateCellDataPB {
|
||||
let timestamp = cell_data.timestamp.unwrap_or_default();
|
||||
let include_time = cell_data.include_time;
|
||||
let timezone_id = cell_data.timezone_id;
|
||||
|
||||
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0);
|
||||
if naive.is_none() {
|
||||
return DateCellDataPB::default();
|
||||
}
|
||||
let naive = naive.unwrap();
|
||||
if timestamp == 0 {
|
||||
return DateCellDataPB::default();
|
||||
}
|
||||
let fmt = self.date_format.format_str();
|
||||
let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt)));
|
||||
let (date, time) = match cell_data.timestamp {
|
||||
Some(timestamp) => {
|
||||
let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap();
|
||||
let offset = match Tz::from_str(&timezone_id) {
|
||||
Ok(timezone) => timezone.offset_from_utc_datetime(&naive).fix(),
|
||||
Err(_) => Local::now().offset().clone(),
|
||||
};
|
||||
|
||||
let time = if include_time {
|
||||
let fmt = self.time_format.format_str();
|
||||
format!("{}", naive.format_with_items(StrftimeItems::new(fmt)))
|
||||
} else {
|
||||
"".to_string()
|
||||
let date_time = DateTime::<Local>::from_utc(naive, offset);
|
||||
|
||||
let fmt = self.date_format.format_str();
|
||||
let date = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt)));
|
||||
let fmt = self.time_format.format_str();
|
||||
let time = format!("{}", date_time.format_with_items(StrftimeItems::new(fmt)));
|
||||
|
||||
(date, time)
|
||||
},
|
||||
None => ("".to_owned(), "".to_owned()),
|
||||
};
|
||||
|
||||
DateCellDataPB {
|
||||
@ -103,32 +104,9 @@ impl DateTypeOption {
|
||||
time,
|
||||
include_time,
|
||||
timestamp,
|
||||
timezone_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp_from_utc_with_time(
|
||||
&self,
|
||||
naive_date: &NaiveDateTime,
|
||||
time_str: &Option<String>,
|
||||
) -> FlowyResult<i64> {
|
||||
if let Some(time_str) = time_str.as_ref() {
|
||||
if !time_str.is_empty() {
|
||||
let naive_time = chrono::NaiveTime::parse_from_str(time_str, self.time_format.format_str());
|
||||
|
||||
match naive_time {
|
||||
Ok(naive_time) => {
|
||||
return Ok(naive_date.date().and_time(naive_time).timestamp());
|
||||
},
|
||||
Err(_e) => {
|
||||
let msg = format!("Parse {} failed", time_str);
|
||||
return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(naive_date.timestamp())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeOptionTransform for DateTypeOption {}
|
||||
@ -167,39 +145,129 @@ impl CellDataChangeset for DateTypeOption {
|
||||
changeset: <Self as TypeOption>::CellChangeset,
|
||||
cell: Option<Cell>,
|
||||
) -> FlowyResult<(Cell, <Self as TypeOption>::CellData)> {
|
||||
let (timestamp, include_time) = match cell {
|
||||
None => (None, false),
|
||||
Some(cell) => {
|
||||
let cell_data = DateCellData::from(&cell);
|
||||
(cell_data.timestamp, cell_data.include_time)
|
||||
// old date cell data
|
||||
let (timestamp, include_time, timezone_id) = match cell {
|
||||
None => (None, false, "".to_owned()),
|
||||
Some(type_cell_data) => {
|
||||
let cell_data = DateCellData::from(&type_cell_data);
|
||||
(
|
||||
cell_data.timestamp,
|
||||
cell_data.include_time,
|
||||
cell_data.timezone_id,
|
||||
)
|
||||
},
|
||||
};
|
||||
|
||||
// update include_time and timezone_id if present
|
||||
let include_time = match changeset.include_time {
|
||||
None => include_time,
|
||||
Some(include_time) => include_time,
|
||||
};
|
||||
let timestamp = match changeset.date_timestamp() {
|
||||
None => timestamp,
|
||||
Some(date_timestamp) => match (include_time, changeset.time) {
|
||||
(true, Some(time)) => {
|
||||
let time = Some(time.trim().to_uppercase());
|
||||
let naive = NaiveDateTime::from_timestamp_opt(date_timestamp, 0);
|
||||
if let Some(naive) = naive {
|
||||
Some(self.timestamp_from_utc_with_time(&naive, &time)?)
|
||||
} else {
|
||||
Some(date_timestamp)
|
||||
let timezone_id = match changeset.timezone_id {
|
||||
None => timezone_id,
|
||||
Some(ref timezone_id) => timezone_id.to_owned(),
|
||||
};
|
||||
|
||||
let previous_datetime = match timestamp {
|
||||
Some(timestamp) => NaiveDateTime::from_timestamp_opt(timestamp, 0),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let new_date_timestamp = changeset.date_timestamp();
|
||||
|
||||
// parse the time string, which would be in the local timezone
|
||||
let parsed_time = match (include_time, changeset.time) {
|
||||
(true, Some(time_str)) => {
|
||||
let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
|
||||
match result {
|
||||
Ok(time) => Ok(Some(time)),
|
||||
Err(_e) => {
|
||||
let msg = format!("Parse {} failed", time_str);
|
||||
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg))
|
||||
},
|
||||
}
|
||||
},
|
||||
_ => Ok(None),
|
||||
}?;
|
||||
|
||||
// Calculate the new timestamp, while considering the timezone. If a new
|
||||
// timestamp is included in the changeset without an accompanying time
|
||||
// string, the new timestamp will simply overwrite the old one. Meaning,
|
||||
// in order to change the day without time in the frontend, the time string
|
||||
// must also be passed.
|
||||
let timestamp = match Tz::from_str(&timezone_id) {
|
||||
Ok(timezone) => match parsed_time {
|
||||
Some(time) => {
|
||||
// a valid time is provided, so we replace the time component of old
|
||||
// (or new timestamp if provided) with this.
|
||||
let local_date = match new_date_timestamp {
|
||||
Some(timestamp) => Some(
|
||||
timezone
|
||||
.from_utc_datetime(&NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap())
|
||||
.date_naive(),
|
||||
),
|
||||
None => match previous_datetime {
|
||||
Some(datetime) => Some(timezone.from_utc_datetime(&datetime).date_naive()),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
match local_date {
|
||||
Some(date) => {
|
||||
let local_datetime_naive = NaiveDateTime::new(date, time);
|
||||
let local_datetime = timezone.from_local_datetime(&local_datetime_naive).unwrap();
|
||||
|
||||
Some(local_datetime.timestamp())
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
},
|
||||
_ => Some(date_timestamp),
|
||||
None => match new_date_timestamp {
|
||||
// no valid time, return old timestamp or new one if provided
|
||||
Some(timestamp) => Some(timestamp),
|
||||
None => timestamp,
|
||||
},
|
||||
},
|
||||
Err(_) => match parsed_time {
|
||||
// same logic as above, but using local time instead of timezone
|
||||
Some(time) => {
|
||||
let offset = Local::now().offset().clone();
|
||||
|
||||
let local_date = match new_date_timestamp {
|
||||
Some(timestamp) => Some(
|
||||
offset
|
||||
.from_utc_datetime(&NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap())
|
||||
.date_naive(),
|
||||
),
|
||||
None => match previous_datetime {
|
||||
Some(datetime) => Some(offset.from_utc_datetime(&datetime).date_naive()),
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
match local_date {
|
||||
Some(date) => {
|
||||
let local_datetime = NaiveDateTime::new(date, time);
|
||||
let datetime = offset.from_local_datetime(&local_datetime).unwrap();
|
||||
|
||||
Some(datetime.timestamp())
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
},
|
||||
None => match new_date_timestamp {
|
||||
Some(timestamp) => Some(timestamp),
|
||||
None => timestamp,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let date_cell_data = DateCellData {
|
||||
timestamp,
|
||||
include_time,
|
||||
timezone_id,
|
||||
};
|
||||
Ok((date_cell_data.clone().into(), date_cell_data))
|
||||
Ok((Cell::from(date_cell_data.clone()), date_cell_data))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ pub struct DateCellChangeset {
|
||||
pub date: Option<String>,
|
||||
pub time: Option<String>,
|
||||
pub include_time: Option<bool>,
|
||||
pub is_utc: bool,
|
||||
pub timezone_id: Option<String>,
|
||||
}
|
||||
|
||||
impl DateCellChangeset {
|
||||
@ -57,27 +57,37 @@ impl ToCellChangeset for DateCellChangeset {
|
||||
pub struct DateCellData {
|
||||
pub timestamp: Option<i64>,
|
||||
pub include_time: bool,
|
||||
pub timezone_id: String,
|
||||
}
|
||||
|
||||
impl From<&Cell> for DateCellData {
|
||||
fn from(cell: &Cell) -> Self {
|
||||
let timestamp = cell
|
||||
.get_str_value(CELL_DATE)
|
||||
.map(|data| data.parse::<i64>().unwrap_or_default());
|
||||
.map(|data| data.parse::<i64>().ok())
|
||||
.flatten();
|
||||
|
||||
let include_time = cell.get_bool_value("include_time").unwrap_or_default();
|
||||
let timezone_id = cell.get_str_value("timezone_id").unwrap_or_default();
|
||||
|
||||
Self {
|
||||
timestamp,
|
||||
include_time,
|
||||
timezone_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateCellData> for Cell {
|
||||
fn from(data: DateCellData) -> Self {
|
||||
let timestamp_string = match data.timestamp {
|
||||
Some(timestamp) => timestamp.to_string(),
|
||||
None => "".to_owned(),
|
||||
};
|
||||
new_cell_builder(FieldType::DateTime)
|
||||
.insert_str_value(CELL_DATE, data.timestamp.unwrap_or_default().to_string())
|
||||
.insert_str_value(CELL_DATE, timestamp_string)
|
||||
.insert_bool_value("include_time", data.include_time)
|
||||
.insert_str_value("timezone_id", data.timezone_id)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@ -105,6 +115,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
|
||||
Ok(DateCellData {
|
||||
timestamp: Some(value),
|
||||
include_time: false,
|
||||
timezone_id: "".to_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -121,6 +132,7 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
|
||||
{
|
||||
let mut timestamp: Option<i64> = None;
|
||||
let mut include_time: Option<bool> = None;
|
||||
let mut timezone_id: Option<String> = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
@ -130,15 +142,20 @@ impl<'de> serde::Deserialize<'de> for DateCellData {
|
||||
"include_time" => {
|
||||
include_time = map.next_value()?;
|
||||
},
|
||||
"timezone_id" => {
|
||||
timezone_id = map.next_value()?;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
let include_time = include_time.unwrap_or(false);
|
||||
let include_time = include_time.unwrap_or_default();
|
||||
let timezone_id = timezone_id.unwrap_or_default();
|
||||
|
||||
Ok(DateCellData {
|
||||
timestamp,
|
||||
include_time,
|
||||
timezone_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -203,7 +220,7 @@ impl DateFormat {
|
||||
DateFormat::Local => "%m/%d/%Y",
|
||||
DateFormat::US => "%Y/%m/%d",
|
||||
DateFormat::ISO => "%Y-%m-%d",
|
||||
DateFormat::Friendly => "%b %d,%Y",
|
||||
DateFormat::Friendly => "%b %d, %Y",
|
||||
DateFormat::DayMonthYear => "%d/%m/%Y",
|
||||
}
|
||||
}
|
||||
|
@ -21,17 +21,18 @@ mod tests {
|
||||
&field_type,
|
||||
&field
|
||||
),
|
||||
"Mar 14,2022"
|
||||
"Mar 14, 2022"
|
||||
);
|
||||
|
||||
let data = DateCellData {
|
||||
timestamp: Some(1647251762),
|
||||
include_time: true,
|
||||
timezone_id: "".to_owned(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
stringify_cell_data(&data.into(), &FieldType::RichText, &field_type, &field),
|
||||
"Mar 14,2022"
|
||||
"Mar 14, 2022"
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -257,8 +257,8 @@ impl TestRowBuilder {
|
||||
let value = serde_json::to_string(&DateCellChangeset {
|
||||
date: Some(data.to_string()),
|
||||
time: None,
|
||||
is_utc: true,
|
||||
include_time: Some(false),
|
||||
timezone_id: None,
|
||||
})
|
||||
.unwrap();
|
||||
let date_field = self.field_with_type(&FieldType::DateTime);
|
||||
|
@ -48,8 +48,8 @@ pub fn make_date_cell_string(s: &str) -> String {
|
||||
serde_json::to_string(&DateCellChangeset {
|
||||
date: Some(s.to_string()),
|
||||
time: None,
|
||||
is_utc: true,
|
||||
include_time: Some(false),
|
||||
timezone_id: None,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
@ -43,7 +43,6 @@ pub fn make_test_board() -> DatabaseData {
|
||||
let date_type_option = DateTypeOption {
|
||||
date_format: DateFormat::US,
|
||||
time_format: TimeFormat::TwentyFourHour,
|
||||
include_time: false,
|
||||
};
|
||||
let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
|
||||
.name("Time")
|
||||
|
@ -44,7 +44,6 @@ pub fn make_test_grid() -> DatabaseData {
|
||||
let date_type_option = DateTypeOption {
|
||||
date_format: DateFormat::US,
|
||||
time_format: TimeFormat::TwentyFourHour,
|
||||
include_time: false,
|
||||
};
|
||||
let date_field = FieldBuilder::new(field_type.clone(), date_type_option)
|
||||
.name("Time")
|
||||
|
Loading…
Reference in New Issue
Block a user