mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: include time per cell (#1901)
* style: autoformat * chore: add include_time to cell data * chore: remove include_time from date field type options * chore: fix tests * chore: custom deserializer for date cell data * chore: add more tests * chore: simplify date calculation logic * chore: move include time to per-cell setting in UI * test: add another text str test * chore: adapt changes from upstream
This commit is contained in:
parent
5e8f6a53a0
commit
77ff2e987a
@ -13,7 +13,7 @@ typedef SelectOptionCellController
|
||||
= CellController<SelectOptionCellDataPB, String>;
|
||||
typedef ChecklistCellController
|
||||
= CellController<SelectOptionCellDataPB, String>;
|
||||
typedef DateCellController = CellController<DateCellDataPB, CalendarData>;
|
||||
typedef DateCellController = CellController<DateCellDataPB, DateCellData>;
|
||||
typedef URLCellController = CellController<URLCellDataPB, String>;
|
||||
|
||||
class CellControllerBuilder {
|
||||
|
@ -27,24 +27,28 @@ class TextCellDataPersistence implements CellDataPersistence<String> {
|
||||
}
|
||||
|
||||
@freezed
|
||||
class CalendarData with _$CalendarData {
|
||||
const factory CalendarData({required DateTime date, String? time}) =
|
||||
_CalendarData;
|
||||
class DateCellData with _$DateCellData {
|
||||
const factory DateCellData({
|
||||
required DateTime date,
|
||||
String? time,
|
||||
required bool includeTime,
|
||||
}) = _DateCellData;
|
||||
}
|
||||
|
||||
class DateCellDataPersistence implements CellDataPersistence<CalendarData> {
|
||||
class DateCellDataPersistence implements CellDataPersistence<DateCellData> {
|
||||
final CellIdentifier cellId;
|
||||
DateCellDataPersistence({
|
||||
required this.cellId,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<Option<FlowyError>> save(CalendarData data) {
|
||||
Future<Option<FlowyError>> save(DateCellData data) {
|
||||
var payload = DateChangesetPB.create()..cellPath = _makeCellPath(cellId);
|
||||
|
||||
final date = (data.date.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
payload.date = date;
|
||||
payload.isUtc = data.date.isUtc;
|
||||
payload.includeTime = data.includeTime;
|
||||
|
||||
if (data.time != null) {
|
||||
payload.time = data.time!;
|
||||
|
@ -15,7 +15,7 @@ import 'package:table_calendar/table_calendar.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:fixnum/fixnum.dart' as $fixnum;
|
||||
|
||||
part 'date_cal_bloc.freezed.dart';
|
||||
|
||||
class DateCellCalendarBloc
|
||||
@ -42,13 +42,13 @@ class DateCellCalendarBloc
|
||||
emit(state.copyWith(focusedDay: focusedDay));
|
||||
},
|
||||
didReceiveCellUpdate: (DateCellDataPB? cellData) {
|
||||
final calData = calDataFromCellData(cellData);
|
||||
final time = calData.foldRight(
|
||||
final dateCellData = calDataFromCellData(cellData);
|
||||
final time = dateCellData.foldRight(
|
||||
"", (dateData, previous) => dateData.time ?? '');
|
||||
emit(state.copyWith(calData: calData, time: time));
|
||||
emit(state.copyWith(dateCellData: dateCellData, time: time));
|
||||
},
|
||||
setIncludeTime: (includeTime) async {
|
||||
await _updateTypeOption(emit, includeTime: includeTime);
|
||||
await _updateDateData(emit, includeTime: includeTime);
|
||||
},
|
||||
setDateFormat: (dateFormat) async {
|
||||
await _updateTypeOption(emit, dateFormat: dateFormat);
|
||||
@ -57,14 +57,14 @@ class DateCellCalendarBloc
|
||||
await _updateTypeOption(emit, timeFormat: timeFormat);
|
||||
},
|
||||
setTime: (time) async {
|
||||
if (state.calData.isSome()) {
|
||||
if (state.dateCellData.isSome()) {
|
||||
await _updateDateData(emit, time: time);
|
||||
}
|
||||
},
|
||||
didUpdateCalData:
|
||||
(Option<CalendarData> data, Option<String> timeFormatError) {
|
||||
(Option<DateCellData> data, Option<String> timeFormatError) {
|
||||
emit(state.copyWith(
|
||||
calData: data, timeFormatError: timeFormatError));
|
||||
dateCellData: data, timeFormatError: timeFormatError));
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -72,9 +72,13 @@ class DateCellCalendarBloc
|
||||
}
|
||||
|
||||
Future<void> _updateDateData(Emitter<DateCellCalendarState> emit,
|
||||
{DateTime? date, String? time}) {
|
||||
final CalendarData newDateData = state.calData.fold(
|
||||
() => CalendarData(date: date ?? DateTime.now(), time: time),
|
||||
{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)) {
|
||||
@ -84,6 +88,11 @@ class DateCellCalendarBloc
|
||||
if (newDateData.time != time) {
|
||||
newDateData = newDateData.copyWith(time: time);
|
||||
}
|
||||
|
||||
if (includeTime != null && newDateData.includeTime != includeTime) {
|
||||
newDateData = newDateData.copyWith(includeTime: includeTime);
|
||||
}
|
||||
|
||||
return newDateData;
|
||||
},
|
||||
);
|
||||
@ -92,15 +101,16 @@ class DateCellCalendarBloc
|
||||
}
|
||||
|
||||
Future<void> _saveDateData(
|
||||
Emitter<DateCellCalendarState> emit, CalendarData newCalData) async {
|
||||
if (state.calData == Some(newCalData)) {
|
||||
Emitter<DateCellCalendarState> emit, DateCellData newCalData) async {
|
||||
if (state.dateCellData == Some(newCalData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateCalData(
|
||||
Option<CalendarData> calData, Option<String> timeFormatError) {
|
||||
Option<DateCellData> dateCellData, Option<String> timeFormatError) {
|
||||
if (!isClosed) {
|
||||
add(DateCellCalendarEvent.didUpdateCalData(calData, timeFormatError));
|
||||
add(DateCellCalendarEvent.didUpdateCalData(
|
||||
dateCellData, timeFormatError));
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +120,7 @@ class DateCellCalendarBloc
|
||||
(err) {
|
||||
switch (ErrorCode.valueOf(err.code)!) {
|
||||
case ErrorCode.InvalidDateTimeFormat:
|
||||
updateCalData(state.calData, Some(timeFormatPrompt(err)));
|
||||
updateCalData(state.dateCellData, Some(timeFormatPrompt(err)));
|
||||
break;
|
||||
default:
|
||||
Log.error(err);
|
||||
@ -159,7 +169,6 @@ class DateCellCalendarBloc
|
||||
Emitter<DateCellCalendarState> emit, {
|
||||
DateFormat? dateFormat,
|
||||
TimeFormat? timeFormat,
|
||||
bool? includeTime,
|
||||
}) async {
|
||||
state.dateTypeOptionPB.freeze();
|
||||
final newDateTypeOption = state.dateTypeOptionPB.rebuild((typeOption) {
|
||||
@ -170,10 +179,6 @@ class DateCellCalendarBloc
|
||||
if (timeFormat != null) {
|
||||
typeOption.timeFormat = timeFormat;
|
||||
}
|
||||
|
||||
if (includeTime != null) {
|
||||
typeOption.includeTime = includeTime;
|
||||
}
|
||||
});
|
||||
|
||||
final result = await FieldBackendService.updateFieldTypeOption(
|
||||
@ -208,7 +213,7 @@ class DateCellCalendarEvent with _$DateCellCalendarEvent {
|
||||
const factory DateCellCalendarEvent.didReceiveCellUpdate(
|
||||
DateCellDataPB? data) = _DidReceiveCellUpdate;
|
||||
const factory DateCellCalendarEvent.didUpdateCalData(
|
||||
Option<CalendarData> data, Option<String> timeFormatError) =
|
||||
Option<DateCellData> data, Option<String> timeFormatError) =
|
||||
_DidUpdateCalData;
|
||||
}
|
||||
|
||||
@ -219,7 +224,7 @@ class DateCellCalendarState with _$DateCellCalendarState {
|
||||
required CalendarFormat format,
|
||||
required DateTime focusedDay,
|
||||
required Option<String> timeFormatError,
|
||||
required Option<CalendarData> calData,
|
||||
required Option<DateCellData> dateCellData,
|
||||
required String? time,
|
||||
required String timeHintText,
|
||||
}) = _DateCellCalendarState;
|
||||
@ -228,15 +233,15 @@ class DateCellCalendarState with _$DateCellCalendarState {
|
||||
DateTypeOptionPB dateTypeOptionPB,
|
||||
DateCellDataPB? cellData,
|
||||
) {
|
||||
Option<CalendarData> calData = calDataFromCellData(cellData);
|
||||
Option<DateCellData> dateCellData = calDataFromCellData(cellData);
|
||||
final time =
|
||||
calData.foldRight("", (dateData, previous) => dateData.time ?? '');
|
||||
dateCellData.foldRight("", (dateData, previous) => dateData.time ?? '');
|
||||
return DateCellCalendarState(
|
||||
dateTypeOptionPB: dateTypeOptionPB,
|
||||
format: CalendarFormat.month,
|
||||
focusedDay: DateTime.now(),
|
||||
time: time,
|
||||
calData: calData,
|
||||
dateCellData: dateCellData,
|
||||
timeFormatError: none(),
|
||||
timeHintText: _timeHintText(dateTypeOptionPB),
|
||||
);
|
||||
@ -249,30 +254,30 @@ String _timeHintText(DateTypeOptionPB typeOption) {
|
||||
return LocaleKeys.document_date_timeHintTextInTwelveHour.tr();
|
||||
case TimeFormat.TwentyFourHour:
|
||||
return LocaleKeys.document_date_timeHintTextInTwentyFourHour.tr();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
Option<CalendarData> calDataFromCellData(DateCellDataPB? cellData) {
|
||||
Option<DateCellData> calDataFromCellData(DateCellDataPB? cellData) {
|
||||
String? time = timeFromCellData(cellData);
|
||||
Option<CalendarData> calData = none();
|
||||
Option<DateCellData> dateData = none();
|
||||
if (cellData != null) {
|
||||
final timestamp = cellData.timestamp * 1000;
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt());
|
||||
calData = Some(CalendarData(date: date, time: time));
|
||||
dateData = Some(DateCellData(
|
||||
date: date,
|
||||
time: time,
|
||||
includeTime: cellData.includeTime,
|
||||
));
|
||||
}
|
||||
return calData;
|
||||
}
|
||||
|
||||
$fixnum.Int64 timestampFromDateTime(DateTime dateTime) {
|
||||
final timestamp = (dateTime.millisecondsSinceEpoch ~/ 1000);
|
||||
return $fixnum.Int64(timestamp);
|
||||
return dateData;
|
||||
}
|
||||
|
||||
String? timeFromCellData(DateCellDataPB? cellData) {
|
||||
String? time;
|
||||
if (cellData?.hasTime() ?? false) {
|
||||
time = cellData?.time;
|
||||
if (cellData == null || !cellData.hasTime()) {
|
||||
return null;
|
||||
}
|
||||
return time;
|
||||
|
||||
return cellData.time;
|
||||
}
|
||||
|
@ -111,12 +111,14 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
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 (state.dateTypeOptionPB.includeTime) ...[
|
||||
if (includeTime) ...[
|
||||
const VSpace(12.0),
|
||||
_TimeTextField(
|
||||
bloc: context.read<DateCellCalendarBloc>(),
|
||||
@ -206,7 +208,7 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
|
||||
textStyle.textColor(Theme.of(context).disabledColor),
|
||||
),
|
||||
selectedDayPredicate: (day) {
|
||||
return state.calData.fold(
|
||||
return state.dateCellData.fold(
|
||||
() => false,
|
||||
(dateData) => isSameDay(dateData.date, day),
|
||||
);
|
||||
@ -238,7 +240,10 @@ class _IncludeTimeButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocSelector<DateCellCalendarBloc, DateCellCalendarState, bool>(
|
||||
selector: (state) => state.dateTypeOptionPB.includeTime,
|
||||
selector: (state) => state.dateCellData.fold(
|
||||
() => false,
|
||||
(dateData) => dateData.includeTime,
|
||||
),
|
||||
builder: (context, includeTime) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
|
@ -517,6 +517,7 @@ pub(crate) async fn update_date_cell_handler(
|
||||
let cell_changeset = DateCellChangeset {
|
||||
date: data.date,
|
||||
time: data.time,
|
||||
include_time: data.include_time,
|
||||
is_utc: data.is_utc,
|
||||
};
|
||||
|
||||
|
@ -39,7 +39,7 @@ pub trait CellDataChangeset: TypeOption {
|
||||
/// The changeset is able to parse into the concrete data struct if `TypeOption::CellChangeset`
|
||||
/// implements the `FromCellChangesetString` trait.
|
||||
/// For example,the SelectOptionCellChangeset,DateCellChangeset. etc.
|
||||
///
|
||||
///
|
||||
fn apply_changeset(
|
||||
&self,
|
||||
changeset: <Self as TypeOption>::CellChangeset,
|
||||
@ -142,7 +142,7 @@ where
|
||||
|
||||
/// Decode the opaque cell data from one field type to another using the corresponding `TypeOption`
|
||||
///
|
||||
/// The cell data might become an empty string depends on the to_field_type's `TypeOption`
|
||||
/// The cell data might become an empty string depends on the to_field_type's `TypeOption`
|
||||
/// support transform the from_field_type's cell data or not.
|
||||
///
|
||||
/// # Arguments
|
||||
@ -252,6 +252,7 @@ pub fn insert_date_cell(timestamp: i64, field_rev: &FieldRevision) -> CellRevisi
|
||||
let cell_data = serde_json::to_string(&DateCellChangeset {
|
||||
date: Some(timestamp.to_string()),
|
||||
time: None,
|
||||
include_time: Some(false),
|
||||
is_utc: true,
|
||||
})
|
||||
.unwrap();
|
||||
@ -279,7 +280,7 @@ pub fn delete_select_option_cell(
|
||||
CellRevision::new(data)
|
||||
}
|
||||
|
||||
/// Deserialize the String into cell specific data type.
|
||||
/// Deserialize the String into cell specific data type.
|
||||
pub trait FromCellString {
|
||||
fn from_cell_str(s: &str) -> FlowyResult<Self>
|
||||
where
|
||||
|
@ -862,7 +862,8 @@ impl DatabaseViewEditor {
|
||||
let timestamp = date_cell
|
||||
.into_date_field_cell_data()
|
||||
.unwrap_or_default()
|
||||
.into();
|
||||
.timestamp
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(CalendarEventPB {
|
||||
row_id: row_id.to_string(),
|
||||
@ -896,7 +897,7 @@ impl DatabaseViewEditor {
|
||||
// timestamp
|
||||
let timestamp = date_cell
|
||||
.into_date_field_cell_data()
|
||||
.map(|date_cell_data| date_cell_data.0.unwrap_or_default())
|
||||
.map(|date_cell_data| date_cell_data.timestamp.unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
(row_id, timestamp)
|
||||
|
@ -3,8 +3,9 @@ mod tests {
|
||||
use crate::entities::FieldType;
|
||||
use crate::services::cell::{CellDataChangeset, CellDataDecoder};
|
||||
|
||||
use crate::services::field::*;
|
||||
// use crate::services::field::{DateCellChangeset, DateCellData, DateFormat, DateTypeOptionPB, TimeFormat};
|
||||
use crate::services::field::{
|
||||
DateCellChangeset, DateFormat, DateTypeOptionPB, FieldBuilder, TimeFormat, TypeOptionCellData,
|
||||
};
|
||||
use chrono::format::strftime::StrftimeItems;
|
||||
use chrono::{FixedOffset, NaiveDateTime};
|
||||
use database_model::FieldRevision;
|
||||
@ -18,16 +19,44 @@ mod tests {
|
||||
type_option.date_format = date_format;
|
||||
match date_format {
|
||||
DateFormat::Friendly => {
|
||||
assert_date(&type_option, 1647251762, None, "Mar 14,2022", &field_rev);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1647251762,
|
||||
None,
|
||||
"Mar 14,2022",
|
||||
false,
|
||||
&field_rev,
|
||||
);
|
||||
},
|
||||
DateFormat::US => {
|
||||
assert_date(&type_option, 1647251762, None, "2022/03/14", &field_rev);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1647251762,
|
||||
None,
|
||||
"2022/03/14",
|
||||
false,
|
||||
&field_rev,
|
||||
);
|
||||
},
|
||||
DateFormat::ISO => {
|
||||
assert_date(&type_option, 1647251762, None, "2022-03-14", &field_rev);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1647251762,
|
||||
None,
|
||||
"2022-03-14",
|
||||
false,
|
||||
&field_rev,
|
||||
);
|
||||
},
|
||||
DateFormat::Local => {
|
||||
assert_date(&type_option, 1647251762, None, "03/14/2022", &field_rev);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1647251762,
|
||||
None,
|
||||
"03/14/2022",
|
||||
false,
|
||||
&field_rev,
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -41,25 +70,56 @@ mod tests {
|
||||
|
||||
for time_format in TimeFormat::iter() {
|
||||
type_option.time_format = time_format;
|
||||
type_option.include_time = true;
|
||||
match time_format {
|
||||
TimeFormat::TwentyFourHour => {
|
||||
assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
None,
|
||||
"May 27,2022 00:00",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("9:00".to_owned()),
|
||||
"May 27,2022 09:00",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("23:00".to_owned()),
|
||||
"May 27,2022 23:00",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
},
|
||||
TimeFormat::TwelveHour => {
|
||||
assert_date(&type_option, 1653609600, None, "May 27,2022", &field_rev);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
None,
|
||||
"May 27,2022 12:00 AM",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("9:00 AM".to_owned()),
|
||||
"May 27,2022 09:00 AM",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("11:23 pm".to_owned()),
|
||||
"May 27,2022 11:23 PM",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
},
|
||||
@ -72,14 +132,13 @@ mod tests {
|
||||
let type_option = DateTypeOptionPB::default();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_rev = FieldBuilder::from_field_type(&field_type).build();
|
||||
assert_date(&type_option, "abc", None, "", &field_rev);
|
||||
assert_date(&type_option, "abc", None, "", false, &field_rev);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn date_type_option_invalid_include_time_str_test() {
|
||||
let mut type_option = DateTypeOptionPB::new();
|
||||
type_option.include_time = true;
|
||||
let type_option = DateTypeOptionPB::new();
|
||||
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
@ -87,31 +146,46 @@ mod tests {
|
||||
1653609600,
|
||||
Some("1:".to_owned()),
|
||||
"May 27,2022 01:00",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_type_option_empty_include_time_str_test() {
|
||||
let mut type_option = DateTypeOptionPB::new();
|
||||
type_option.include_time = true;
|
||||
let type_option = DateTypeOptionPB::new();
|
||||
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
&type_option,
|
||||
1653609600,
|
||||
Some("".to_owned()),
|
||||
"May 27,2022",
|
||||
"May 27,2022 00:00",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
}
|
||||
|
||||
/// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error.
|
||||
#[test]
|
||||
fn date_type_midnight_include_time_str_test() {
|
||||
let type_option = DateTypeOptionPB::new();
|
||||
let field_type = FieldType::DateTime;
|
||||
let field_rev = 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,
|
||||
);
|
||||
}
|
||||
|
||||
/// The default time format is TwentyFourHour, so the include_time_str in twelve_hours_format will cause parser error.
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn date_type_option_twelve_hours_include_time_str_in_twenty_four_hours_format() {
|
||||
let mut type_option = DateTypeOptionPB::new();
|
||||
type_option.include_time = true;
|
||||
let type_option = DateTypeOptionPB::new();
|
||||
let field_rev = FieldBuilder::from_field_type(&FieldType::DateTime).build();
|
||||
|
||||
assert_date(
|
||||
@ -119,6 +193,25 @@ mod tests {
|
||||
1653609600,
|
||||
Some("1:00 am".to_owned()),
|
||||
"May 27,2022 01:00 AM",
|
||||
true,
|
||||
&field_rev,
|
||||
);
|
||||
}
|
||||
|
||||
// Attempting to parse include_time_str as TwelveHour when TwentyFourHour format is given should cause parser error.
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn date_type_option_twenty_four_hours_include_time_str_in_twelve_hours_format() {
|
||||
let mut type_option = DateTypeOptionPB::new();
|
||||
type_option.time_format = TimeFormat::TwelveHour;
|
||||
let field_rev = 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,
|
||||
);
|
||||
}
|
||||
@ -154,17 +247,19 @@ mod tests {
|
||||
timestamp: T,
|
||||
include_time_str: Option<String>,
|
||||
expected_str: &str,
|
||||
include_time: bool,
|
||||
field_rev: &FieldRevision,
|
||||
) {
|
||||
let changeset = DateCellChangeset {
|
||||
date: Some(timestamp.to_string()),
|
||||
time: include_time_str,
|
||||
is_utc: false,
|
||||
include_time: Some(include_time),
|
||||
};
|
||||
let (cell_str, _) = type_option.apply_changeset(changeset, None).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
decode_cell_data(cell_str, type_option, field_rev),
|
||||
decode_cell_data(cell_str, type_option, include_time, field_rev),
|
||||
expected_str.to_owned(),
|
||||
);
|
||||
}
|
||||
@ -172,13 +267,14 @@ mod tests {
|
||||
fn decode_cell_data(
|
||||
cell_str: String,
|
||||
type_option: &DateTypeOptionPB,
|
||||
include_time: bool,
|
||||
field_rev: &FieldRevision,
|
||||
) -> String {
|
||||
let decoded_data = type_option
|
||||
.decode_cell_str(cell_str, &FieldType::DateTime, field_rev)
|
||||
.unwrap();
|
||||
let decoded_data = type_option.convert_to_protobuf(decoded_data);
|
||||
if type_option.include_time {
|
||||
if include_time {
|
||||
format!("{} {}", decoded_data.date, decoded_data.time)
|
||||
.trim_end()
|
||||
.to_owned()
|
||||
|
@ -8,7 +8,7 @@ use crate::services::field::{
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use chrono::format::strftime::StrftimeItems;
|
||||
use chrono::{NaiveDateTime, Timelike};
|
||||
use chrono::NaiveDateTime;
|
||||
use database_model::{FieldRevision, TypeOptionDataDeserializer, TypeOptionDataSerializer};
|
||||
use flowy_derive::ProtoBuf;
|
||||
use flowy_error::{ErrorCode, FlowyError, FlowyResult};
|
||||
@ -58,97 +58,59 @@ impl DateTypeOptionPB {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn today_desc_from_timestamp<T: Into<i64>>(&self, timestamp: T) -> DateCellDataPB {
|
||||
let timestamp = timestamp.into();
|
||||
let native = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0);
|
||||
if native.is_none() {
|
||||
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 naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0);
|
||||
if naive.is_none() {
|
||||
return DateCellDataPB::default();
|
||||
}
|
||||
let native = native.unwrap();
|
||||
if native.timestamp() == 0 {
|
||||
let naive = naive.unwrap();
|
||||
if timestamp == 0 {
|
||||
return DateCellDataPB::default();
|
||||
}
|
||||
|
||||
let time = native.time();
|
||||
let has_time = time.hour() != 0 || time.second() != 0;
|
||||
|
||||
let utc = self.utc_date_time_from_native(native);
|
||||
let fmt = self.date_format.format_str();
|
||||
let date = format!("{}", utc.format_with_items(StrftimeItems::new(fmt)));
|
||||
let date = format!("{}", naive.format_with_items(StrftimeItems::new(fmt)));
|
||||
|
||||
let mut time = "".to_string();
|
||||
if has_time && self.include_time {
|
||||
let fmt = format!(
|
||||
"{}{}",
|
||||
self.date_format.format_str(),
|
||||
self.time_format.format_str()
|
||||
);
|
||||
time = format!("{}", utc.format_with_items(StrftimeItems::new(&fmt))).replace(&date, "");
|
||||
}
|
||||
let time = if include_time {
|
||||
let fmt = self.time_format.format_str();
|
||||
format!("{}", naive.format_with_items(StrftimeItems::new(&fmt)))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let timestamp = native.timestamp();
|
||||
DateCellDataPB {
|
||||
date,
|
||||
time,
|
||||
include_time,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
fn date_fmt(&self, time: &Option<String>) -> String {
|
||||
if self.include_time {
|
||||
match time.as_ref() {
|
||||
None => self.date_format.format_str().to_string(),
|
||||
Some(time_str) => {
|
||||
if time_str.is_empty() {
|
||||
self.date_format.format_str().to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{} {}",
|
||||
self.date_format.format_str(),
|
||||
self.time_format.format_str()
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
self.date_format.format_str().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp_from_utc_with_time(
|
||||
&self,
|
||||
utc: &chrono::DateTime<chrono::Utc>,
|
||||
time: &Option<String>,
|
||||
naive_date: &NaiveDateTime,
|
||||
time_str: &Option<String>,
|
||||
) -> FlowyResult<i64> {
|
||||
if let Some(time_str) = time.as_ref() {
|
||||
if let Some(time_str) = time_str.as_ref() {
|
||||
if !time_str.is_empty() {
|
||||
let date_str = format!(
|
||||
"{}{}",
|
||||
utc.format_with_items(StrftimeItems::new(self.date_format.format_str())),
|
||||
&time_str
|
||||
);
|
||||
let naive_time =
|
||||
chrono::NaiveTime::parse_from_str(&time_str, self.time_format.format_str());
|
||||
|
||||
return match NaiveDateTime::parse_from_str(&date_str, &self.date_fmt(time)) {
|
||||
Ok(native) => {
|
||||
let utc = self.utc_date_time_from_native(native);
|
||||
Ok(utc.timestamp())
|
||||
match naive_time {
|
||||
Ok(naive_time) => {
|
||||
return Ok(naive_date.date().and_time(naive_time).timestamp());
|
||||
},
|
||||
Err(_e) => {
|
||||
let msg = format!("Parse {} failed", date_str);
|
||||
Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg))
|
||||
let msg = format!("Parse {} failed", time_str);
|
||||
return Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, &msg));
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(utc.timestamp())
|
||||
}
|
||||
|
||||
fn utc_date_time_from_native(
|
||||
&self,
|
||||
naive: chrono::NaiveDateTime,
|
||||
) -> chrono::DateTime<chrono::Utc> {
|
||||
chrono::DateTime::<chrono::Utc>::from_utc(naive, chrono::Utc)
|
||||
Ok(naive_date.timestamp())
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,25 +143,40 @@ impl CellDataChangeset for DateTypeOptionPB {
|
||||
fn apply_changeset(
|
||||
&self,
|
||||
changeset: <Self as TypeOption>::CellChangeset,
|
||||
_type_cell_data: Option<TypeCellData>,
|
||||
type_cell_data: Option<TypeCellData>,
|
||||
) -> FlowyResult<(String, <Self as TypeOption>::CellData)> {
|
||||
let cell_data = match changeset.date_timestamp() {
|
||||
None => 0,
|
||||
Some(date_timestamp) => match (self.include_time, changeset.time) {
|
||||
(true, Some(time)) => {
|
||||
let time = Some(time.trim().to_uppercase());
|
||||
let native = NaiveDateTime::from_timestamp_opt(date_timestamp, 0);
|
||||
if let Some(native) = native {
|
||||
let utc = self.utc_date_time_from_native(native);
|
||||
self.timestamp_from_utc_with_time(&utc, &time)?
|
||||
} else {
|
||||
date_timestamp
|
||||
}
|
||||
},
|
||||
_ => date_timestamp,
|
||||
let (timestamp, include_time) = match type_cell_data {
|
||||
None => (None, false),
|
||||
Some(type_cell_data) => {
|
||||
let cell_data = DateCellData::from_cell_str(&type_cell_data.cell_str).unwrap_or_default();
|
||||
(cell_data.timestamp, cell_data.include_time)
|
||||
},
|
||||
};
|
||||
let date_cell_data = DateCellData(Some(cell_data));
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
_ => Some(date_timestamp),
|
||||
},
|
||||
};
|
||||
|
||||
let date_cell_data = DateCellData {
|
||||
timestamp,
|
||||
include_time,
|
||||
};
|
||||
Ok((date_cell_data.to_string(), date_cell_data))
|
||||
}
|
||||
}
|
||||
@ -215,7 +192,7 @@ impl TypeOptionCellDataFilter for DateTypeOptionPB {
|
||||
return true;
|
||||
}
|
||||
|
||||
filter.is_visible(cell_data.0)
|
||||
filter.is_visible(cell_data.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,7 +202,7 @@ impl TypeOptionCellDataCompare for DateTypeOptionPB {
|
||||
cell_data: &<Self as TypeOption>::CellData,
|
||||
other_cell_data: &<Self as TypeOption>::CellData,
|
||||
) -> Ordering {
|
||||
match (cell_data.0, other_cell_data.0) {
|
||||
match (cell_data.timestamp, other_cell_data.timestamp) {
|
||||
(Some(left), Some(right)) => left.cmp(&right),
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::entities::CellIdPB;
|
||||
use crate::services::cell::{
|
||||
CellProtobufBlobParser, DecodedCellData, FromCellChangesetString, FromCellString,
|
||||
@ -6,6 +8,7 @@ use crate::services::cell::{
|
||||
use bytes::Bytes;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use flowy_error::{internal_error, FlowyResult};
|
||||
use serde::de::Visitor;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
@ -19,6 +22,9 @@ pub struct DateCellDataPB {
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub timestamp: i64,
|
||||
|
||||
#[pb(index = 4)]
|
||||
pub include_time: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ProtoBuf)]
|
||||
@ -32,7 +38,10 @@ pub struct DateChangesetPB {
|
||||
#[pb(index = 3, one_of)]
|
||||
pub time: Option<String>,
|
||||
|
||||
#[pb(index = 4)]
|
||||
#[pb(index = 4, one_of)]
|
||||
pub include_time: Option<bool>,
|
||||
|
||||
#[pb(index = 5)]
|
||||
pub is_utc: bool,
|
||||
}
|
||||
|
||||
@ -40,6 +49,7 @@ pub struct DateChangesetPB {
|
||||
pub struct DateCellChangeset {
|
||||
pub date: Option<String>,
|
||||
pub time: Option<String>,
|
||||
pub include_time: Option<bool>,
|
||||
pub is_utc: bool,
|
||||
}
|
||||
|
||||
@ -71,18 +81,74 @@ impl ToCellChangesetString for DateCellChangeset {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct DateCellData(pub Option<i64>);
|
||||
|
||||
impl std::convert::From<DateCellData> for i64 {
|
||||
fn from(timestamp: DateCellData) -> Self {
|
||||
timestamp.0.unwrap_or(0)
|
||||
}
|
||||
#[derive(Default, Clone, Debug, Serialize)]
|
||||
pub struct DateCellData {
|
||||
pub timestamp: Option<i64>,
|
||||
pub include_time: bool,
|
||||
}
|
||||
|
||||
impl std::convert::From<DateCellData> for Option<i64> {
|
||||
fn from(timestamp: DateCellData) -> Self {
|
||||
timestamp.0
|
||||
impl<'de> serde::Deserialize<'de> for DateCellData {
|
||||
fn deserialize<D>(deserializer: D) -> core::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct DateCellVisitor();
|
||||
|
||||
impl<'de> Visitor<'de> for DateCellVisitor {
|
||||
type Value = DateCellData;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str(
|
||||
"DateCellData with type: str containing either an integer timestamp or the JSON representation",
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
Ok(DateCellData {
|
||||
timestamp: Some(value),
|
||||
include_time: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
self.visit_i64(value as i64)
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut timestamp: Option<i64> = None;
|
||||
let mut include_time: Option<bool> = None;
|
||||
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
"timestamp" => {
|
||||
timestamp = map.next_value()?;
|
||||
},
|
||||
"include_time" => {
|
||||
include_time = map.next_value()?;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
let include_time = include_time.unwrap_or(false);
|
||||
|
||||
Ok(DateCellData {
|
||||
timestamp,
|
||||
include_time,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(DateCellVisitor())
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,17 +157,14 @@ impl FromCellString for DateCellData {
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let num = s.parse::<i64>().ok();
|
||||
Ok(DateCellData(num))
|
||||
let result: DateCellData = serde_json::from_str(s).unwrap();
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for DateCellData {
|
||||
fn to_string(&self) -> String {
|
||||
match self.0 {
|
||||
None => "".to_string(),
|
||||
Some(val) => val.to_string(),
|
||||
}
|
||||
serde_json::to_string(self).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,21 @@ mod tests {
|
||||
),
|
||||
"Mar 14,2022"
|
||||
);
|
||||
|
||||
let data = DateCellData {
|
||||
timestamp: Some(1647251762),
|
||||
include_time: true,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
stringify_cell_data(
|
||||
data.to_string(),
|
||||
&FieldType::RichText,
|
||||
&field_type,
|
||||
&field_rev
|
||||
),
|
||||
"Mar 14,2022"
|
||||
);
|
||||
}
|
||||
|
||||
// Test parser the cell data which field's type is FieldType::SingleSelect to cell data
|
||||
|
@ -43,6 +43,7 @@ impl DatabaseRowTestBuilder {
|
||||
date: Some(data.to_string()),
|
||||
time: None,
|
||||
is_utc: true,
|
||||
include_time: Some(false),
|
||||
})
|
||||
.unwrap();
|
||||
let date_field = self.field_rev_with_type(&FieldType::DateTime);
|
||||
|
@ -64,6 +64,7 @@ pub fn make_date_cell_string(s: &str) -> String {
|
||||
date: Some(s.to_string()),
|
||||
time: None,
|
||||
is_utc: true,
|
||||
include_time: Some(false),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user