diff --git a/frontend/appflowy_flutter/integration_test/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/database_cell_test.dart index d5f9fa637f..e9862db208 100644 --- a/frontend/appflowy_flutter/integration_test/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_cell_test.dart @@ -218,7 +218,6 @@ void main() { await tester.assertDateCellInGrid( rowIndex: 0, - fieldType: fieldType, content: DateFormat('MMM dd, y').format(today), ); @@ -233,7 +232,6 @@ void main() { await tester.assertDateCellInGrid( rowIndex: 0, - fieldType: fieldType, content: DateFormat('MMM dd, y HH:mm').format(now), ); @@ -247,7 +245,6 @@ void main() { await tester.assertDateCellInGrid( rowIndex: 0, - fieldType: fieldType, content: DateFormat('dd/MM/y HH:mm').format(now), ); @@ -261,7 +258,6 @@ void main() { await tester.assertDateCellInGrid( rowIndex: 0, - fieldType: fieldType, content: DateFormat('dd/MM/y hh:mm a').format(now), ); @@ -273,7 +269,6 @@ void main() { await tester.assertDateCellInGrid( rowIndex: 0, - fieldType: fieldType, content: '', ); diff --git a/frontend/appflowy_flutter/integration_test/database_share_test.dart b/frontend/appflowy_flutter/integration_test/database_share_test.dart index d6571875b9..d81311028c 100644 --- a/frontend/appflowy_flutter/integration_test/database_share_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_share_test.dart @@ -157,7 +157,6 @@ void main() { for (final (index, content) in dateCells.indexed) { await tester.assertDateCellInGrid( rowIndex: index, - fieldType: FieldType.DateTime, content: content, ); } diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index be34b62610..db6433b510 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -43,6 +43,7 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_ import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; @@ -262,15 +263,12 @@ extension AppFlowyDatabaseTest on WidgetTester { Future assertDateCellInGrid({ required int rowIndex, - required FieldType fieldType, required String content, }) async { final findRow = find.byType(GridRow, skipOffstage: false); final findCell = find.descendant( of: findRow.at(rowIndex), - matching: find.byWidgetPredicate( - (widget) => widget is GridDateCell && widget.fieldType == fieldType, - ), + matching: find.byType(GridDateCell), skipOffstage: false, ); @@ -1287,7 +1285,7 @@ Finder finderForFieldType(FieldType fieldType) { return find.byType(GridDateCell, skipOffstage: false); case FieldType.LastEditedTime: case FieldType.CreatedTime: - return find.byType(GridDateCell, skipOffstage: false); + return find.byType(GridTimestampCell, skipOffstage: false); case FieldType.SingleSelect: return find.byType(GridSingleSelectCell, skipOffstage: false); case FieldType.MultiSelect: diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart index 31aefb4a8f..7d0458adcf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_controller_builder.dart @@ -2,6 +2,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb. import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'cell_controller.dart'; @@ -14,6 +15,7 @@ typedef SelectOptionCellController = CellController; typedef ChecklistCellController = CellController; typedef DateCellController = CellController; +typedef TimestampCellController = CellController; typedef URLCellController = CellController; class CellControllerBuilder { @@ -41,14 +43,11 @@ class CellControllerBuilder { TextCellDataPersistence(cellContext: _cellContext), ); case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: final cellDataLoader = CellDataLoader( cellContext: _cellContext, parser: DateCellDataParser(), reloadOnFieldChanged: true, ); - return DateCellController( cellContext: _cellContext, cellCache: _cellCache, @@ -56,6 +55,20 @@ class CellControllerBuilder { cellDataPersistence: TextCellDataPersistence(cellContext: _cellContext), ); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + final cellDataLoader = CellDataLoader( + cellContext: _cellContext, + parser: TimestampCellDataParser(), + reloadOnFieldChanged: true, + ); + return TimestampCellController( + cellContext: _cellContext, + cellCache: _cellCache, + cellDataLoader: cellDataLoader, + cellDataPersistence: + TextCellDataPersistence(cellContext: _cellContext), + ); case FieldType.Number: final cellDataLoader = CellDataLoader( cellContext: _cellContext, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart index f8be693e6b..5896e17204 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_data_loader.dart @@ -73,6 +73,16 @@ class DateCellDataParser implements CellDataParser { } } +class TimestampCellDataParser implements CellDataParser { + @override + TimestampCellDataPB? parserData(List data) { + if (data.isEmpty) { + return null; + } + return TimestampCellDataPB.fromBuffer(data); + } +} + class SelectOptionCellDataParser implements CellDataParser { @override diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart index 4809d4afc8..6f4d330c25 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/cell_service.dart @@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb. import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:dartz/dartz.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart index 637557c75e..393f1c243b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/date_bloc.dart @@ -27,13 +27,6 @@ class DateTypeOptionBloc ), ); }, - includeTime: (_IncludeTime value) { - emit( - state.copyWith( - typeOption: _updateTypeOption(includeTime: value.includeTime), - ), - ); - }, ); }, ); @@ -42,7 +35,6 @@ class DateTypeOptionBloc DateTypeOptionPB _updateTypeOption({ DateFormatPB? dateFormat, TimeFormatPB? timeFormat, - bool? includeTime, }) { state.typeOption.freeze(); return state.typeOption.rebuild((typeOption) { @@ -63,8 +55,6 @@ class DateTypeOptionEvent with _$DateTypeOptionEvent { _DidSelectDateFormat; const factory DateTypeOptionEvent.didSelectTimeFormat(TimeFormatPB format) = _DidSelectTimeFormat; - const factory DateTypeOptionEvent.includeTime(bool includeTime) = - _IncludeTime; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/timestamp_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/timestamp_bloc.dart new file mode 100644 index 0000000000..1e59b1379a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/timestamp_bloc.dart @@ -0,0 +1,76 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; + +import 'type_option_context.dart'; +part 'timestamp_bloc.freezed.dart'; + +class TimestampTypeOptionBloc + extends Bloc { + TimestampTypeOptionBloc({ + required TimestampTypeOptionContext typeOptionContext, + }) : super(TimestampTypeOptionState.initial(typeOptionContext.typeOption)) { + on( + (event, emit) async { + event.map( + didSelectDateFormat: (_DidSelectDateFormat value) { + _updateTypeOption(dateFormat: value.format, emit: emit); + }, + didSelectTimeFormat: (_DidSelectTimeFormat value) { + _updateTypeOption(timeFormat: value.format, emit: emit); + }, + includeTime: (_IncludeTime value) { + _updateTypeOption(includeTime: value.includeTime, emit: emit); + }, + ); + }, + ); + } + + void _updateTypeOption({ + DateFormatPB? dateFormat, + TimeFormatPB? timeFormat, + bool? includeTime, + required Emitter emit, + }) { + state.typeOption.freeze(); + final newTypeOption = state.typeOption.rebuild((typeOption) { + if (dateFormat != null) { + typeOption.dateFormat = dateFormat; + } + + if (timeFormat != null) { + typeOption.timeFormat = timeFormat; + } + + if (includeTime != null) { + typeOption.includeTime = includeTime; + } + }); + emit(state.copyWith(typeOption: newTypeOption)); + } +} + +@freezed +class TimestampTypeOptionEvent with _$TimestampTypeOptionEvent { + const factory TimestampTypeOptionEvent.didSelectDateFormat( + DateFormatPB format, + ) = _DidSelectDateFormat; + const factory TimestampTypeOptionEvent.didSelectTimeFormat( + TimeFormatPB format, + ) = _DidSelectTimeFormat; + const factory TimestampTypeOptionEvent.includeTime(bool includeTime) = + _IncludeTime; +} + +@freezed +class TimestampTypeOptionState with _$TimestampTypeOptionState { + const factory TimestampTypeOptionState({ + required TimestampTypeOptionPB typeOption, + }) = _TimestampTypeOptionState; + + factory TimestampTypeOptionState.initial(TimestampTypeOptionPB typeOption) => + TimestampTypeOptionState(typeOption: typeOption); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart index a79de8ee61..a4a48eedfa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/field/type_option/type_option_context.dart @@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart' import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; @@ -58,7 +59,7 @@ class URLTypeOptionWidgetDataParser extends TypeOptionParser { } } -// Date +// DateTime typedef DateTypeOptionContext = TypeOptionContext; class DateTypeOptionDataParser extends TypeOptionParser { @@ -68,6 +69,17 @@ class DateTypeOptionDataParser extends TypeOptionParser { } } +// LastModified and CreatedAt +typedef TimestampTypeOptionContext = TypeOptionContext; + +class TimestampTypeOptionDataParser + extends TypeOptionParser { + @override + TimestampTypeOptionPB fromBuffer(List buffer) { + return TimestampTypeOptionPB.fromBuffer(buffer); + } +} + // SingleSelect typedef SingleSelectTypeOptionContext = TypeOptionContext; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart index 3de4ecd6fa..ad94f1b8eb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/builder.dart @@ -8,6 +8,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart' import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/text_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; @@ -20,6 +21,7 @@ import 'multi_select.dart'; import 'number.dart'; import 'rich_text.dart'; import 'single_select.dart'; +import 'timestamp.dart'; import 'url.dart'; typedef TypeOptionData = Uint8List; @@ -73,8 +75,6 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ ), ); case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: return DateTypeOptionWidgetBuilder( makeTypeOptionContextWithDataController( viewId: viewId, @@ -83,6 +83,16 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ ), popoverMutex, ); + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return TimestampTypeOptionWidgetBuilder( + makeTypeOptionContextWithDataController( + viewId: viewId, + fieldType: fieldType, + dataController: dataController, + ), + popoverMutex, + ); case FieldType.SingleSelect: return SingleSelectTypeOptionWidgetBuilder( makeTypeOptionContextWithDataController( @@ -203,12 +213,16 @@ TypeOptionContext dataParser: CheckboxTypeOptionWidgetDataParser(), ) as TypeOptionContext; case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: return DateTypeOptionContext( dataController: dataController, dataParser: DateTypeOptionDataParser(), ) as TypeOptionContext; + case FieldType.LastEditedTime: + case FieldType.CreatedTime: + return TimestampTypeOptionContext( + dataController: dataController, + dataParser: TimestampTypeOptionDataParser(), + ) as TypeOptionContext; case FieldType.SingleSelect: return SingleSelectTypeOptionContext( dataController: dataController, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart new file mode 100644 index 0000000000..3f506fc5b7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart @@ -0,0 +1,179 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/timestamp_bloc.dart'; +import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_option_editor.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; +import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'builder.dart'; +import 'date.dart'; + +class TimestampTypeOptionWidgetBuilder extends TypeOptionWidgetBuilder { + final TimestampTypeOptionWidget _widget; + + TimestampTypeOptionWidgetBuilder( + TimestampTypeOptionContext typeOptionContext, + PopoverMutex popoverMutex, + ) : _widget = TimestampTypeOptionWidget( + typeOptionContext: typeOptionContext, + popoverMutex: popoverMutex, + ); + + @override + Widget? build(BuildContext context) { + return _widget; + } +} + +class TimestampTypeOptionWidget extends TypeOptionWidget { + final TimestampTypeOptionContext typeOptionContext; + final PopoverMutex popoverMutex; + const TimestampTypeOptionWidget({ + required this.typeOptionContext, + required this.popoverMutex, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + TimestampTypeOptionBloc(typeOptionContext: typeOptionContext), + child: BlocConsumer( + listener: (context, state) => + typeOptionContext.typeOption = state.typeOption, + builder: (context, state) { + final List children = [ + const TypeOptionSeparator(), + _renderDateFormatButton(context, state.typeOption.dateFormat), + _renderTimeFormatButton(context, state.typeOption.timeFormat), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: IncludeTimeButton( + onChanged: (value) => context + .read() + .add(TimestampTypeOptionEvent.includeTime(!value)), + value: state.typeOption.includeTime, + ), + ), + ]; + + return ListView.separated( + shrinkWrap: true, + separatorBuilder: (context, index) { + if (index == 0) { + return const SizedBox(); + } else { + return VSpace(GridSize.typeOptionSeparatorHeight); + } + }, + itemCount: children.length, + itemBuilder: (BuildContext context, int index) => children[index], + ); + }, + ), + ); + } + + Widget _renderDateFormatButton( + BuildContext context, + DateFormatPB dataFormat, + ) { + return AppFlowyPopover( + mutex: popoverMutex, + asBarrier: true, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + constraints: BoxConstraints.loose(const Size(460, 440)), + popupBuilder: (popoverContext) { + return DateFormatList( + selectedFormat: dataFormat, + onSelected: (format) { + context + .read() + .add(TimestampTypeOptionEvent.didSelectDateFormat(format)); + PopoverContainer.of(popoverContext).close(); + }, + ); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: DateFormatButton(), + ), + ); + } + + Widget _renderTimeFormatButton( + BuildContext context, + TimeFormatPB timeFormat, + ) { + return AppFlowyPopover( + mutex: popoverMutex, + asBarrier: true, + triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(8, 0), + constraints: BoxConstraints.loose(const Size(460, 440)), + popupBuilder: (BuildContext popoverContext) { + return TimeFormatList( + selectedFormat: timeFormat, + onSelected: (format) { + context + .read() + .add(TimestampTypeOptionEvent.didSelectTimeFormat(format)); + PopoverContainer.of(popoverContext).close(); + }, + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: TimeFormatButton(timeFormat: timeFormat), + ), + ); + } +} + +class IncludeTimeButton extends StatelessWidget { + final bool value; + final Function(bool value) onChanged; + const IncludeTimeButton({ + super.key, + required this.onChanged, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: GridSize.typeOptionContentInsets, + child: Row( + children: [ + FlowySvg( + FlowySvgs.clock_alarm_s, + color: Theme.of(context).iconTheme.color, + ), + const HSpace(6), + FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()), + const Spacer(), + Toggle( + value: value, + onChanged: onChanged, + style: ToggleStyle.big, + padding: EdgeInsets.zero, + ), + ], + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart new file mode 100644 index 0000000000..4cb5ed7467 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart @@ -0,0 +1,80 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'timestamp_card_cell_bloc.freezed.dart'; + +class TimestampCardCellBloc + extends Bloc { + final TimestampCellController cellController; + void Function()? _onCellChangedFn; + + TimestampCardCellBloc({required this.cellController}) + : super(TimestampCardCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () => _startListening(), + didReceiveCellUpdate: (TimestampCellDataPB? cellData) { + emit( + state.copyWith( + data: cellData, + dateStr: cellData?.dateTime ?? "", + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((data) { + if (!isClosed) { + add(TimestampCardCellEvent.didReceiveCellUpdate(data)); + } + }), + ); + } +} + +@freezed +class TimestampCardCellEvent with _$TimestampCardCellEvent { + const factory TimestampCardCellEvent.initial() = _InitialCell; + const factory TimestampCardCellEvent.didReceiveCellUpdate( + TimestampCellDataPB? data, + ) = _DidReceiveCellUpdate; +} + +@freezed +class TimestampCardCellState with _$TimestampCardCellState { + const factory TimestampCardCellState({ + required TimestampCellDataPB? data, + required String dateStr, + required FieldInfo fieldInfo, + }) = _TimestampCardCellState; + + factory TimestampCardCellState.initial(TimestampCellController context) { + final cellData = context.getCellData(); + + return TimestampCardCellState( + fieldInfo: context.fieldInfo, + data: cellData, + dateStr: cellData?.dateTime ?? "", + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart index 3570643a5c..3ab828b1ef 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/card_cell_builder.dart @@ -10,6 +10,7 @@ import 'cells/date_card_cell.dart'; import 'cells/number_card_cell.dart'; import 'cells/select_option_card_cell.dart'; import 'cells/text_card_cell.dart'; +import 'cells/timestamp_card_cell.dart'; import 'cells/url_card_cell.dart'; // T represents as the Generic card data @@ -39,13 +40,23 @@ class CardCellBuilder { key: key, ); case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: return DateCardCell( renderHook: renderHook?.renderHook[FieldType.DateTime], cellControllerBuilder: cellControllerBuilder, key: key, ); + case FieldType.LastEditedTime: + return TimestampCardCell( + renderHook: renderHook?.renderHook[FieldType.LastEditedTime], + cellControllerBuilder: cellControllerBuilder, + key: key, + ); + case FieldType.CreatedTime: + return TimestampCardCell( + renderHook: renderHook?.renderHook[FieldType.CreatedTime], + cellControllerBuilder: cellControllerBuilder, + key: key, + ); case FieldType.SingleSelect: return SelectOptionCardCell( renderHook: renderHook?.renderHook[FieldType.SingleSelect], diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/timestamp_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/timestamp_card_cell.dart new file mode 100644 index 0000000000..b92245ece1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/timestamp_card_cell.dart @@ -0,0 +1,79 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/card/bloc/timestamp_card_cell_bloc.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../define.dart'; +import 'card_cell.dart'; + +class TimestampCardCell extends CardCell { + final CellControllerBuilder cellControllerBuilder; + final CellRenderHook? renderHook; + + const TimestampCardCell({ + required this.cellControllerBuilder, + this.renderHook, + Key? key, + }) : super(key: key); + + @override + State createState() => _TimestampCardCellState(); +} + +class _TimestampCardCellState extends State { + late TimestampCardCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as TimestampCellController; + + _cellBloc = TimestampCardCellBloc(cellController: cellController) + ..add(const TimestampCardCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + buildWhen: (previous, current) => previous.dateStr != current.dateStr, + builder: (context, state) { + if (state.dateStr.isEmpty) { + return const SizedBox.shrink(); + } + final Widget? custom = widget.renderHook?.call( + state.data, + widget.cardData, + context, + ); + if (custom != null) { + return custom; + } + + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: EdgeInsets.symmetric( + vertical: CardSizes.cardCellVPadding, + ), + child: FlowyText.regular( + state.dateStr, + fontSize: 13, + color: Theme.of(context).hintColor, + ), + ), + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index 5ff5755d91..d5a0fee160 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -11,6 +11,7 @@ import 'cells/date_cell/date_cell.dart'; import 'cells/number_cell/number_cell.dart'; import 'cells/select_option_cell/select_option_cell.dart'; import 'cells/text_cell/text_cell.dart'; +import 'cells/timestamp_cell/timestamp_cell.dart'; import 'cells/url_cell/url_cell.dart'; /// Build the cell widget in Grid style. @@ -41,14 +42,12 @@ class GridCellBuilder { cellControllerBuilder: cellControllerBuilder, key: key, style: style, - fieldType: cellContext.fieldType, ); case FieldType.LastEditedTime: case FieldType.CreatedTime: - return GridDateCell( + return GridTimestampCell( cellControllerBuilder: cellControllerBuilder, key: key, - editable: false, style: style, fieldType: cellContext.fieldType, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart index 7caa9ffe51..1eb14193ce 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_cell.dart @@ -1,5 +1,4 @@ import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; @@ -22,20 +21,12 @@ abstract class GridCellDelegate { } class GridDateCell extends GridCellWidget { - final bool editable; - - /// The [GridDateCell] is used by Field Type [FieldType.DateTime], - /// [FieldType.CreatedTime], [FieldType.LastEditedTime]. So it needs - /// to know the field type. - final FieldType fieldType; final CellControllerBuilder cellControllerBuilder; late final DateCellStyle? cellStyle; GridDateCell({ GridCellStyle? style, - required this.fieldType, required this.cellControllerBuilder, - this.editable = true, Key? key, }) : super(key: key) { if (style != null) { @@ -72,33 +63,27 @@ class _DateCellState extends GridCellState { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - Widget dateTextWidget = GridDateCellText( - dateStr: state.dateStr, - alignment: alignment, + return AppFlowyPopover( + controller: _popover, + triggerActions: PopoverTriggerFlags.none, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(260, 520)), + margin: EdgeInsets.zero, + child: GridDateCellText( + dateStr: state.dateStr, + alignment: alignment, + ), + popupBuilder: (BuildContext popoverContent) { + return DateCellEditor( + cellController: + widget.cellControllerBuilder.build() as DateCellController, + onDismissed: () => widget.onCellFocus.value = false, + ); + }, + onClose: () { + widget.onCellFocus.value = false; + }, ); - - // If the cell is editable, wrap it in a popover. - if (widget.editable) { - dateTextWidget = AppFlowyPopover( - controller: _popover, - triggerActions: PopoverTriggerFlags.none, - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(260, 520)), - margin: EdgeInsets.zero, - child: dateTextWidget, - popupBuilder: (BuildContext popoverContent) { - return DateCellEditor( - cellController: widget.cellControllerBuilder.build() - as DateCellController, - onDismissed: () => widget.onCellFocus.value = false, - ); - }, - onClose: () { - widget.onCellFocus.value = false; - }, - ); - } - return dateTextWidget; }, ), ); @@ -113,10 +98,7 @@ class _DateCellState extends GridCellState { @override void requestBeginFocus() { _popover.show(); - - if (widget.editable) { - widget.onCellFocus.value = true; - } + widget.onCellFocus.value = true; } @override diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart index adef7e9aec..1668f7b266 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart @@ -2,8 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database_view/application/field/type_option/type_option_context.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; -import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/type_option/timestamp.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart'; @@ -241,30 +240,11 @@ class _IncludeTimeButton extends StatelessWidget { builder: (context, includeTime) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: Padding( - padding: GridSize.typeOptionContentInsets, - child: Row( - children: [ - FlowySvg( - FlowySvgs.clock_alarm_s, - color: Theme.of(context).iconTheme.color, - ), - const HSpace(6), - FlowyText.medium(LocaleKeys.grid_field_includeTime.tr()), - const Spacer(), - Toggle( - value: includeTime, - onChanged: (value) => context - .read() - .add(DateCellCalendarEvent.setIncludeTime(!value)), - style: ToggleStyle.big, - padding: EdgeInsets.zero, - ), - ], - ), - ), + child: IncludeTimeButton( + onChanged: (value) => context + .read() + .add(DateCellCalendarEvent.setIncludeTime(!value)), + value: includeTime, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart new file mode 100644 index 0000000000..7f155c6eae --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart @@ -0,0 +1,109 @@ +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cell_builder.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class TimestampCellStyle extends GridCellStyle { + Alignment alignment; + + TimestampCellStyle({this.alignment = Alignment.center}); +} + +class GridTimestampCell extends GridCellWidget { + /// The [GridTimestampCell] is used by both [FieldType.CreatedTime] + /// and [FieldType.LastEditedTime]. So it needs to know the field type. + final FieldType fieldType; + final CellControllerBuilder cellControllerBuilder; + late final TimestampCellStyle? cellStyle; + + GridTimestampCell({ + GridCellStyle? style, + required this.fieldType, + required this.cellControllerBuilder, + Key? key, + }) : super(key: key) { + if (style != null) { + cellStyle = (style as TimestampCellStyle); + } else { + cellStyle = null; + } + } + + @override + GridCellState createState() => _TimestampCellState(); +} + +class _TimestampCellState extends GridCellState { + late TimestampCellBloc _cellBloc; + + @override + void initState() { + final cellController = + widget.cellControllerBuilder.build() as TimestampCellController; + _cellBloc = TimestampCellBloc(cellController: cellController) + ..add(const TimestampCellEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final alignment = widget.cellStyle != null + ? widget.cellStyle!.alignment + : Alignment.centerLeft; + return BlocProvider.value( + value: _cellBloc, + child: BlocBuilder( + builder: (context, state) { + return GridTimestampCellText( + dateStr: state.dateStr, + alignment: alignment, + ); + }, + ), + ); + } + + @override + Future dispose() async { + _cellBloc.close(); + super.dispose(); + } + + @override + String? onCopy() => _cellBloc.state.dateStr; + + @override + void requestBeginFocus() { + return; + } +} + +class GridTimestampCellText extends StatelessWidget { + final String dateStr; + final Alignment alignment; + const GridTimestampCellText({ + required this.dateStr, + required this.alignment, + super.key, + }); + + @override + Widget build(BuildContext context) { + return SizedBox.expand( + child: Align( + alignment: alignment, + child: Padding( + padding: GridSize.cellContentInsets, + child: FlowyText.medium( + dateStr, + maxLines: null, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart new file mode 100644 index 0000000000..3c0af66d06 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell_bloc.dart @@ -0,0 +1,80 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database_view/application/field/field_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/timestamp_entities.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'timestamp_cell_bloc.freezed.dart'; + +class TimestampCellBloc extends Bloc { + final TimestampCellController cellController; + void Function()? _onCellChangedFn; + + TimestampCellBloc({required this.cellController}) + : super(TimestampCellState.initial(cellController)) { + on( + (event, emit) async { + event.when( + initial: () => _startListening(), + didReceiveCellUpdate: (TimestampCellDataPB? cellData) { + emit( + state.copyWith( + data: cellData, + dateStr: cellData?.dateTime ?? "", + ), + ); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + + void _startListening() { + _onCellChangedFn = cellController.startListening( + onCellChanged: ((data) { + if (!isClosed) { + add(TimestampCellEvent.didReceiveCellUpdate(data)); + } + }), + ); + } +} + +@freezed +class TimestampCellEvent with _$TimestampCellEvent { + const factory TimestampCellEvent.initial() = _InitialCell; + const factory TimestampCellEvent.didReceiveCellUpdate( + TimestampCellDataPB? data, + ) = _DidReceiveCellUpdate; +} + +@freezed +class TimestampCellState with _$TimestampCellState { + const factory TimestampCellState({ + required TimestampCellDataPB? data, + required String dateStr, + required FieldInfo fieldInfo, + }) = _TimestampCellState; + + factory TimestampCellState.initial(TimestampCellController context) { + final cellData = context.getCellData(); + + return TimestampCellState( + fieldInfo: context.fieldInfo, + data: cellData, + dateStr: cellData?.dateTime ?? "", + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart index 738d9c84bf..e3dd7f3f04 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -18,6 +18,7 @@ import 'cell_builder.dart'; import 'cells/date_cell/date_cell.dart'; import 'cells/select_option_cell/select_option_cell.dart'; import 'cells/text_cell/text_cell.dart'; +import 'cells/timestamp_cell/timestamp_cell.dart'; import 'cells/url_cell/url_cell.dart'; /// Display the row properties in a list. Only use this widget in the @@ -156,9 +157,12 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { case FieldType.Checkbox: return null; case FieldType.DateTime: + return DateCellStyle( + alignment: Alignment.centerLeft, + ); case FieldType.LastEditedTime: case FieldType.CreatedTime: - return DateCellStyle( + return TimestampCellStyle( alignment: Alignment.centerLeft, ); case FieldType.MultiSelect: diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs index 67ff1f19b9..bed6cfc5f8 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/date_entities.rs @@ -4,7 +4,7 @@ use strum_macros::EnumIter; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use crate::entities::{CellIdPB, FieldType}; +use crate::entities::CellIdPB; use crate::services::field::{DateFormat, DateTypeOption, TimeFormat}; #[derive(Clone, Debug, Default, ProtoBuf)] @@ -51,9 +51,6 @@ pub struct DateTypeOptionPB { #[pb(index = 3)] pub timezone_id: String, - - #[pb(index = 4)] - pub field_type: FieldType, } impl From for DateTypeOptionPB { @@ -62,7 +59,6 @@ impl From for DateTypeOptionPB { date_format: data.date_format.into(), time_format: data.time_format.into(), timezone_id: data.timezone_id, - field_type: data.field_type, } } } @@ -73,7 +69,6 @@ impl From for DateTypeOption { date_format: data.date_format.into(), time_format: data.time_format.into(), timezone_id: data.timezone_id, - field_type: data.field_type, } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs index 15691cbc73..2340f4d63a 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/mod.rs @@ -4,6 +4,7 @@ mod date_entities; mod number_entities; mod select_option; mod text_entities; +mod timestamp_entities; mod url_entities; pub use checkbox_entities::*; @@ -12,4 +13,5 @@ pub use date_entities::*; pub use number_entities::*; pub use select_option::*; pub use text_entities::*; +pub use timestamp_entities::*; pub use url_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs new file mode 100644 index 0000000000..b4afcadaf4 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/timestamp_entities.rs @@ -0,0 +1,50 @@ +use flowy_derive::ProtoBuf; + +use crate::entities::{DateFormatPB, FieldType, TimeFormatPB}; +use crate::services::field::TimestampTypeOption; + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct TimestampCellDataPB { + #[pb(index = 1)] + pub date_time: String, + + #[pb(index = 2, one_of)] + pub timestamp: Option, +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct TimestampTypeOptionPB { + #[pb(index = 1)] + pub date_format: DateFormatPB, + + #[pb(index = 2)] + pub time_format: TimeFormatPB, + + #[pb(index = 3)] + pub include_time: bool, + + #[pb(index = 4)] + pub field_type: FieldType, +} + +impl From for TimestampTypeOptionPB { + fn from(data: TimestampTypeOption) -> Self { + Self { + date_format: data.date_format.into(), + time_format: data.time_format.into(), + include_time: data.include_time, + field_type: data.field_type, + } + } +} + +impl From for TimestampTypeOption { + fn from(data: TimestampTypeOptionPB) -> Self { + Self { + date_format: data.date_format.into(), + time_format: data.time_format.into(), + include_time: data.include_time, + field_type: data.field_type, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 4416ad8f2d..01098e6431 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -24,8 +24,8 @@ use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, Data use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData}; use crate::services::field::{ default_type_option_data_from_type, select_type_option_from_field, transform_type_option, - type_option_data_from_pb_or_default, type_option_to_pb, DateCellData, SelectOptionCellChangeset, - SelectOptionIds, TypeOptionCellDataHandler, TypeOptionCellExt, + type_option_data_from_pb_or_default, type_option_to_pb, SelectOptionCellChangeset, + SelectOptionIds, TimestampCellData, TypeOptionCellDataHandler, TypeOptionCellExt, }; use crate::services::field_settings::{ default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams, @@ -614,9 +614,9 @@ impl DatabaseEditor { FieldType::LastEditedTime | FieldType::CreatedTime => { let row = database.get_row(row_id); let cell_data = if field_type.is_created_time() { - DateCellData::new(row.created_at, true) + TimestampCellData::new(row.created_at) } else { - DateCellData::new(row.modified_at, true) + TimestampCellData::new(row.modified_at) }; Some(Cell::from(cell_data)) }, @@ -651,9 +651,9 @@ impl DatabaseEditor { .into_iter() .map(|row| { let data = if field_type.is_created_time() { - DateCellData::new(row.created_at, true) + TimestampCellData::new(row.created_at) } else { - DateCellData::new(row.modified_at, true) + TimestampCellData::new(row.modified_at) }; RowCell { row_id: row.id, diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs index 7caf141f62..3df61c1dfd 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_tests.rs @@ -10,7 +10,6 @@ mod tests { use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::field::{ DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, TimeFormat, - TypeOptionCellDataSerde, }; #[test] @@ -409,33 +408,18 @@ mod tests { old_cell_data: Option, expected_str: &str, ) { - let (cell, cell_data) = type_option + let (cell, _) = type_option .apply_changeset(changeset, old_cell_data) .unwrap(); - assert_eq!( - decode_cell_data(&cell, type_option, cell_data.include_time, field), - expected_str, - ); + assert_eq!(decode_cell_data(&cell, type_option, field), expected_str,); } - fn decode_cell_data( - cell: &Cell, - type_option: &DateTypeOption, - include_time: bool, - field: &Field, - ) -> String { + fn decode_cell_data(cell: &Cell, type_option: &DateTypeOption, field: &Field) -> String { let decoded_data = type_option .decode_cell(cell, &FieldType::DateTime, field) .unwrap(); - let decoded_data = type_option.protobuf_encode(decoded_data); - if include_time { - format!("{} {}", decoded_data.date, decoded_data.time) - .trim_end() - .to_owned() - } else { - decoded_data.date - } + type_option.stringify_cell_data(decoded_data) } fn initialize_date_cell(type_option: &DateTypeOption, changeset: DateCellChangeset) -> Cell { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs index 7d03368c80..ced65b1640 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -13,8 +13,8 @@ use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use crate::entities::{DateCellDataPB, DateFilterPB, FieldType}; use crate::services::cell::{CellDataChangeset, CellDataDecoder}; use crate::services::field::{ - default_order, DateCellChangeset, DateCellData, DateCellDataWrapper, DateFormat, TimeFormat, - TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, + default_order, DateCellChangeset, DateCellData, DateFormat, TimeFormat, TypeOption, + TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, }; use crate::services::sort::SortCondition; @@ -27,7 +27,6 @@ pub struct DateTypeOption { pub date_format: DateFormat, pub time_format: TimeFormat, pub timezone_id: String, - pub field_type: FieldType, } impl Default for DateTypeOption { @@ -36,7 +35,6 @@ impl Default for DateTypeOption { date_format: Default::default(), time_format: Default::default(), timezone_id: Default::default(), - field_type: FieldType::DateTime, } } } @@ -59,15 +57,10 @@ impl From for DateTypeOption { .map(TimeFormat::from) .unwrap_or_default(); let timezone_id = data.get_str_value("timezone_id").unwrap_or_default(); - let field_type = data - .get_i64_value("field_type") - .map(FieldType::from) - .unwrap_or(FieldType::DateTime); Self { date_format, time_format, timezone_id, - field_type, } } } @@ -78,7 +71,6 @@ impl From for TypeOptionData { .insert_i64_value("date_format", data.date_format.value()) .insert_i64_value("time_format", data.time_format.value()) .insert_str_value("timezone_id", data.timezone_id) - .insert_i64_value("field_type", data.field_type.value()) .build() } } @@ -88,7 +80,16 @@ impl TypeOptionCellDataSerde for DateTypeOption { &self, cell_data: ::CellData, ) -> ::CellProtobufType { - self.today_desc_from_timestamp(cell_data) + let timestamp = cell_data.timestamp; + let include_time = cell_data.include_time; + let (date, time) = self.formatted_date_time_from_timestamp(×tamp); + + DateCellDataPB { + date, + time, + timestamp: timestamp.unwrap_or_default(), + include_time, + } } fn parse_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { @@ -97,46 +98,50 @@ impl TypeOptionCellDataSerde for DateTypeOption { } impl DateTypeOption { - pub fn new(field_type: FieldType) -> Self { - Self { - field_type, - ..Default::default() - } + pub fn new() -> Self { + Self::default() } pub fn test() -> Self { Self { timezone_id: "Etc/UTC".to_owned(), - field_type: FieldType::DateTime, ..Self::default() } } - 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; + fn formatted_date_time_from_timestamp(&self, timestamp: &Option) -> (String, String) { + if let Some(timestamp) = timestamp { + let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap(); + let offset = self.get_timezone_offset(naive); + let date_time = DateTime::::from_utc(naive, offset); - let (date, time) = match cell_data.timestamp { - Some(timestamp) => { - let naive = chrono::NaiveDateTime::from_timestamp_opt(timestamp, 0).unwrap(); - let offset = self.get_timezone_offset(naive); - let date_time = DateTime::::from_utc(naive, offset); + let fmt = self.date_format.format_str(); + let date = format!("{}", date_time.format(fmt)); + let fmt = self.time_format.format_str(); + let time = format!("{}", date_time.format(fmt)); + (date, time) + } else { + ("".to_owned(), "".to_owned()) + } + } - let fmt = self.date_format.format_str(); - let date = format!("{}", date_time.format(fmt)); - let fmt = self.time_format.format_str(); - let time = format!("{}", date_time.format(fmt)); - - (date, time) + fn naive_time_from_time_string( + &self, + include_time: bool, + time_str: Option, + ) -> FlowyResult> { + match (include_time, time_str) { + (true, Some(time_str)) => { + let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str()); + match result { + Ok(time) => Ok(Some(time)), + Err(_e) => { + let msg = format!("Parse {} failed", time_str); + Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, msg)) + }, + } }, - None => ("".to_owned(), "".to_owned()), - }; - - DateCellDataPB { - date, - time, - include_time, - timestamp, + _ => Ok(None), } } @@ -211,7 +216,14 @@ impl CellDataDecoder for DateTypeOption { } fn stringify_cell_data(&self, cell_data: ::CellData) -> String { - self.today_desc_from_timestamp(cell_data).date + let timestamp = cell_data.timestamp; + let include_time = cell_data.include_time; + let (date_string, time_string) = self.formatted_date_time_from_timestamp(×tamp); + if include_time && timestamp.is_some() { + format!("{} {}", date_string, time_string) + } else { + date_string + } } fn stringify_cell(&self, cell: &Cell) -> String { @@ -236,15 +248,12 @@ impl CellDataChangeset for DateTypeOption { }; if changeset.clear_flag == Some(true) { - let (timestamp, include_time) = (None, include_time); - let cell_data = DateCellData { - timestamp, + timestamp: None, include_time, }; - let cell_wrapper: DateCellDataWrapper = (self.field_type.clone(), cell_data.clone()).into(); - return Ok((Cell::from(cell_wrapper), cell_data)); + return Ok((Cell::from(&cell_data), cell_data)); } // update include_time if necessary @@ -256,27 +265,12 @@ impl CellDataChangeset for DateTypeOption { // order to change the day without changing the time, the old time string // should be passed in as well. - let changeset_timestamp = changeset.date; - - // parse the time string, which is in the local timezone - let parsed_time = match (include_time, changeset.time) { - (true, Some(time_str)) => { - let result = NaiveTime::parse_from_str(&time_str, self.time_format.format_str()); - match result { - Ok(time) => Ok(Some(time)), - Err(_e) => { - let msg = format!("Parse {} failed", time_str); - Err(FlowyError::new(ErrorCode::InvalidDateTimeFormat, msg)) - }, - } - }, - _ => Ok(None), - }?; + let parsed_time = self.naive_time_from_time_string(include_time, changeset.time)?; let timestamp = self.timestamp_from_parsed_time_previous_and_new_timestamp( parsed_time, previous_timestamp, - changeset_timestamp, + changeset.date, ); let cell_data = DateCellData { @@ -284,8 +278,7 @@ impl CellDataChangeset for DateTypeOption { include_time, }; - let cell_wrapper: DateCellDataWrapper = (self.field_type.clone(), cell_data.clone()).into(); - Ok((Cell::from(cell_wrapper), cell_data)) + Ok((Cell::from(&cell_data), cell_data)) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs index e6bfb4c717..ab55be3b36 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option_entities.rs @@ -83,42 +83,19 @@ impl From<&DateCellDataPB> for DateCellData { } } -/// Wrapper for DateCellData that also contains the field type. -/// Handy struct to use when you need to convert a DateCellData to a Cell. -pub struct DateCellDataWrapper { - data: DateCellData, - field_type: FieldType, -} - -impl From<(FieldType, DateCellData)> for DateCellDataWrapper { - fn from((field_type, data): (FieldType, DateCellData)) -> Self { - Self { data, field_type } - } -} - -impl From for Cell { - fn from(wrapper: DateCellDataWrapper) -> Self { - let (field_type, data) = (wrapper.field_type, wrapper.data); - let timestamp_string = match data.timestamp { +impl From<&DateCellData> for Cell { + fn from(cell_data: &DateCellData) -> Self { + let timestamp_string = match cell_data.timestamp { Some(timestamp) => timestamp.to_string(), None => "".to_owned(), }; - // Most of the case, don't use these keys in other places. Otherwise, we should define - // constants for them. - new_cell_builder(field_type) + new_cell_builder(FieldType::DateTime) .insert_str_value(CELL_DATA, timestamp_string) - .insert_bool_value("include_time", data.include_time) + .insert_bool_value("include_time", cell_data.include_time) .build() } } -impl From for Cell { - fn from(data: DateCellData) -> Self { - let data: DateCellDataWrapper = (FieldType::DateTime, data).into(); - Cell::from(data) - } -} - impl<'de> serde::Deserialize<'de> for DateCellData { fn deserialize(deserializer: D) -> core::result::Result where diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs index ec788a6a43..46ef5470b4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/mod.rs @@ -4,6 +4,7 @@ pub mod date_type_option; pub mod number_type_option; pub mod selection_type_option; pub mod text_type_option; +pub mod timestamp_type_option; mod type_option; mod type_option_cell; mod url_type_option; @@ -14,6 +15,7 @@ pub use date_type_option::*; pub use number_type_option::*; pub use selection_type_option::*; pub use text_type_option::*; +pub use timestamp_type_option::*; pub use type_option::*; pub use type_option_cell::*; pub use url_type_option::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs index 1829f9d4fd..cccc5f84ab 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_tests.rs @@ -30,8 +30,8 @@ mod tests { }; assert_eq!( - stringify_cell_data(&data.into(), &FieldType::RichText, &field_type, &field), - "Mar 14, 2022" + stringify_cell_data(&(&data).into(), &FieldType::RichText, &field_type, &field), + "Mar 14, 2022 09:56" ); } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index acf9f7dd97..0e8f48302c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -238,14 +238,14 @@ impl TypeOptionCellData for StrCellData { impl From<&Cell> for StrCellData { fn from(cell: &Cell) -> Self { - Self(cell.get_str_value("data").unwrap_or_default()) + Self(cell.get_str_value(CELL_DATA).unwrap_or_default()) } } impl From for Cell { fn from(data: StrCellData) -> Self { new_cell_builder(FieldType::RichText) - .insert_str_value("data", data.0) + .insert_str_value(CELL_DATA, data.0) .build() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs new file mode 100644 index 0000000000..3041a7947b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/mod.rs @@ -0,0 +1,6 @@ +#![allow(clippy::module_inception)] +mod timestamp_type_option; +mod timestamp_type_option_entities; + +pub use timestamp_type_option::*; +pub use timestamp_type_option_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs new file mode 100644 index 0000000000..c27816052f --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs @@ -0,0 +1,205 @@ +use std::cmp::Ordering; + +use chrono::{DateTime, Local, Offset}; +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::{Field, TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use serde::{Deserialize, Serialize}; + +use crate::entities::{DateFilterPB, FieldType, TimestampCellDataPB}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::{ + default_order, DateFormat, TimeFormat, TimestampCellData, TypeOption, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, +}; +use crate::services::sort::SortCondition; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TimestampTypeOption { + pub date_format: DateFormat, + pub time_format: TimeFormat, + pub include_time: bool, + pub field_type: FieldType, +} + +impl Default for TimestampTypeOption { + fn default() -> Self { + Self { + date_format: Default::default(), + time_format: Default::default(), + include_time: true, + field_type: FieldType::LastEditedTime, + } + } +} + +impl TypeOption for TimestampTypeOption { + type CellData = TimestampCellData; + type CellChangeset = String; + type CellProtobufType = TimestampCellDataPB; + type CellFilter = DateFilterPB; +} + +impl From for TimestampTypeOption { + fn from(data: TypeOptionData) -> Self { + let date_format = data + .get_i64_value("date_format") + .map(DateFormat::from) + .unwrap_or_default(); + let time_format = data + .get_i64_value("time_format") + .map(TimeFormat::from) + .unwrap_or_default(); + let include_time = data.get_bool_value("include_time").unwrap_or_default(); + let field_type = data + .get_i64_value("field_type") + .map(FieldType::from) + .unwrap_or(FieldType::LastEditedTime); + Self { + date_format, + time_format, + include_time, + field_type, + } + } +} + +impl From for TypeOptionData { + fn from(option: TimestampTypeOption) -> Self { + TypeOptionDataBuilder::new() + .insert_i64_value("date_format", option.date_format.value()) + .insert_i64_value("time_format", option.time_format.value()) + .insert_bool_value("include_time", option.include_time) + .insert_i64_value("field_type", option.field_type.value()) + .build() + } +} + +impl TypeOptionCellDataSerde for TimestampTypeOption { + fn protobuf_encode( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + let timestamp = cell_data.timestamp; + let date_time = self.stringify_cell_data(cell_data); + + TimestampCellDataPB { + date_time, + timestamp, + } + } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(TimestampCellData::from(cell)) + } +} + +impl TimestampTypeOption { + pub fn new(field_type: FieldType) -> Self { + Self { + field_type, + include_time: true, + ..Default::default() + } + } + + fn formatted_date_time_from_timestamp(&self, timestamp: &Option) -> (String, String) { + if let Some(timestamp) = timestamp { + let naive = chrono::NaiveDateTime::from_timestamp_opt(*timestamp, 0).unwrap(); + let offset = Local::now().offset().fix(); + let date_time = DateTime::::from_utc(naive, offset); + + let fmt = self.date_format.format_str(); + let date = format!("{}", date_time.format(fmt)); + let fmt = self.time_format.format_str(); + let time = format!("{}", date_time.format(fmt)); + (date, time) + } else { + ("".to_owned(), "".to_owned()) + } + } +} + +impl TypeOptionTransform for TimestampTypeOption {} + +impl CellDataDecoder for TimestampTypeOption { + fn decode_cell( + &self, + cell: &Cell, + decoded_field_type: &FieldType, + _field: &Field, + ) -> FlowyResult<::CellData> { + // Return default data if the type_option_cell_data is not FieldType::DateTime. + // It happens when switching from one field to another. + // For example: + // FieldType::RichText -> FieldType::DateTime, it will display empty content on the screen. + if !decoded_field_type.is_date() { + return Ok(Default::default()); + } + + self.parse_cell(cell) + } + + fn stringify_cell_data(&self, cell_data: ::CellData) -> String { + let timestamp = cell_data.timestamp; + let (date_string, time_string) = self.formatted_date_time_from_timestamp(×tamp); + if self.include_time { + format!("{} {}", date_string, time_string) + } else { + date_string + } + } + + fn stringify_cell(&self, cell: &Cell) -> String { + let cell_data = Self::CellData::from(cell); + self.stringify_cell_data(cell_data) + } +} + +impl CellDataChangeset for TimestampTypeOption { + fn apply_changeset( + &self, + _changeset: ::CellChangeset, + _cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + Err(FlowyError::new( + ErrorCode::FieldInvalidOperation, + "Cells of this field type cannot be edited", + )) + } +} + +impl TypeOptionCellDataFilter for TimestampTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + field_type: &FieldType, + cell_data: &::CellData, + ) -> bool { + if !field_type.is_date() { + return true; + } + + filter.is_visible(cell_data.timestamp) + } +} + +impl TypeOptionCellDataCompare for TimestampTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + sort_condition: SortCondition, + ) -> Ordering { + match (cell_data.timestamp, other_cell_data.timestamp) { + (Some(left), Some(right)) => { + let order = left.cmp(&right); + sort_condition.evaluate_order(order) + }, + (Some(_), None) => Ordering::Less, + (None, Some(_)) => Ordering::Greater, + (None, None) => default_order(), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs new file mode 100644 index 0000000000..307b7637b8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option_entities.rs @@ -0,0 +1,69 @@ +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; +use serde::Serialize; + +use crate::{ + entities::FieldType, + services::field::{TypeOptionCellData, CELL_DATA}, +}; + +#[derive(Clone, Debug, Default, Serialize)] +pub struct TimestampCellData { + pub timestamp: Option, +} + +impl TimestampCellData { + pub fn new(timestamp: i64) -> Self { + Self { + timestamp: Some(timestamp), + } + } +} + +impl From<&Cell> for TimestampCellData { + fn from(cell: &Cell) -> Self { + let timestamp = cell + .get_str_value(CELL_DATA) + .and_then(|data| data.parse::().ok()); + Self { timestamp } + } +} + +/// Wrapper for DateCellData that also contains the field type. +/// Handy struct to use when you need to convert a DateCellData to a Cell. +pub struct TimestampCellDataWrapper { + data: TimestampCellData, + field_type: FieldType, +} + +impl From<(FieldType, TimestampCellData)> for TimestampCellDataWrapper { + fn from((field_type, data): (FieldType, TimestampCellData)) -> Self { + Self { data, field_type } + } +} + +impl From for Cell { + fn from(wrapper: TimestampCellDataWrapper) -> Self { + let (field_type, data) = (wrapper.field_type, wrapper.data); + let timestamp_string = data.timestamp.unwrap_or_default(); + + new_cell_builder(field_type) + .insert_str_value(CELL_DATA, timestamp_string) + .build() + } +} + +impl From for Cell { + fn from(data: TimestampCellData) -> Self { + let data: TimestampCellDataWrapper = (FieldType::LastEditedTime, data).into(); + Cell::from(data) + } +} + +impl TypeOptionCellData for TimestampCellData {} + +impl ToString for TimestampCellData { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index 7fcd10c0c9..59cb8fa14e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -11,13 +11,13 @@ use flowy_error::FlowyResult; use crate::entities::{ CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType, MultiSelectTypeOptionPB, NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, - URLTypeOptionPB, + TimestampTypeOptionPB, URLTypeOptionPB, }; use crate::services::cell::{CellDataDecoder, FromCellChangeset, ToCellChangeset}; use crate::services::field::checklist_type_option::ChecklistTypeOption; use crate::services::field::{ CheckboxTypeOption, DateFormat, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, - RichTextTypeOption, SingleSelectTypeOption, TimeFormat, URLTypeOption, + RichTextTypeOption, SingleSelectTypeOption, TimeFormat, TimestampTypeOption, URLTypeOption, }; use crate::services::filter::FromFilterString; use crate::services::sort::SortCondition; @@ -179,9 +179,12 @@ pub fn type_option_data_from_pb_or_default>( FieldType::Number => { NumberTypeOptionPB::try_from(bytes).map(|pb| NumberTypeOption::from(pb).into()) }, - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { DateTypeOptionPB::try_from(bytes).map(|pb| DateTypeOption::from(pb).into()) }, + FieldType::LastEditedTime | FieldType::CreatedTime => { + TimestampTypeOptionPB::try_from(bytes).map(|pb| TimestampTypeOption::from(pb).into()) + }, FieldType::SingleSelect => { SingleSelectTypeOptionPB::try_from(bytes).map(|pb| SingleSelectTypeOption::from(pb).into()) }, @@ -214,10 +217,16 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> .try_into() .unwrap() }, - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { let date_type_option: DateTypeOption = type_option.into(); DateTypeOptionPB::from(date_type_option).try_into().unwrap() }, + FieldType::LastEditedTime | FieldType::CreatedTime => { + let timestamp_type_option: TimestampTypeOption = type_option.into(); + TimestampTypeOptionPB::from(timestamp_type_option) + .try_into() + .unwrap() + }, FieldType::SingleSelect => { let single_select_type_option: SingleSelectTypeOption = type_option.into(); SingleSelectTypeOptionPB::from(single_select_type_option) @@ -254,14 +263,14 @@ pub fn default_type_option_data_from_type(field_type: &FieldType) -> TypeOptionD FieldType::RichText => RichTextTypeOption::default().into(), FieldType::Number => NumberTypeOption::default().into(), FieldType::DateTime => DateTypeOption { - field_type: field_type.clone(), ..Default::default() } .into(), - FieldType::LastEditedTime | FieldType::CreatedTime => DateTypeOption { + FieldType::LastEditedTime | FieldType::CreatedTime => TimestampTypeOption { field_type: field_type.clone(), date_format: DateFormat::Friendly, time_format: TimeFormat::TwelveHour, + include_time: true, ..Default::default() } .into(), diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index b9a2a276f7..52b7db8751 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -16,8 +16,8 @@ use crate::services::cell::{ use crate::services::field::checklist_type_option::ChecklistTypeOption; use crate::services::field::{ CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RichTextTypeOption, - SingleSelectTypeOption, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, - TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption, + SingleSelectTypeOption, TimestampTypeOption, TypeOption, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption, }; use crate::services::sort::SortCondition; @@ -407,7 +407,7 @@ impl<'a> TypeOptionCellExt<'a> { self.cell_data_cache.clone(), ) }), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => self + FieldType::DateTime => self .field .get_type_option::(field_type) .map(|type_option| { @@ -417,6 +417,16 @@ impl<'a> TypeOptionCellExt<'a> { self.cell_data_cache.clone(), ) }), + FieldType::LastEditedTime | FieldType::CreatedTime => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + self.cell_filter_cache.clone(), + self.cell_data_cache.clone(), + ) + }), FieldType::SingleSelect => self .field .get_type_option::(field_type) @@ -527,9 +537,12 @@ fn get_type_option_transform_handler( FieldType::Number => { Box::new(NumberTypeOption::from(type_option_data)) as Box }, - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { Box::new(DateTypeOption::from(type_option_data)) as Box }, + FieldType::LastEditedTime | FieldType::CreatedTime => { + Box::new(TimestampTypeOption::from(type_option_data)) as Box + }, FieldType::SingleSelect => Box::new(SingleSelectTypeOption::from(type_option_data)) as Box, FieldType::MultiSelect => { @@ -590,6 +603,10 @@ impl RowSingleCellData { into_date_field_cell_data, ::CellData ); + into_cell_data!( + into_timestamp_field_cell_data, + ::CellData + ); into_cell_data!( into_check_list_field_cell_data, ::CellData diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index 349d0f7c63..0b24ed65d2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -455,7 +455,6 @@ mod tests { use chrono::{offset, Days, Duration, NaiveDateTime}; - use crate::entities::FieldType; use crate::services::{ field::{date_type_option::DateTypeOption, DateCellData}, group::controller_impls::date_controller::{ @@ -481,9 +480,9 @@ mod tests { let today = offset::Local::now(); let three_days_before = today.checked_add_signed(Duration::days(-3)).unwrap(); - let mut local_date_type_option = DateTypeOption::new(FieldType::DateTime); + let mut local_date_type_option = DateTypeOption::new(); local_date_type_option.timezone_id = today.offset().to_string(); - let mut default_date_type_option = DateTypeOption::new(FieldType::DateTime); + let mut default_date_type_option = DateTypeOption::new(); default_date_type_option.timezone_id = "".to_string(); let tests = vec![ diff --git a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs index ab3be4dcc5..ea1bd80328 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/cell_test/test.rs @@ -22,12 +22,13 @@ async fn grid_cell_update() { for (_, row_detail) in rows.iter().enumerate() { for field in &fields { let field_type = FieldType::from(field.field_type); + if field_type == FieldType::LastEditedTime || field_type == FieldType::CreatedTime { + continue; + } let cell_changeset = match field_type { FieldType::RichText => "".to_string(), FieldType::Number => "123".to_string(), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { - make_date_cell_string(123) - }, + FieldType::DateTime => make_date_cell_string(123), FieldType::SingleSelect => { let type_option = field .get_type_option::(field.field_type) @@ -49,6 +50,7 @@ async fn grid_cell_update() { .to_cell_changeset_str(), FieldType::Checkbox => "1".to_string(), FieldType::URL => "1".to_string(), + _ => "".to_string(), }; scripts.push(UpdateCell { diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs index 22837bd9a5..6b95c1db3d 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs @@ -31,7 +31,7 @@ async fn grid_create_field() { ]; test.run_scripts(scripts).await; - let (params, field) = create_date_field(&test.view_id(), FieldType::CreatedTime); + let (params, field) = create_timestamp_field(&test.view_id(), FieldType::CreatedTime); let scripts = vec![ CreateField { params }, AssertFieldTypeOptionEqual { diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs index 7ec748eb9d..383c8bab5e 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/util.rs @@ -2,7 +2,7 @@ use collab_database::fields::Field; use flowy_database2::entities::{CreateFieldParams, FieldType}; use flowy_database2::services::field::{ type_option_to_pb, DateCellChangeset, DateFormat, DateTypeOption, FieldBuilder, - RichTextTypeOption, SelectOption, SingleSelectTypeOption, TimeFormat, + RichTextTypeOption, SelectOption, SingleSelectTypeOption, TimeFormat, TimestampTypeOption, }; pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) { @@ -41,32 +41,52 @@ pub fn create_single_select_field(grid_id: &str) -> (CreateFieldParams, Field) { }; (params, single_select_field) } - -pub fn create_date_field(grid_id: &str, field_type: FieldType) -> (CreateFieldParams, Field) { +#[allow(dead_code)] +pub fn create_date_field(grid_id: &str) -> (CreateFieldParams, Field) { let date_type_option = DateTypeOption { date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, timezone_id: "Etc/UTC".to_owned(), + }; + + let field = FieldBuilder::new(FieldType::DateTime, date_type_option.clone()) + .name("Date") + .visibility(true) + .build(); + + let type_option_data = type_option_to_pb(date_type_option.into(), &FieldType::DateTime).to_vec(); + + let params = CreateFieldParams { + view_id: grid_id.to_owned(), + field_type: FieldType::DateTime, + type_option_data: Some(type_option_data), + }; + (params, field) +} + +pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFieldParams, Field) { + let timestamp_type_option = TimestampTypeOption { + date_format: DateFormat::US, + time_format: TimeFormat::TwentyFourHour, + include_time: true, field_type: field_type.clone(), }; let field: Field = match field_type { - FieldType::DateTime => FieldBuilder::new(field_type.clone(), date_type_option.clone()) - .name("Date") - .visibility(true) - .build(), - FieldType::LastEditedTime => FieldBuilder::new(field_type.clone(), date_type_option.clone()) - .name("Updated At") - .visibility(true) - .build(), - FieldType::CreatedTime => FieldBuilder::new(field_type.clone(), date_type_option.clone()) + FieldType::LastEditedTime => { + FieldBuilder::new(field_type.clone(), timestamp_type_option.clone()) + .name("Updated At") + .visibility(true) + .build() + }, + FieldType::CreatedTime => FieldBuilder::new(field_type.clone(), timestamp_type_option.clone()) .name("Created At") .visibility(true) .build(), _ => panic!("Unsupported group field type"), }; - let type_option_data = type_option_to_pb(date_type_option.into(), &field_type).to_vec(); + let type_option_data = type_option_to_pb(timestamp_type_option.into(), &field_type).to_vec(); let params = CreateFieldParams { view_id: grid_id.to_owned(), diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs index 3378bdb1bc..a6fd10cce7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/board_mock_data.rs @@ -7,7 +7,7 @@ use flowy_database2::entities::FieldType; use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption; use flowy_database2::services::field::{ DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, SelectOption, SelectOptionColor, - SingleSelectTypeOption, TimeFormat, + SingleSelectTypeOption, TimeFormat, TimestampTypeOption, }; use crate::database::database_editor::TestRowBuilder; @@ -36,17 +36,30 @@ pub fn make_test_board() -> DatabaseData { .build(); fields.push(number_field); }, - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { // Date let date_type_option = DateTypeOption { date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, timezone_id: "Etc/UTC".to_owned(), + }; + let name = "Time"; + let date_field = FieldBuilder::new(field_type.clone(), date_type_option) + .name(name) + .visibility(true) + .build(); + fields.push(date_field); + }, + FieldType::LastEditedTime | FieldType::CreatedTime => { + // LastEditedTime and CreatedTime + let date_type_option = TimestampTypeOption { + date_format: DateFormat::US, + time_format: TimeFormat::TwentyFourHour, + include_time: true, field_type: field_type.clone(), }; let name = match field_type { - FieldType::DateTime => "Time", - FieldType::LastEditedTime => "Updated At", + FieldType::LastEditedTime => "Last Modified", FieldType::CreatedTime => "Created At", _ => "", }; @@ -128,7 +141,7 @@ pub fn make_test_board() -> DatabaseData { FieldType::RichText => row_builder.insert_text_cell("A"), FieldType::Number => row_builder.insert_number_cell("1"), // 1647251762 => Mar 14,2022 - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1647251762, None, None, &field_type) }, FieldType::SingleSelect => { @@ -148,7 +161,7 @@ pub fn make_test_board() -> DatabaseData { FieldType::RichText => row_builder.insert_text_cell("B"), FieldType::Number => row_builder.insert_number_cell("2"), // 1647251762 => Mar 14,2022 - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1647251762, None, None, &field_type) }, FieldType::SingleSelect => { @@ -167,7 +180,7 @@ pub fn make_test_board() -> DatabaseData { FieldType::RichText => row_builder.insert_text_cell("C"), FieldType::Number => row_builder.insert_number_cell("3"), // 1647251762 => Mar 14,2022 - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1647251762, None, None, &field_type) }, FieldType::SingleSelect => { @@ -189,7 +202,7 @@ pub fn make_test_board() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("DA"), FieldType::Number => row_builder.insert_number_cell("4"), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1668704685, None, None, &field_type) }, FieldType::SingleSelect => { @@ -206,7 +219,7 @@ pub fn make_test_board() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("AE"), FieldType::Number => row_builder.insert_number_cell(""), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1668359085, None, None, &field_type) }, FieldType::SingleSelect => { diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index d4878a4d8f..fcba0a73f2 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -7,7 +7,7 @@ use flowy_database2::entities::FieldType; use flowy_database2::services::field::checklist_type_option::ChecklistTypeOption; use flowy_database2::services::field::{ DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption, - SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, + SelectOption, SelectOptionColor, SingleSelectTypeOption, TimeFormat, TimestampTypeOption, }; use crate::database::database_editor::TestRowBuilder; @@ -39,26 +39,39 @@ pub fn make_test_grid() -> DatabaseData { .build(); fields.push(number_field); }, - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { // Date let date_type_option = DateTypeOption { date_format: DateFormat::US, time_format: TimeFormat::TwentyFourHour, timezone_id: "Etc/UTC".to_owned(), - field_type: field_type.clone(), - }; - let name = match field_type { - FieldType::DateTime => "Time", - FieldType::LastEditedTime => "Updated At", - FieldType::CreatedTime => "Created At", - _ => "", }; + let name = "Time"; let date_field = FieldBuilder::new(field_type.clone(), date_type_option) .name(name) .visibility(true) .build(); fields.push(date_field); }, + FieldType::LastEditedTime | FieldType::CreatedTime => { + // LastEditedTime and CreatedTime + let timestamp_type_option = TimestampTypeOption { + date_format: DateFormat::US, + time_format: TimeFormat::TwentyFourHour, + include_time: true, + field_type: field_type.clone(), + }; + let name = match field_type { + FieldType::LastEditedTime => "Last Modified", + FieldType::CreatedTime => "Created At", + _ => "", + }; + let timestamp_field = FieldBuilder::new(field_type.clone(), timestamp_type_option) + .name(name) + .visibility(true) + .build(); + fields.push(timestamp_field); + }, FieldType::SingleSelect => { // Single Select let option1 = SelectOption::with_color(COMPLETED, SelectOptionColor::Purple); @@ -129,7 +142,7 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("A"), FieldType::Number => row_builder.insert_number_cell("1"), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1647251762, None, None, &field_type) }, FieldType::MultiSelect => row_builder @@ -150,7 +163,7 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell(""), FieldType::Number => row_builder.insert_number_cell("2"), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1647251762, None, None, &field_type) }, FieldType::MultiSelect => row_builder @@ -165,7 +178,7 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("C"), FieldType::Number => row_builder.insert_number_cell("3"), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1647251762, None, None, &field_type) }, FieldType::SingleSelect => { @@ -184,7 +197,7 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("DA"), FieldType::Number => row_builder.insert_number_cell("14"), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1668704685, None, None, &field_type) }, FieldType::SingleSelect => { @@ -200,7 +213,7 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("AE"), FieldType::Number => row_builder.insert_number_cell(""), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1668359085, None, None, &field_type) }, FieldType::SingleSelect => { @@ -218,7 +231,7 @@ pub fn make_test_grid() -> DatabaseData { match field_type { FieldType::RichText => row_builder.insert_text_cell("AE"), FieldType::Number => row_builder.insert_number_cell("5"), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + FieldType::DateTime => { row_builder.insert_date_cell(1671938394, None, None, &field_type) }, FieldType::SingleSelect => { diff --git a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs index be112c0c2f..af4f19f863 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/share_test/export_test.rs @@ -27,13 +27,13 @@ async fn export_csv_test() { let test = DatabaseEditorTest::new_grid().await; let database = test.editor.clone(); let s = database.export_csv(CSVFormat::Original).await.unwrap(); - let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Updated At,Created At -A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,2022/03/14,2022/03/14 -,$2,2022/03/14,,"Google,Twitter",Yes,,,2022/03/14,2022/03/14 -C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,2022/03/14,2022/03/14 -DA,$14,2022/11/17,Completed,,No,,,2022/11/17,2022/11/17 -AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,2022/11/13,2022/11/13 -AE,$5,2022/12/25,Planned,Facebook,Yes,,,2022/12/25,2022/12/25 + let expected = r#"Name,Price,Time,Status,Platform,is urgent,link,TODO,Last Modified,Created At +A,$1,2022/03/14,,"Google,Facebook",Yes,AppFlowy website - https://www.appflowy.io,,, +,$2,2022/03/14,,"Google,Twitter",Yes,,,, +C,$3,2022/03/14,Completed,"Facebook,Google,Twitter",No,,,, +DA,$14,2022/11/17,Completed,,No,,,, +AE,,2022/11/13,Planned,"Facebook,Twitter",No,,,, +AE,$5,2022/12/25,Planned,Facebook,Yes,,,, CB,,,,,,,,, "#; println!("{}", s);