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:
Richard Shiue 2023-03-09 14:25:33 +08:00 committed by GitHub
parent 5e8f6a53a0
commit 77ff2e987a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 345 additions and 175 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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