Explorar o código

chore: add date type option test

appflowy %!s(int64=3) %!d(string=hai) anos
pai
achega
4f9470b076

+ 46 - 35
frontend/app_flowy/lib/workspace/application/grid/cell/date_cal_bloc.dart

@@ -23,50 +23,61 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
   }) : super(DateCalState.initial(dateTypeOption, selectedDay)) {
     on<DateCalEvent>(
       (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<DateCalState> 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<void> close() async {
     if (_onCellChangedFn != null) {
@@ -142,16 +153,16 @@ class DateCalState with _$DateCalState {
     required DateTime focusedDay,
     required String time,
     required Option<FlowyError> inputTimeError,
-    DateCellPersistenceData? dateData,
+    required Option<DateCellPersistenceData> dateData,
   }) = _DateCalState;
 
   factory DateCalState.initial(
     DateTypeOption dateTypeOption,
     DateTime? selectedDay,
   ) {
-    DateCellPersistenceData? dateData;
+    Option<DateCellPersistenceData> dateData = none();
     if (selectedDay != null) {
-      dateData = DateCellPersistenceData(date: selectedDay);
+      dateData = Some(DateCellPersistenceData(date: selectedDay));
     }
 
     return DateCalState(

+ 6 - 16
frontend/app_flowy/lib/workspace/application/grid/cell/date_cell_bloc.dart

@@ -12,21 +12,11 @@ class DateCellBloc extends Bloc<DateCellEvent, DateCellState> {
   DateCellBloc({required this.cellContext}) : super(DateCellState.initial(cellContext)) {
     on<DateCellEvent>(
       (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<DateCellEvent, DateCellState> {
 @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;
 }

+ 57 - 13
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<DateCalBloc, DateCalState>(
         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<DateCalBloc>().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<DateCalBloc>().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<DateCalBloc>().state.inputTimeError.fold(() => "", (error) => error.toString()),
-        onEditingComplete: (value) => context.read<DateCalBloc>().add(DateCalEvent.setTime(value)),
+        errorText: widget.errorText,
+        onEditingComplete: (value) {
+          widget.onEditingComplete(value);
+        },
       ),
     );
   }
+
+  @override
+  void dispose() {
+    _focusNode.dispose();
+    super.dispose();
+  }
 }
 
 class _DateTypeOptionButton extends StatelessWidget {

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/plugins/grid/src/widgets/cell/date_cell/date_cell.dart

@@ -78,7 +78,7 @@ class _DateCellState extends State<DateCell> {
     calendar.show(
       context,
       cellContext: bloc.cellContext.clone(),
-      onSelected: (day) => bloc.add(DateCellEvent.selectDay(day)),
+      onSelected: (data) => bloc.add(DateCellEvent.selectDate(data)),
     );
   }
 

+ 161 - 68
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::Utc> = chrono::DateTime::from_utc(naive, chrono::Utc);
-        let local: chrono::DateTime<chrono::Local> = 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<i64> {
+        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<chrono::Utc> {
+        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::Utc> {
+        chrono::DateTime::<chrono::Utc>::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::<i64>() {
-                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<T: Into<CellContentChangeset>>(
         &self,
         changeset: T,
-        cell_meta: Option<CellMeta>,
+        _cell_meta: Option<CellMeta>,
     ) -> Result<String, FlowyError> {
         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::<f64>().is_err() || changeset.parse::<i64>().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,6 +266,22 @@ impl TryInto<DateChangesetParams> for DateChangesetPayload {
     }
 }
 
+impl std::convert::From<DateChangesetParams> for CellChangeset {
+    fn from(params: DateChangesetParams) -> Self {
+        let changeset = DateCellContentChangeset {
+            date: params.date,
+            time: params.time,
+        };
+        let s = serde_json::to_string(&changeset).unwrap();
+        CellChangeset {
+            grid_id: params.cell_identifier.grid_id,
+            row_id: params.cell_identifier.row_id,
+            field_id: params.cell_identifier.field_id,
+            cell_content_changeset: Some(s),
+        }
+    }
+}
+
 #[derive(Clone, Serialize, Deserialize)]
 pub struct DateCellContentChangeset {
     pub date: Option<String>,
@@ -247,50 +289,29 @@ pub struct DateCellContentChangeset {
 }
 
 impl DateCellContentChangeset {
-    pub fn timestamp(self) -> Option<i64> {
-        let mut timestamp = 0;
-        if let Some(date) = self.date {
+    pub fn date_timestamp(&self) -> Option<i64> {
+        if let Some(date) = &self.date {
             match date.parse::<i64>() {
-                Ok(date_timestamp) => {
-                    timestamp += date_timestamp;
-                }
-                Err(_) => {}
+                Ok(date_timestamp) => Some(date_timestamp),
+                Err(_) => None,
             }
         } else {
-            return None;
-        }
-
-        if let Some(time) = self.time {
-            match time.parse::<i64>() {
-                Ok(time_timestamp) => timestamp += time_timestamp,
-                Err(_) => {}
-            }
+            None
         }
-
-        return Some(timestamp);
     }
 }
 
-impl std::convert::From<DateChangesetParams> for CellChangeset {
-    fn from(params: DateChangesetParams) -> Self {
-        let changeset = DateCellContentChangeset {
-            date: params.date,
-            time: params.time,
-        };
+impl std::convert::From<DateCellContentChangeset> for CellContentChangeset {
+    fn from(changeset: DateCellContentChangeset) -> Self {
         let s = serde_json::to_string(&changeset).unwrap();
-        CellChangeset {
-            grid_id: params.cell_identifier.grid_id,
-            row_id: params.cell_identifier.row_id,
-            field_id: params.cell_identifier.field_id,
-            cell_content_changeset: Some(s),
-        }
+        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() {

+ 1 - 1
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(());
         }