diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart index 0f980d4a98..8ec623f042 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart @@ -23,50 +23,61 @@ class DateCalBloc extends Bloc { }) : super(DateCalState.initial(dateTypeOption, selectedDay)) { on( (event, emit) async { - await event.map( - initial: (_Initial value) async { - _startListening(); - // await _loadDateTypeOption(emit); + await event.when( + initial: () async => _startListening(), + selectDay: (date) { + _updateDateData(emit, date: date); }, - selectDay: (_SelectDay value) { - if (state.dateData != null) { - if (!isSameDay(state.dateData!.date, value.day)) { - final newDateData = state.dateData!.copyWith(date: value.day); - emit(state.copyWith(dateData: newDateData)); - } - } else { - emit(state.copyWith(dateData: DateCellPersistenceData(date: value.day))); - } + setCalFormat: (format) { + emit(state.copyWith(format: format)); }, - setCalFormat: (_CalendarFormat value) { - emit(state.copyWith(format: value.format)); + setFocusedDay: (focusedDay) { + emit(state.copyWith(focusedDay: focusedDay)); }, - setFocusedDay: (_FocusedDay value) { - emit(state.copyWith(focusedDay: value.day)); + didReceiveCellUpdate: (value) {}, + setIncludeTime: (includeTime) async { + await _updateTypeOption(emit, includeTime: includeTime); }, - didReceiveCellUpdate: (_DidReceiveCellUpdate value) {}, - setIncludeTime: (_IncludeTime value) async { - await _updateTypeOption(emit, includeTime: value.includeTime); + setDateFormat: (dateFormat) async { + await _updateTypeOption(emit, dateFormat: dateFormat); }, - setDateFormat: (_DateFormat value) async { - await _updateTypeOption(emit, dateFormat: value.dateFormat); + setTimeFormat: (timeFormat) async { + await _updateTypeOption(emit, timeFormat: timeFormat); }, - setTimeFormat: (_TimeFormat value) async { - await _updateTypeOption(emit, timeFormat: value.timeFormat); - }, - setTime: (_Time value) { - if (state.dateData != null) { - final newDateData = state.dateData!.copyWith(time: value.time); - emit(state.copyWith(dateData: newDateData)); - } else { - emit(state.copyWith(dateData: DateCellPersistenceData(date: DateTime.now(), time: value.time))); - } + setTime: (time) { + _updateDateData(emit, time: time); }, ); }, ); } + void _updateDateData(Emitter emit, {DateTime? date, String? time}) { + state.dateData.fold( + () { + var newDateData = DateCellPersistenceData(date: date ?? DateTime.now()); + if (time != null) { + newDateData = newDateData.copyWith(time: time); + } + emit(state.copyWith(dateData: Some(newDateData))); + }, + (dateData) { + var newDateData = dateData; + if (date != null && !isSameDay(newDateData.date, date)) { + newDateData = newDateData.copyWith(date: date); + } + + if (newDateData.time != time) { + newDateData = newDateData.copyWith(time: time); + } + + if (newDateData != dateData) { + emit(state.copyWith(dateData: Some(newDateData))); + } + }, + ); + } + @override Future close() async { if (_onCellChangedFn != null) { @@ -142,16 +153,16 @@ class DateCalState with _$DateCalState { required DateTime focusedDay, required String time, required Option inputTimeError, - DateCellPersistenceData? dateData, + required Option dateData, }) = _DateCalState; factory DateCalState.initial( DateTypeOption dateTypeOption, DateTime? selectedDay, ) { - DateCellPersistenceData? dateData; + Option dateData = none(); if (selectedDay != null) { - dateData = DateCellPersistenceData(date: selectedDay); + dateData = Some(DateCellPersistenceData(date: selectedDay)); } return DateCalState( diff --git a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart index c740cf0e57..24ddb564cd 100644 --- a/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart @@ -12,21 +12,11 @@ class DateCellBloc extends Bloc { DateCellBloc({required this.cellContext}) : super(DateCellState.initial(cellContext)) { on( (event, emit) async { - event.map( - initial: (_InitialCell value) { - _startListening(); - }, - selectDay: (_SelectDay value) { - cellContext.saveCellData(value.data); - }, - didReceiveCellUpdate: (_DidReceiveCellUpdate value) { - emit(state.copyWith( - content: value.cell.content, - )); - }, - didReceiveFieldUpdate: (_DidReceiveFieldUpdate value) { - emit(state.copyWith(field: value.field)); - }, + event.when( + initial: () => _startListening(), + selectDate: (DateCellPersistenceData value) => cellContext.saveCellData(value), + didReceiveCellUpdate: (Cell value) => emit(state.copyWith(content: value.content)), + didReceiveFieldUpdate: (Field value) => emit(state.copyWith(field: value)), ); }, ); @@ -56,7 +46,7 @@ class DateCellBloc extends Bloc { @freezed class DateCellEvent with _$DateCellEvent { const factory DateCellEvent.initial() = _InitialCell; - const factory DateCellEvent.selectDay(DateCellPersistenceData data) = _SelectDay; + const factory DateCellEvent.selectDate(DateCellPersistenceData data) = _SelectDay; const factory DateCellEvent.didReceiveCellUpdate(Cell cell) = _DidReceiveCellUpdate; const factory DateCellEvent.didReceiveFieldUpdate(Field field) = _DidReceiveFieldUpdate; } diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/calendar.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/calendar.dart index 84a4760f0c..8c34105f0b 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/calendar.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/calendar.dart @@ -112,9 +112,10 @@ class _CellCalendarWidget extends StatelessWidget { )..add(const DateCalEvent.initial()), child: BlocConsumer( listener: (context, state) { - if (state.dateData != null) { - onSelected(state.dateData!); - } + state.dateData.fold( + () => null, + (dateData) => onSelected(dateData), + ); }, listenWhen: (p, c) => p.dateData != c.dateData, builder: (context, state) { @@ -127,7 +128,13 @@ class _CellCalendarWidget extends StatelessWidget { if (state.dateTypeOption.includeTime) { children.addAll([ - const _TimeTextField(), + _TimeTextField( + time: "", + errorText: state.inputTimeError.fold(() => "", (error) => error.toString()), + onEditingComplete: (text) { + context.read().add(DateCalEvent.setTime(text)); + }, + ), ]); } @@ -190,11 +197,10 @@ class _CellCalendarWidget extends StatelessWidget { ), ), selectedDayPredicate: (day) { - if (state.dateData != null) { - return isSameDay(state.dateData!.date, day); - } else { - return false; - } + return state.dateData.fold( + () => false, + (dateData) => isSameDay(dateData.date, day), + ); }, onDaySelected: (selectedDay, focusedDay) { context.read().add(DateCalEvent.selectDay(selectedDay)); @@ -241,8 +247,36 @@ class _IncludeTimeButton extends StatelessWidget { } } -class _TimeTextField extends StatelessWidget { - const _TimeTextField({Key? key}) : super(key: key); +class _TimeTextField extends StatefulWidget { + final String errorText; + final String time; + final void Function(String) onEditingComplete; + const _TimeTextField({ + Key? key, + required this.time, + required this.errorText, + required this.onEditingComplete, + }) : super(key: key); + + @override + State<_TimeTextField> createState() => _TimeTextFieldState(); +} + +class _TimeTextFieldState extends State<_TimeTextField> { + late final FocusNode _focusNode; + late final TextEditingController _controller; + + @override + void initState() { + _focusNode = FocusNode(); + _controller = TextEditingController(text: widget.time); + _focusNode.addListener(() { + if (mounted) { + widget.onEditingComplete(_controller.text); + } + }); + super.initState(); + } @override Widget build(BuildContext context) { @@ -251,15 +285,25 @@ class _TimeTextField extends StatelessWidget { padding: kMargin, child: RoundedInputField( height: 40, + focusNode: _focusNode, + controller: _controller, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), normalBorderColor: theme.shader4, errorBorderColor: theme.red, cursorColor: theme.main1, - errorText: context.read().state.inputTimeError.fold(() => "", (error) => error.toString()), - onEditingComplete: (value) => context.read().add(DateCalEvent.setTime(value)), + errorText: widget.errorText, + onEditingComplete: (value) { + widget.onEditingComplete(value); + }, ), ); } + + @override + void dispose() { + _focusNode.dispose(); + super.dispose(); + } } class _DateTypeOptionButton extends StatelessWidget { diff --git a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart index bbf0f55ffc..1a278ce1da 100644 --- a/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart +++ b/frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart @@ -78,7 +78,7 @@ class _DateCellState extends State { calendar.show( context, cellContext: bloc.cellContext.clone(), - onSelected: (day) => bloc.add(DateCellEvent.selectDay(day)), + onSelected: (data) => bloc.add(DateCellEvent.selectDate(data)), ); } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs index b8a858c80d..01a748ea68 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/date_type_option.rs @@ -1,20 +1,18 @@ use crate::impl_type_option; +use crate::services::entities::{CellIdentifier, CellIdentifierPayload}; +use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use crate::services::row::{CellContentChangeset, CellDataOperation, DecodedCellData, TypeOptionCellData}; use bytes::Bytes; use chrono::format::strftime::StrftimeItems; -use chrono::{Datelike, NaiveDateTime}; +use chrono::NaiveDateTime; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::{ErrorCode, FlowyError}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_grid_data_model::entities::{ CellChangeset, CellMeta, FieldMeta, FieldType, TypeOptionDataDeserializer, TypeOptionDataEntry, }; - use serde::{Deserialize, Serialize}; +use std::ops::Add; use std::str::FromStr; - -use crate::services::entities::{CellIdentifier, CellIdentifierPayload}; -use crate::services::field::type_options::util::get_cell_data; -use crate::services::field::{BoxTypeOptionBuilder, TypeOptionBuilder}; use strum_macros::EnumIter; // Date @@ -33,18 +31,45 @@ impl_type_option!(DateTypeOption, FieldType::DateTime); impl DateTypeOption { #[allow(dead_code)] - fn today_from_timestamp(&self, timestamp: i64) -> String { + fn today_desc_from_timestamp(&self, timestamp: i64) -> String { let native = chrono::NaiveDateTime::from_timestamp(timestamp, 0); - self.today_from_native(native) + self.today_desc_from_native(native) } - fn today_from_native(&self, naive: chrono::NaiveDateTime) -> String { - let utc: chrono::DateTime = chrono::DateTime::from_utc(naive, chrono::Utc); - let local: chrono::DateTime = chrono::DateTime::from(utc); - let output = format!("{}", local.format_with_items(StrftimeItems::new(&self.fmt_str()))); + fn today_desc_from_str(&self, s: String) -> String { + match NaiveDateTime::parse_from_str(&s, &self.fmt_str()) { + Ok(native) => self.today_desc_from_native(native), + Err(_) => "".to_owned(), + } + } + + fn today_desc_from_native(&self, native: chrono::NaiveDateTime) -> String { + let utc = self.utc_date_time_from_native(native); + // let china_timezone = FixedOffset::east(8 * 3600); + // let a = utc.with_timezone(&china_timezone); + let output = format!("{}", utc.format_with_items(StrftimeItems::new(&self.fmt_str()))); output } + fn timestamp_from_str(&self, s: &str) -> FlowyResult { + match NaiveDateTime::parse_from_str(s, &self.fmt_str()) { + Ok(native) => { + let utc = self.utc_date_time_from_native(native); + Ok(utc.timestamp()) + } + Err(_) => Err(ErrorCode::InvalidData.into()), + } + } + + fn utc_date_time_from_timestamp(&self, timestamp: i64) -> chrono::DateTime { + let native = NaiveDateTime::from_timestamp(timestamp, 0); + self.utc_date_time_from_native(native) + } + + fn utc_date_time_from_native(&self, naive: chrono::NaiveDateTime) -> chrono::DateTime { + chrono::DateTime::::from_utc(naive, chrono::Utc) + } + fn fmt_str(&self) -> String { if self.include_time { format!("{} {}", self.date_format.format_str(), self.time_format.format_str()) @@ -63,14 +88,11 @@ impl CellDataOperation for DateTypeOption { let cell_data = type_option_cell_data.data; if let Ok(timestamp) = cell_data.parse::() { - let native = NaiveDateTime::from_timestamp(timestamp, 0); - return DecodedCellData::new(format!("{}", timestamp), self.today_from_native(native)); + return DecodedCellData::new(format!("{}", timestamp), self.today_desc_from_timestamp(timestamp)); } - return match NaiveDateTime::parse_from_str(&cell_data, &self.fmt_str()) { - Ok(date_time) => DecodedCellData::new(format!("{}", date_time.timestamp()), cell_data), - Err(_) => DecodedCellData::default(), - }; + let cell_content = self.today_desc_from_str(cell_data.clone()); + return DecodedCellData::new(cell_data, cell_content); } DecodedCellData::default() @@ -79,25 +101,29 @@ impl CellDataOperation for DateTypeOption { fn apply_changeset>( &self, changeset: T, - cell_meta: Option, + _cell_meta: Option, ) -> Result { let content_changeset: DateCellContentChangeset = serde_json::from_str(&changeset.into())?; - match cell_meta { - None => Ok(TypeOptionCellData::new("", self.field_type()).json()), - Some(cell_meta) => { - let s = match content_changeset.timestamp() { - None => get_cell_data(&cell_meta), - Some(timestamp) => timestamp.to_string(), - }; - - Ok(TypeOptionCellData::new(s, self.field_type()).json()) - - // let changeset = changeset.into(); - // if changeset.parse::().is_err() || changeset.parse::().is_err() { - // return Err(FlowyError::internal().context(format!("Parse {} failed", changeset))); - // }; + let cell_content = match content_changeset.date_timestamp() { + None => "".to_owned(), + Some(date_timestamp) => { + // + match (self.include_time, content_changeset.time) { + (true, Some(time)) => { + let utc = self.utc_date_time_from_timestamp(date_timestamp); + let mut date_str = format!( + "{}", + utc.format_with_items(StrftimeItems::new(self.date_format.format_str())) + ); + date_str = date_str.add(&time); + let timestamp = self.timestamp_from_str(&date_str)?; + timestamp.to_string() + } + _ => date_timestamp.to_string(), + } } - } + }; + Ok(TypeOptionCellData::new(cell_content, self.field_type()).json()) } } @@ -197,7 +223,7 @@ impl TimeFormat { // https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html pub fn format_str(&self) -> &'static str { match self { - TimeFormat::TwelveHour => "%r", + TimeFormat::TwelveHour => "%I:%M %p", TimeFormat::TwentyFourHour => "%R", } } @@ -240,37 +266,6 @@ impl TryInto for DateChangesetPayload { } } -#[derive(Clone, Serialize, Deserialize)] -pub struct DateCellContentChangeset { - pub date: Option, - pub time: Option, -} - -impl DateCellContentChangeset { - pub fn timestamp(self) -> Option { - let mut timestamp = 0; - if let Some(date) = self.date { - match date.parse::() { - Ok(date_timestamp) => { - timestamp += date_timestamp; - } - Err(_) => {} - } - } else { - return None; - } - - if let Some(time) = self.time { - match time.parse::() { - Ok(time_timestamp) => timestamp += time_timestamp, - Err(_) => {} - } - } - - return Some(timestamp); - } -} - impl std::convert::From for CellChangeset { fn from(params: DateChangesetParams) -> Self { let changeset = DateCellContentChangeset { @@ -287,10 +282,36 @@ impl std::convert::From for CellChangeset { } } +#[derive(Clone, Serialize, Deserialize)] +pub struct DateCellContentChangeset { + pub date: Option, + pub time: Option, +} + +impl DateCellContentChangeset { + pub fn date_timestamp(&self) -> Option { + if let Some(date) = &self.date { + match date.parse::() { + Ok(date_timestamp) => Some(date_timestamp), + Err(_) => None, + } + } else { + None + } + } +} + +impl std::convert::From for CellContentChangeset { + fn from(changeset: DateCellContentChangeset) -> Self { + let s = serde_json::to_string(&changeset).unwrap(); + CellContentChangeset::from(s) + } +} + #[cfg(test)] mod tests { use crate::services::field::FieldBuilder; - use crate::services::field::{DateFormat, DateTypeOption, TimeFormat}; + use crate::services::field::{DateCellContentChangeset, DateFormat, DateTypeOption, TimeFormat}; use crate::services::row::{CellDataOperation, TypeOptionCellData}; use flowy_grid_data_model::entities::FieldType; use strum::IntoEnumIterator; @@ -362,14 +383,20 @@ mod tests { type_option.time_format = time_format; match time_format { TimeFormat::TwentyFourHour => { - assert_eq!("Mar 14,2022".to_owned(), type_option.today_from_timestamp(1647251762)); + assert_eq!( + "Mar 14,2022".to_owned(), + type_option.today_desc_from_timestamp(1647251762) + ); assert_eq!( "Mar 14,2022".to_owned(), type_option.decode_cell_data(data("1647251762"), &field_meta).content ); } TimeFormat::TwelveHour => { - assert_eq!("Mar 14,2022".to_owned(), type_option.today_from_timestamp(1647251762)); + assert_eq!( + "Mar 14,2022".to_owned(), + type_option.today_desc_from_timestamp(1647251762) + ); assert_eq!( "Mar 14,2022".to_owned(), type_option.decode_cell_data(data("1647251762"), &field_meta).content @@ -379,6 +406,72 @@ mod tests { } } + #[test] + fn date_description_apply_changeset_test() { + let mut type_option = DateTypeOption::default(); + let field_meta = FieldBuilder::from_field_type(&FieldType::Number).build(); + let date_timestamp = "1653609600".to_owned(); + + let changeset = DateCellContentChangeset { + date: Some(date_timestamp.clone()), + time: None, + }; + let result = type_option.apply_changeset(changeset, None).unwrap(); + let content = type_option.decode_cell_data(result.clone(), &field_meta).content; + assert_eq!(content, "May 27,2022".to_owned()); + + type_option.include_time = true; + let content = type_option.decode_cell_data(result, &field_meta).content; + assert_eq!(content, "May 27,2022 00:00".to_owned()); + + let changeset = DateCellContentChangeset { + date: Some(date_timestamp.clone()), + time: Some("1:00".to_owned()), + }; + let result = type_option.apply_changeset(changeset, None).unwrap(); + let content = type_option.decode_cell_data(result, &field_meta).content; + assert_eq!(content, "May 27,2022 01:00".to_owned()); + + let changeset = DateCellContentChangeset { + date: Some(date_timestamp), + time: Some("1:00 am".to_owned()), + }; + type_option.time_format = TimeFormat::TwelveHour; + let result = type_option.apply_changeset(changeset, None).unwrap(); + let content = type_option.decode_cell_data(result, &field_meta).content; + assert_eq!(content, "May 27,2022 01:00 AM".to_owned()); + } + + #[test] + #[should_panic] + fn date_description_apply_changeset_error_test() { + let mut type_option = DateTypeOption::default(); + type_option.include_time = true; + let field_meta = FieldBuilder::from_field_type(&FieldType::Number).build(); + let date_timestamp = "1653609600".to_owned(); + + let changeset = DateCellContentChangeset { + date: Some(date_timestamp.clone()), + time: Some("1:a0".to_owned()), + }; + let _ = type_option.apply_changeset(changeset, None).unwrap(); + + let changeset = DateCellContentChangeset { + date: Some(date_timestamp.clone()), + time: Some("1:".to_owned()), + }; + let _ = type_option.apply_changeset(changeset, None).unwrap(); + + // let changeset = DateCellContentChangeset { + // date: Some(date_timestamp), + // time: Some("1:00 am".to_owned()), + // }; + // type_option.time_format = TimeFormat::TwelveHour; + // let result = type_option.apply_changeset(changeset, None).unwrap(); + // let content = type_option.decode_cell_data(result, &field_meta).content; + // assert_eq!(content, "May 27,2022 01:00 AM".to_owned()); + } + #[test] #[should_panic] fn date_description_invalid_data_test() { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index c07b81ac58..b0695af1fd 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -337,7 +337,7 @@ impl ClientGridEditor { } #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn update_cell(&self, mut cell_changeset: CellChangeset) -> FlowyResult<()> { + pub async fn update_cell(&self, cell_changeset: CellChangeset) -> FlowyResult<()> { if cell_changeset.cell_content_changeset.as_ref().is_none() { return Ok(()); }