From aa621a8d8433c7ca738392dc00a3dbc6ad94de1f Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Thu, 13 Jun 2024 10:22:13 +0330 Subject: [PATCH] feat: timer field (#5349) * feat: wip timer field * feat: timer field fixing errors * feat: wip timer field frontend * fix: parsing TimerCellDataPB * feat: parse time string to minutes * fix: don't allow none number input * fix: timer filter * style: cargo fmt * fix: clippy errors * refactor: rename field type timer to time * refactor: missed some variable and files to rename * style: cargo fmt fix * feat: format time field type data in frontend * style: fix cargo fmt * fix: fixes after merge --------- Co-authored-by: Mathias Mogensen --- .../field/mobile_field_bottom_sheets.dart | 1 + .../field/mobile_full_field_editor.dart | 1 + .../application/cell/bloc/time_cell_bloc.dart | 117 +++++++++ .../cell/cell_controller_builder.dart | 14 +- .../application/cell/cell_data_loader.dart | 15 ++ .../application/field/field_info.dart | 2 + .../database/domain/filter_service.dart | 24 ++ .../filter/filter_create_bloc.dart | 5 + .../filter/time_filter_editor_bloc.dart | 111 +++++++++ .../widgets/filter/choicechip/time.dart | 227 ++++++++++++++++++ .../widgets/filter/filter_info.dart | 6 + .../widgets/filter/filter_menu_item.dart | 6 +- .../widgets/cell/card_cell_builder.dart | 16 +- .../card_cell_skeleton/time_card_cell.dart | 62 +++++ .../desktop_board_card_cell_style.dart | 8 +- .../mobile_board_card_cell_style.dart | 8 +- .../desktop_grid/desktop_grid_time_cell.dart | 37 +++ .../desktop_row_detail_time_cell.dart | 40 +++ .../widgets/cell/editable_cell_builder.dart | 18 +- .../cell/editable_cell_skeleton/time.dart | 120 +++++++++ .../mobile_grid/mobile_grid_time_cell.dart | 29 +++ .../mobile_row_detail_time_cell.dart | 46 ++++ .../widgets/field/field_type_list.dart | 4 +- .../field/type_option_editor/builder.dart | 5 +- .../field/type_option_editor/time.dart | 19 ++ .../lib/util/field_type_extension.dart | 4 + frontend/appflowy_flutter/lib/util/time.dart | 43 ++++ frontend/appflowy_flutter/pubspec.lock | 24 ++ .../test/unit_test/util/time.dart | 24 ++ frontend/resources/translations/en.json | 3 +- .../src/database_event.rs | 6 + .../src/entities/field_entities.rs | 6 + .../src/entities/filter_entities/mod.rs | 2 + .../entities/filter_entities/time_filter.rs | 23 ++ .../src/entities/filter_entities/util.rs | 9 +- .../flowy-database2/src/entities/macros.rs | 1 + .../src/entities/type_option_entities/mod.rs | 2 + .../type_option_entities/time_entities.rs | 28 +++ .../src/services/cell/cell_operation.rs | 2 +- .../src/services/field/type_options/mod.rs | 2 + .../text_type_option/text_type_option.rs | 7 +- .../type_options/time_type_option/mod.rs | 6 + .../type_options/time_type_option/time.rs | 115 +++++++++ .../time_type_option/time_entities.rs | 47 ++++ .../time_type_option/time_filter.rs | 72 ++++++ .../field/type_options/type_option.rs | 10 +- .../field/type_options/type_option_cell.rs | 19 +- .../src/services/filter/controller.rs | 4 + .../src/services/filter/entities.rs | 6 + .../tests/database/cell_test/test.rs | 19 +- .../tests/database/field_test/test.rs | 20 ++ .../tests/database/field_test/util.rs | 20 +- .../tests/database/filter_test/mod.rs | 1 + .../database/filter_test/time_filter_test.rs | 121 ++++++++++ .../database/mock_data/board_mock_data.rs | 6 + .../database/mock_data/grid_mock_data.rs | 10 +- .../tests/database/share_test/export_test.rs | 2 + 57 files changed, 1579 insertions(+), 26 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/time_filter_editor_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/time_card_cell.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_time_cell.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart create mode 100644 frontend/appflowy_flutter/lib/util/time.dart create mode 100644 frontend/appflowy_flutter/test/unit_test/util/time.dart create mode 100644 frontend/rust-lib/flowy-database2/src/entities/filter_entities/time_filter.rs create mode 100644 frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs create mode 100644 frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart index 266a06de7f..7d81801d73 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart @@ -28,6 +28,7 @@ const mobileSupportedFieldTypes = [ FieldType.CreatedTime, FieldType.Checkbox, FieldType.Checklist, + FieldType.Time, ]; Future showFieldTypeGridBottomSheet( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart index 26c9d462a6..ef8ce6e51d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart @@ -119,6 +119,7 @@ class FieldOptionValues { case FieldType.RichText: case FieldType.URL: case FieldType.Checkbox: + case FieldType.Time: return null; case FieldType.Number: return NumberTypeOptionPB( diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart new file mode 100644 index 0000000000..62ff95850f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/time_cell_bloc.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy/util/time.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; + +part 'time_cell_bloc.freezed.dart'; + +class TimeCellBloc extends Bloc { + TimeCellBloc({ + required this.cellController, + }) : super(TimeCellState.initial(cellController)) { + _dispatch(); + _startListening(); + } + + final TimeCellController cellController; + void Function()? _onCellChangedFn; + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener( + onCellChanged: _onCellChangedFn!, + onFieldChanged: _onFieldChangedListener, + ); + } + await cellController.dispose(); + return super.close(); + } + + void _dispatch() { + on( + (event, emit) async { + await event.when( + didReceiveCellUpdate: (content) { + emit( + state.copyWith( + content: + content != null ? formatTime(content.time.toInt()) : "", + ), + ); + }, + didUpdateField: (fieldInfo) { + final wrap = fieldInfo.wrapCellContent; + if (wrap != null) { + emit(state.copyWith(wrap: wrap)); + } + }, + updateCell: (text) async { + text = parseTime(text)?.toString() ?? text; + if (state.content != text) { + emit(state.copyWith(content: text)); + await cellController.saveCellData(text); + + // If the input content is "abc" that can't parsered as number + // then the data stored in the backend will be an empty string. + // So for every cell data that will be formatted in the backend. + // It needs to get the formatted data after saving. + add( + TimeCellEvent.didReceiveCellUpdate( + cellController.getCellData(), + ), + ); + } + }, + ); + }, + ); + } + + void _startListening() { + _onCellChangedFn = cellController.addListener( + onCellChanged: (cellContent) { + if (!isClosed) { + add(TimeCellEvent.didReceiveCellUpdate(cellContent)); + } + }, + onFieldChanged: _onFieldChangedListener, + ); + } + + void _onFieldChangedListener(FieldInfo fieldInfo) { + if (!isClosed) { + add(TimeCellEvent.didUpdateField(fieldInfo)); + } + } +} + +@freezed +class TimeCellEvent with _$TimeCellEvent { + const factory TimeCellEvent.didReceiveCellUpdate(TimeCellDataPB? cell) = + _DidReceiveCellUpdate; + const factory TimeCellEvent.didUpdateField(FieldInfo fieldInfo) = + _DidUpdateField; + const factory TimeCellEvent.updateCell(String text) = _UpdateCell; +} + +@freezed +class TimeCellState with _$TimeCellState { + const factory TimeCellState({ + required String content, + required bool wrap, + }) = _TimeCellState; + + factory TimeCellState.initial(TimeCellController cellController) { + final wrap = cellController.fieldInfo.wrapCellContent; + final cellData = cellController.getCellData(); + return TimeCellState( + content: cellData != null ? formatTime(cellData.time.toInt()) : "", + wrap: wrap ?? true, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart index e9457e23dc..50ef7ccb74 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_controller_builder.dart @@ -16,6 +16,7 @@ typedef TimestampCellController = CellController; typedef URLCellController = CellController; typedef RelationCellController = CellController; typedef SummaryCellController = CellController; +typedef TimeCellController = CellController; typedef TranslateCellController = CellController; CellController makeCellController( @@ -121,7 +122,6 @@ CellController makeCellController( ), cellDataPersistence: TextCellDataPersistence(), ); - case FieldType.Relation: return RelationCellController( viewId: viewId, @@ -146,6 +146,18 @@ CellController makeCellController( ), cellDataPersistence: TextCellDataPersistence(), ); + case FieldType.Time: + return TimeCellController( + viewId: viewId, + fieldController: fieldController, + cellContext: cellContext, + rowCache: rowCache, + cellDataLoader: CellDataLoader( + parser: TimeCellDataParser(), + reloadOnFieldChange: true, + ), + cellDataPersistence: TextCellDataPersistence(), + ); case FieldType.Translate: return TranslateCellController( viewId: viewId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart index 4edae575ce..1c03239cde 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/cell_data_loader.dart @@ -181,3 +181,18 @@ class RelationCellDataParser implements CellDataParser { } } } + +class TimeCellDataParser implements CellDataParser { + @override + TimeCellDataPB? parserData(List data) { + if (data.isEmpty) { + return null; + } + try { + return TimeCellDataPB.fromBuffer(data); + } catch (e) { + Log.error("Failed to parse timer data: $e"); + return null; + } + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart index e071c6edae..bc5107f75c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart @@ -64,6 +64,7 @@ class FieldInfo with _$FieldInfo { case FieldType.SingleSelect: case FieldType.Checklist: case FieldType.URL: + case FieldType.Time: return true; default: return false; @@ -85,6 +86,7 @@ class FieldInfo with _$FieldInfo { case FieldType.LastEditedTime: case FieldType.CreatedTime: case FieldType.Checklist: + case FieldType.Time: return true; default: return false; diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart index 64854a8faf..e618da5de9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart @@ -202,6 +202,30 @@ class FilterBackendService { ); } + Future> insertTimeFilter({ + required String fieldId, + String? filterId, + required NumberFilterConditionPB condition, + String content = "", + }) { + final filter = TimeFilterPB() + ..condition = condition + ..content = content; + + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: FieldType.Time, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: FieldType.Time, + data: filter.writeToBuffer(), + ); + } + Future> insertFilter({ required String fieldId, required FieldType fieldType, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart index d8ea5906a8..a27b0bf000 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart @@ -127,6 +127,11 @@ class GridCreateFilterBloc fieldId: fieldId, condition: NumberFilterConditionPB.Equal, ); + case FieldType.Time: + return _filterBackendSvc.insertTimeFilter( + fieldId: fieldId, + condition: NumberFilterConditionPB.Equal, + ); case FieldType.RichText: return _filterBackendSvc.insertTextFilter( fieldId: fieldId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/time_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/time_filter_editor_bloc.dart new file mode 100644 index 0000000000..65625ca7f2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/time_filter_editor_bloc.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/domain/filter_listener.dart'; +import 'package:appflowy/plugins/database/domain/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'time_filter_editor_bloc.freezed.dart'; + +class TimeFilterEditorBloc + extends Bloc { + TimeFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(TimeFilterEditorState.initial(filterInfo)) { + _dispatch(); + _startListening(); + } + + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + void _dispatch() { + on( + (event, emit) async { + event.when( + didReceiveFilter: (filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + emit( + state.copyWith( + filterInfo: filterInfo, + filter: filterInfo.timeFilter()!, + ), + ); + }, + updateCondition: (NumberFilterConditionPB condition) { + _filterBackendSvc.insertTimeFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ); + }, + updateContent: (content) { + _filterBackendSvc.insertTimeFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + content: content, + ); + }, + delete: () { + _filterBackendSvc.deleteFilter( + fieldId: filterInfo.fieldInfo.id, + filterId: filterInfo.filter.id, + ); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onUpdated: (filter) { + if (!isClosed) { + add(TimeFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class TimeFilterEditorEvent with _$TimeFilterEditorEvent { + const factory TimeFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory TimeFilterEditorEvent.updateCondition( + NumberFilterConditionPB condition, + ) = _UpdateCondition; + const factory TimeFilterEditorEvent.updateContent(String content) = + _UpdateContent; + const factory TimeFilterEditorEvent.delete() = _Delete; +} + +@freezed +class TimeFilterEditorState with _$TimeFilterEditorState { + const factory TimeFilterEditorState({ + required FilterInfo filterInfo, + required TimeFilterPB filter, + }) = _TimeFilterEditorState; + + factory TimeFilterEditorState.initial(FilterInfo filterInfo) { + return TimeFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.timeFilter()!, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart new file mode 100644 index 0000000000..828f124de1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/time.dart @@ -0,0 +1,227 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/time_filter_editor_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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 '../condition_button.dart'; +import '../disclosure_button.dart'; +import '../filter_info.dart'; +import 'choicechip.dart'; + +class TimeFilterChoiceChip extends StatefulWidget { + const TimeFilterChoiceChip({ + super.key, + required this.filterInfo, + }); + + final FilterInfo filterInfo; + + @override + State createState() => _TimeFilterChoiceChipState(); +} + +class _TimeFilterChoiceChipState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => TimeFilterEditorBloc( + filterInfo: widget.filterInfo, + ), + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(200, 100)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: const TimeFilterEditor(), + ); + }, + child: ChoiceChipButton( + filterInfo: state.filterInfo, + ), + ); + }, + ), + ); + } +} + +class TimeFilterEditor extends StatefulWidget { + const TimeFilterEditor({super.key}); + + @override + State createState() => _TimeFilterEditorState(); +} + +class _TimeFilterEditorState extends State { + final popoverMutex = PopoverMutex(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final List children = [ + _buildFilterPanel(context, state), + if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty && + state.filter.condition != + NumberFilterConditionPB.NumberIsNotEmpty) ...[ + const VSpace(4), + _buildFilterTimeField(context, state), + ], + ]; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: IntrinsicHeight(child: Column(children: children)), + ); + }, + ); + } + + Widget _buildFilterPanel( + BuildContext context, + TimeFilterEditorState state, + ) { + return SizedBox( + height: 20, + child: Row( + children: [ + Expanded( + child: FlowyText( + state.filterInfo.fieldInfo.name, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(4), + Expanded( + child: TimeFilterConditionPBList( + filterInfo: state.filterInfo, + popoverMutex: popoverMutex, + onCondition: (condition) { + context + .read() + .add(TimeFilterEditorEvent.updateCondition(condition)); + }, + ), + ), + const HSpace(4), + DisclosureButton( + popoverMutex: popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const TimeFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + } + + Widget _buildFilterTimeField( + BuildContext context, + TimeFilterEditorState state, + ) { + return FlowyTextField( + text: state.filter.content, + hintText: LocaleKeys.grid_settings_typeAValue.tr(), + debounceDuration: const Duration(milliseconds: 300), + autoFocus: false, + onChanged: (text) { + context + .read() + .add(TimeFilterEditorEvent.updateContent(text)); + }, + ); + } +} + +class TimeFilterConditionPBList extends StatelessWidget { + const TimeFilterConditionPBList({ + super.key, + required this.filterInfo, + required this.popoverMutex, + required this.onCondition, + }); + + final FilterInfo filterInfo; + final PopoverMutex popoverMutex; + final Function(NumberFilterConditionPB) onCondition; + + @override + Widget build(BuildContext context) { + final timeFilter = filterInfo.timeFilter()!; + return PopoverActionList( + asBarrier: true, + mutex: popoverMutex, + direction: PopoverDirection.bottomWithCenterAligned, + actions: NumberFilterConditionPB.values + .map( + (action) => ConditionWrapper( + action, + timeFilter.condition == action, + ), + ) + .toList(), + buildChild: (controller) { + return ConditionButton( + conditionName: timeFilter.condition.filterName, + onTap: () => controller.show(), + ); + }, + onSelected: (action, controller) { + onCondition(action.inner); + controller.close(); + }, + ); + } +} + +class ConditionWrapper extends ActionCell { + ConditionWrapper(this.inner, this.isSelected); + + final NumberFilterConditionPB inner; + final bool isSelected; + + @override + Widget? rightIcon(Color iconColor) => + isSelected ? const FlowySvg(FlowySvgs.check_s) : null; + + @override + String get name => inner.filterName; +} + +extension TimeFilterConditionPBExtension on NumberFilterConditionPB { + String get filterName { + return switch (this) { + NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(), + NumberFilterConditionPB.NotEqual => + LocaleKeys.grid_numberFilter_notEqual.tr(), + NumberFilterConditionPB.LessThan => + LocaleKeys.grid_numberFilter_lessThan.tr(), + NumberFilterConditionPB.LessThanOrEqualTo => + LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(), + NumberFilterConditionPB.GreaterThan => + LocaleKeys.grid_numberFilter_greaterThan.tr(), + NumberFilterConditionPB.GreaterThanOrEqualTo => + LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(), + NumberFilterConditionPB.NumberIsEmpty => + LocaleKeys.grid_numberFilter_isEmpty.tr(), + NumberFilterConditionPB.NumberIsNotEmpty => + LocaleKeys.grid_numberFilter_isNotEmpty.tr(), + _ => "", + }; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart index 19c201d026..0f355ebc4c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart @@ -60,4 +60,10 @@ class FilterInfo { ? NumberFilterPB.fromBuffer(filter.data.data) : null; } + + TimeFilterPB? timeFilter() { + return filter.data.fieldType == FieldType.Time + ? TimeFilterPB.fromBuffer(filter.data.data) + : null; + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart index 3ca86d3969..7ce6b5a223 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart @@ -8,6 +8,7 @@ import 'choicechip/number.dart'; import 'choicechip/select_option/select_option.dart'; import 'choicechip/text.dart'; import 'choicechip/url.dart'; +import 'choicechip/time.dart'; import 'filter_info.dart'; class FilterMenuItem extends StatelessWidget { @@ -22,12 +23,15 @@ class FilterMenuItem extends StatelessWidget { FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo), FieldType.MultiSelect => SelectOptionFilterChoicechip(filterInfo: filterInfo), - FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo), + FieldType.Number => + NumberFilterChoiceChip(filterInfo: filterInfo), FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo), FieldType.SingleSelect => SelectOptionFilterChoicechip(filterInfo: filterInfo), FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo), FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo), + FieldType.Time => + TimeFilterChoiceChip(filterInfo: filterInfo), _ => const SizedBox(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart index 2b83b590d9..aff11f6584 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_builder.dart @@ -1,19 +1,21 @@ +import 'package:flutter/widgets.dart'; + import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; -import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/timestamp_card_cell.dart'; -import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/translate_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:flutter/widgets.dart'; import 'card_cell_skeleton/card_cell.dart'; import 'card_cell_skeleton/checkbox_card_cell.dart'; import 'card_cell_skeleton/checklist_card_cell.dart'; import 'card_cell_skeleton/date_card_cell.dart'; import 'card_cell_skeleton/number_card_cell.dart'; +import 'card_cell_skeleton/relation_card_cell.dart'; import 'card_cell_skeleton/select_option_card_cell.dart'; import 'card_cell_skeleton/summary_card_cell.dart'; import 'card_cell_skeleton/text_card_cell.dart'; +import 'card_cell_skeleton/time_card_cell.dart'; +import 'card_cell_skeleton/timestamp_card_cell.dart'; +import 'card_cell_skeleton/translate_card_cell.dart'; import 'card_cell_skeleton/url_card_cell.dart'; typedef CardCellStyleMap = Map; @@ -99,6 +101,12 @@ class CardCellBuilder { databaseController: databaseController, cellContext: cellContext, ), + FieldType.Time => TimeCardCell( + key: key, + style: isStyleOrNull(style), + databaseController: databaseController, + cellContext: cellContext, + ), FieldType.Translate => TranslateCardCell( key: key, style: isStyleOrNull(style), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/time_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/time_card_cell.dart new file mode 100644 index 0000000000..68a95e53e2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/time_card_cell.dart @@ -0,0 +1,62 @@ +import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; + +import 'card_cell.dart'; + +class TimeCardCellStyle extends CardCellStyle { + const TimeCardCellStyle({ + required super.padding, + required this.textStyle, + }); + + final TextStyle textStyle; +} + +class TimeCardCell extends CardCell { + const TimeCardCell({ + super.key, + required super.style, + required this.databaseController, + required this.cellContext, + }); + + final DatabaseController databaseController; + final CellContext cellContext; + + @override + State createState() => _TimeCellState(); +} + +class _TimeCellState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) { + return TimeCellBloc( + cellController: makeCellController( + widget.databaseController, + widget.cellContext, + ).as(), + ); + }, + child: BlocBuilder( + buildWhen: (previous, current) => previous.content != current.content, + builder: (context, state) { + if (state.content.isEmpty) { + return const SizedBox.shrink(); + } + + return Container( + alignment: AlignmentDirectional.centerStart, + padding: widget.style.padding, + child: Text(state.content, style: widget.style.textStyle), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart index 333886c6f9..95b7baa494 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/desktop_board_card_cell_style.dart @@ -1,6 +1,7 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/material.dart'; import '../card_cell_builder.dart'; import '../card_cell_skeleton/checkbox_card_cell.dart'; @@ -10,6 +11,7 @@ import '../card_cell_skeleton/number_card_cell.dart'; import '../card_cell_skeleton/relation_card_cell.dart'; import '../card_cell_skeleton/select_option_card_cell.dart'; import '../card_cell_skeleton/text_card_cell.dart'; +import '../card_cell_skeleton/time_card_cell.dart'; import '../card_cell_skeleton/timestamp_card_cell.dart'; import '../card_cell_skeleton/url_card_cell.dart'; @@ -84,6 +86,10 @@ CardCellStyleMap desktopBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), + FieldType.Time: TimeCardCellStyle( + padding: padding, + textStyle: textStyle, + ), FieldType.Translate: SummaryCardCellStyle( padding: padding, textStyle: textStyle, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart index 71a4d54b95..952d20e7e5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart @@ -1,6 +1,7 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/summary_card_cell.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/material.dart'; import '../card_cell_builder.dart'; import '../card_cell_skeleton/checkbox_card_cell.dart'; @@ -10,6 +11,7 @@ import '../card_cell_skeleton/number_card_cell.dart'; import '../card_cell_skeleton/relation_card_cell.dart'; import '../card_cell_skeleton/select_option_card_cell.dart'; import '../card_cell_skeleton/text_card_cell.dart'; +import '../card_cell_skeleton/time_card_cell.dart'; import '../card_cell_skeleton/timestamp_card_cell.dart'; import '../card_cell_skeleton/url_card_cell.dart'; @@ -83,6 +85,10 @@ CardCellStyleMap mobileBoardCardCellStyleMap(BuildContext context) { padding: padding, textStyle: textStyle, ), + FieldType.Time: TimeCardCellStyle( + padding: padding, + textStyle: textStyle, + ), FieldType.Translate: SummaryCardCellStyle( padding: padding, textStyle: textStyle, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart new file mode 100644 index 0000000000..a948e92b03 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_time_cell.dart @@ -0,0 +1,37 @@ +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../editable_cell_skeleton/time.dart'; + +class DesktopGridTimeCellSkin extends IEditableTimeCellSkin { + @override + Widget build( + BuildContext context, + CellContainerNotifier cellContainerNotifier, + TimeCellBloc bloc, + FocusNode focusNode, + TextEditingController textEditingController, + ) { + return TextField( + controller: textEditingController, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + maxLines: context.watch().state.wrap ? null : 1, + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: GridSize.cellContentInsets, + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + isDense: true, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_time_cell.dart new file mode 100644 index 0000000000..ffd68933c9 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_time_cell.dart @@ -0,0 +1,40 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../editable_cell_skeleton/time.dart'; + +class DesktopRowDetailTimeCellSkin extends IEditableTimeCellSkin { + @override + Widget build( + BuildContext context, + CellContainerNotifier cellContainerNotifier, + TimeCellBloc bloc, + FocusNode focusNode, + TextEditingController textEditingController, + ) { + return TextField( + controller: textEditingController, + focusNode: focusNode, + onEditingComplete: () => focusNode.unfocus(), + onSubmitted: (_) => focusNode.unfocus(), + style: Theme.of(context).textTheme.bodyMedium, + textInputAction: TextInputAction.done, + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 9), + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + disabledBorder: InputBorder.none, + hintText: LocaleKeys.grid_row_textPlaceholder.tr(), + hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).hintColor, + ), + isDense: true, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart index 2ac68ed034..155a6003ce 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart @@ -1,9 +1,9 @@ -import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/translate.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import '../row/accessory/cell_accessory.dart'; @@ -18,6 +18,7 @@ import 'editable_cell_skeleton/relation.dart'; import 'editable_cell_skeleton/select_option.dart'; import 'editable_cell_skeleton/summary.dart'; import 'editable_cell_skeleton/text.dart'; +import 'editable_cell_skeleton/time.dart'; import 'editable_cell_skeleton/timestamp.dart'; import 'editable_cell_skeleton/url.dart'; @@ -121,6 +122,12 @@ class EditableCellBuilder { skin: IEditableSummaryCellSkin.fromStyle(style), key: key, ), + FieldType.Time => EditableTimeCell( + databaseController: databaseController, + cellContext: cellContext, + skin: IEditableTimeCellSkin.fromStyle(style), + key: key, + ), FieldType.Translate => EditableTranslateCell( databaseController: databaseController, cellContext: cellContext, @@ -213,6 +220,12 @@ class EditableCellBuilder { skin: skinMap.relationSkin!, key: key, ), + FieldType.Time => EditableTimeCell( + databaseController: databaseController, + cellContext: cellContext, + skin: skinMap.timeSkin!, + key: key, + ), _ => throw UnimplementedError(), }; } @@ -368,6 +381,7 @@ class EditableCellSkinMap { this.textSkin, this.urlSkin, this.relationSkin, + this.timeSkin, }); final IEditableCheckboxCellSkin? checkboxSkin; @@ -379,6 +393,7 @@ class EditableCellSkinMap { final IEditableTextCellSkin? textSkin; final IEditableURLCellSkin? urlSkin; final IEditableRelationCellSkin? relationSkin; + final IEditableTimeCellSkin? timeSkin; bool has(FieldType fieldType) { return switch (fieldType) { @@ -394,6 +409,7 @@ class EditableCellSkinMap { FieldType.Number => numberSkin != null, FieldType.RichText => textSkin != null, FieldType.URL => urlSkin != null, + FieldType.Time => timeSkin != null, _ => throw UnimplementedError(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart new file mode 100644 index 0000000000..83c34bdf5d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/time.dart @@ -0,0 +1,120 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; +import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../desktop_grid/desktop_grid_time_cell.dart'; +import '../desktop_row_detail/desktop_row_detail_time_cell.dart'; +import '../mobile_grid/mobile_grid_time_cell.dart'; +import '../mobile_row_detail/mobile_row_detail_time_cell.dart'; + +abstract class IEditableTimeCellSkin { + const IEditableTimeCellSkin(); + + factory IEditableTimeCellSkin.fromStyle(EditableCellStyle style) { + return switch (style) { + EditableCellStyle.desktopGrid => DesktopGridTimeCellSkin(), + EditableCellStyle.desktopRowDetail => DesktopRowDetailTimeCellSkin(), + EditableCellStyle.mobileGrid => MobileGridTimeCellSkin(), + EditableCellStyle.mobileRowDetail => MobileRowDetailTimeCellSkin(), + }; + } + + Widget build( + BuildContext context, + CellContainerNotifier cellContainerNotifier, + TimeCellBloc bloc, + FocusNode focusNode, + TextEditingController textEditingController, + ); +} + +class EditableTimeCell extends EditableCellWidget { + EditableTimeCell({ + super.key, + required this.databaseController, + required this.cellContext, + required this.skin, + }); + + final DatabaseController databaseController; + final CellContext cellContext; + final IEditableTimeCellSkin skin; + + @override + GridEditableTextCell createState() => _TimeCellState(); +} + +class _TimeCellState extends GridEditableTextCell { + late final TextEditingController _textEditingController; + late final cellBloc = TimeCellBloc( + cellController: makeCellController( + widget.databaseController, + widget.cellContext, + ).as(), + ); + + @override + void initState() { + super.initState(); + _textEditingController = + TextEditingController(text: cellBloc.state.content); + } + + @override + void dispose() { + _textEditingController.dispose(); + cellBloc.close(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: cellBloc, + child: BlocListener( + listener: (context, state) => + _textEditingController.text = state.content, + child: Builder( + builder: (context) { + return widget.skin.build( + context, + widget.cellContainerNotifier, + cellBloc, + focusNode, + _textEditingController, + ); + }, + ), + ), + ); + } + + @override + SingleListenerFocusNode focusNode = SingleListenerFocusNode(); + + @override + void onRequestFocus() { + focusNode.requestFocus(); + } + + @override + String? onCopy() => cellBloc.state.content; + + @override + Future focusChanged() async { + if (mounted && + !cellBloc.isClosed && + cellBloc.state.content != _textEditingController.text.trim()) { + cellBloc + .add(TimeCellEvent.updateCell(_textEditingController.text.trim())); + } + return super.focusChanged(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart new file mode 100644 index 0000000000..08ab04c7c7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_time_cell.dart @@ -0,0 +1,29 @@ +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; +import 'package:flutter/material.dart'; + +import '../editable_cell_skeleton/time.dart'; + +class MobileGridTimeCellSkin extends IEditableTimeCellSkin { + @override + Widget build( + BuildContext context, + CellContainerNotifier cellContainerNotifier, + TimeCellBloc bloc, + FocusNode focusNode, + TextEditingController textEditingController, + ) { + return TextField( + controller: textEditingController, + focusNode: focusNode, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 15), + decoration: const InputDecoration( + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 14, vertical: 12), + isCollapsed: true, + ), + onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart new file mode 100644 index 0000000000..159f2063a4 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_time_cell.dart @@ -0,0 +1,46 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/time_cell_bloc.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +import '../editable_cell_skeleton/time.dart'; + +class MobileRowDetailTimeCellSkin extends IEditableTimeCellSkin { + @override + Widget build( + BuildContext context, + CellContainerNotifier cellContainerNotifier, + TimeCellBloc bloc, + FocusNode focusNode, + TextEditingController textEditingController, + ) { + return TextField( + controller: textEditingController, + focusNode: focusNode, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 16), + decoration: InputDecoration( + enabledBorder: + _getInputBorder(color: Theme.of(context).colorScheme.outline), + focusedBorder: + _getInputBorder(color: Theme.of(context).colorScheme.primary), + hintText: LocaleKeys.grid_row_textPlaceholder.tr(), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + isCollapsed: true, + isDense: true, + constraints: const BoxConstraints(), + ), + // close keyboard when tapping outside of the text field + onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(), + ); + } + + InputBorder _getInputBorder({Color? color}) { + return OutlineInputBorder( + borderSide: BorderSide(color: color!), + borderRadius: const BorderRadius.all(Radius.circular(14)), + gapPadding: 0, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index afcb7da38f..94ed2d8405 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -1,10 +1,11 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; typedef SelectFieldCallback = void Function(FieldType); @@ -21,6 +22,7 @@ const List _supportedFieldTypes = [ FieldType.CreatedTime, FieldType.Relation, FieldType.Summary, + FieldType.Time, FieldType.Translate, ]; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart index 8ec91bdbaf..e4bcdd4911 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart @@ -1,9 +1,10 @@ import 'dart:typed_data'; +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/widgets/field/type_option_editor/translate.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flutter/material.dart'; import 'checkbox.dart'; import 'checklist.dart'; @@ -14,6 +15,7 @@ import 'relation.dart'; import 'rich_text.dart'; import 'single_select.dart'; import 'summary.dart'; +import 'time.dart'; import 'timestamp.dart'; import 'url.dart'; @@ -34,6 +36,7 @@ abstract class TypeOptionEditorFactory { FieldType.Checklist => const ChecklistTypeOptionEditorFactory(), FieldType.Relation => const RelationTypeOptionEditorFactory(), FieldType.Summary => const SummaryTypeOptionEditorFactory(), + FieldType.Time => const TimeTypeOptionEditorFactory(), FieldType.Translate => const TranslateTypeOptionEditorFactory(), _ => throw UnimplementedError(), }; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart new file mode 100644 index 0000000000..01a8c519c2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/time.dart @@ -0,0 +1,19 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flutter/material.dart'; + +import 'builder.dart'; + +class TimeTypeOptionEditorFactory implements TypeOptionEditorFactory { + const TimeTypeOptionEditorFactory(); + + @override + Widget? build({ + required BuildContext context, + required String viewId, + required FieldPB field, + required PopoverMutex popoverMutex, + required TypeOptionDataCallback onTypeOptionUpdated, + }) => + null; +} diff --git a/frontend/appflowy_flutter/lib/util/field_type_extension.dart b/frontend/appflowy_flutter/lib/util/field_type_extension.dart index 99812b7c7f..f36c2fe264 100644 --- a/frontend/appflowy_flutter/lib/util/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/util/field_type_extension.dart @@ -22,6 +22,7 @@ extension FieldTypeExtension on FieldType { FieldType.CreatedTime => LocaleKeys.grid_field_createdAtFieldName.tr(), FieldType.Relation => LocaleKeys.grid_field_relationFieldName.tr(), FieldType.Summary => LocaleKeys.grid_field_summaryFieldName.tr(), + FieldType.Time => LocaleKeys.grid_field_timeFieldName.tr(), FieldType.Translate => LocaleKeys.grid_field_translateFieldName.tr(), _ => throw UnimplementedError(), }; @@ -39,6 +40,7 @@ extension FieldTypeExtension on FieldType { FieldType.CreatedTime => FlowySvgs.created_at_s, FieldType.Relation => FlowySvgs.relation_s, FieldType.Summary => FlowySvgs.ai_summary_s, + FieldType.Time => FlowySvgs.timer_start_s, FieldType.Translate => FlowySvgs.ai_translate_s, _ => throw UnimplementedError(), }; @@ -62,6 +64,7 @@ extension FieldTypeExtension on FieldType { FieldType.Checklist => const Color(0xFF98F4CD), FieldType.Relation => const Color(0xFFFDEDA7), FieldType.Summary => const Color(0xFFBECCFF), + FieldType.Time => const Color(0xFFFDEDA7), FieldType.Translate => const Color(0xFFBECCFF), _ => throw UnimplementedError(), }; @@ -80,6 +83,7 @@ extension FieldTypeExtension on FieldType { FieldType.Checklist => const Color(0xFF42AD93), FieldType.Relation => const Color(0xFFFDEDA7), FieldType.Summary => const Color(0xFF6859A7), + FieldType.Time => const Color(0xFFFDEDA7), FieldType.Translate => const Color(0xFF6859A7), _ => throw UnimplementedError(), }; diff --git a/frontend/appflowy_flutter/lib/util/time.dart b/frontend/appflowy_flutter/lib/util/time.dart new file mode 100644 index 0000000000..cdeb9834fc --- /dev/null +++ b/frontend/appflowy_flutter/lib/util/time.dart @@ -0,0 +1,43 @@ +final RegExp timerRegExp = + RegExp(r'(?:(?\d*)h)? ?(?:(?\d*)m)?'); + +int? parseTime(String timerStr) { + int? res = int.tryParse(timerStr); + if (res != null) { + return res; + } + + final matches = timerRegExp.firstMatch(timerStr); + if (matches == null) { + return null; + } + final hours = int.tryParse(matches.namedGroup('hours') ?? ""); + final minutes = int.tryParse(matches.namedGroup('minutes') ?? ""); + if (hours == null && minutes == null) { + return null; + } + + final expected = + "${hours != null ? '${hours}h' : ''}${hours != null && minutes != null ? ' ' : ''}${minutes != null ? '${minutes}m' : ''}"; + if (timerStr != expected) { + return null; + } + + res = 0; + res += hours != null ? hours * 60 : res; + res += minutes ?? 0; + + return res; +} + +String formatTime(int minutes) { + if (minutes >= 60) { + if (minutes % 60 == 0) { + return "${minutes ~/ 60}h"; + } + return "${minutes ~/ 60}h ${minutes % 60}m"; + } else if (minutes >= 0) { + return "${minutes}m"; + } + return ""; +} diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 8ad8a3f762..ffdf26e42a 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -1041,6 +1041,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.19.0" + intl_utils: + dependency: transitive + description: + name: intl_utils + sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4 + url: "https://pub.dev" + source: hosted + version: "2.8.7" io: dependency: transitive description: @@ -2192,6 +2200,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" universal_platform: dependency: transitive description: diff --git a/frontend/appflowy_flutter/test/unit_test/util/time.dart b/frontend/appflowy_flutter/test/unit_test/util/time.dart new file mode 100644 index 0000000000..ca4f2b8230 --- /dev/null +++ b/frontend/appflowy_flutter/test/unit_test/util/time.dart @@ -0,0 +1,24 @@ +import 'package:appflowy/util/time.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('parseTime should parse time string to minutes', () { + expect(parseTime('10'), 10); + expect(parseTime('70m'), 70); + expect(parseTime('4h 20m'), 260); + expect(parseTime('1h 80m'), 140); + expect(parseTime('asffsa2h3m'), null); + expect(parseTime('2h3m'), null); + expect(parseTime('blah'), null); + expect(parseTime('10a'), null); + expect(parseTime('2h'), 120); + }); + + test('formatTime should format time minutes to formatted string', () { + expect(formatTime(5), "5m"); + expect(formatTime(75), "1h 15m"); + expect(formatTime(120), "2h"); + expect(formatTime(-50), ""); + expect(formatTime(0), "0m"); + }); +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 4c00612fec..56ce12e546 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -30,7 +30,6 @@ "passwordHint": "Password", "repeatPasswordHint": "Repeat password", "signUpWith": "Sign up with:" - }, "signIn": { "loginTitle": "Login to @:appName", @@ -1012,6 +1011,7 @@ "checklistFieldName": "Checklist", "relationFieldName": "Relation", "summaryFieldName": "AI Summary", + "timeFieldName": "Time", "translateFieldName": "AI Translate", "translateTo": "Translate to", "numberFormat": "Number format", @@ -1915,4 +1915,3 @@ "title": "Spaces" } } - diff --git a/frontend/rust-lib/event-integration-test/src/database_event.rs b/frontend/rust-lib/event-integration-test/src/database_event.rs index 12c76387c6..79b8b528d2 100644 --- a/frontend/rust-lib/event-integration-test/src/database_event.rs +++ b/frontend/rust-lib/event-integration-test/src/database_event.rs @@ -657,6 +657,12 @@ impl<'a> TestRowBuilder<'a> { checklist_field.id.clone() } + pub fn insert_time_cell(&mut self, time: i64) -> String { + let time_field = self.field_with_type(&FieldType::Time); + self.cell_build.insert_number_cell(&time_field.id, time); + time_field.id.clone() + } + pub fn field_with_type(&self, field_type: &FieldType) -> Field { self .fields diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index 3a1dd63b2f..3263986db8 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -450,6 +450,7 @@ pub enum FieldType { Relation = 10, Summary = 11, Translate = 12, + Time = 13, } impl Display for FieldType { @@ -491,6 +492,7 @@ impl FieldType { FieldType::Relation => "Relation", FieldType::Summary => "Summarize", FieldType::Translate => "Translate", + FieldType::Time => "Time", }; s.to_string() } @@ -547,6 +549,10 @@ impl FieldType { matches!(self, FieldType::Relation) } + pub fn is_time(&self) -> bool { + matches!(self, FieldType::Time) + } + pub fn can_be_group(&self) -> bool { self.is_select_option() || self.is_checkbox() || self.is_url() } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs index 7840bd4ff6..a6a990a458 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/mod.rs @@ -6,6 +6,7 @@ mod number_filter; mod relation_filter; mod select_option_filter; mod text_filter; +mod time_filter; mod util; pub use checkbox_filter::*; @@ -16,4 +17,5 @@ pub use number_filter::*; pub use relation_filter::*; pub use select_option_filter::*; pub use text_filter::*; +pub use time_filter::*; pub use util::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/time_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/time_filter.rs new file mode 100644 index 0000000000..bf1f734450 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/time_filter.rs @@ -0,0 +1,23 @@ +use flowy_derive::ProtoBuf; + +use crate::entities::NumberFilterConditionPB; +use crate::services::filter::ParseFilterData; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct TimeFilterPB { + #[pb(index = 1)] + pub condition: NumberFilterConditionPB, + + #[pb(index = 2)] + pub content: String, +} + +impl ParseFilterData for TimeFilterPB { + fn parse(condition: u8, content: String) -> Self { + TimeFilterPB { + condition: NumberFilterConditionPB::try_from(condition) + .unwrap_or(NumberFilterConditionPB::Equal), + content, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs index d4f3bedb7c..af1288506e 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs @@ -10,7 +10,7 @@ use validator::Validate; use crate::entities::{ CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, RelationFilterPB, - SelectOptionFilterPB, TextFilterPB, + SelectOptionFilterPB, TextFilterPB, TimeFilterPB, }; use crate::services::filter::{Filter, FilterChangeset, FilterInner}; @@ -109,6 +109,10 @@ impl From<&Filter> for FilterPB { .cloned::() .unwrap() .try_into(), + FieldType::Time => condition_and_content + .cloned::() + .unwrap() + .try_into(), FieldType::Translate => condition_and_content .cloned::() .unwrap() @@ -160,6 +164,9 @@ impl TryFrom for FilterInner { FieldType::Summary => { BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) }, + FieldType::Time => { + BoxAny::new(TimeFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, FieldType::Translate => { BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) }, diff --git a/frontend/rust-lib/flowy-database2/src/entities/macros.rs b/frontend/rust-lib/flowy-database2/src/entities/macros.rs index 35c594a07e..2d30eb15f0 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/macros.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/macros.rs @@ -17,6 +17,7 @@ macro_rules! impl_into_field_type { 10 => FieldType::Relation, 11 => FieldType::Summary, 12 => FieldType::Translate, + 13 => FieldType::Time, _ => { tracing::error!("🔴Can't parse FieldType from value: {}", ty); FieldType::RichText 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 ceeeab3874..f92072eabd 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 @@ -6,6 +6,7 @@ mod relation_entities; mod select_option_entities; mod summary_entities; mod text_entities; +mod time_entities; mod timestamp_entities; mod translate_entities; mod url_entities; @@ -18,6 +19,7 @@ pub use relation_entities::*; pub use select_option_entities::*; pub use summary_entities::*; pub use text_entities::*; +pub use time_entities::*; pub use timestamp_entities::*; pub use translate_entities::*; pub use url_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs new file mode 100644 index 0000000000..fdb3bdb6fd --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/time_entities.rs @@ -0,0 +1,28 @@ +use crate::services::field::TimeTypeOption; +use flowy_derive::ProtoBuf; + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct TimeTypeOptionPB { + #[pb(index = 1)] + pub dummy: String, +} + +impl From for TimeTypeOptionPB { + fn from(_data: TimeTypeOption) -> Self { + Self { + dummy: "".to_string(), + } + } +} + +impl From for TimeTypeOption { + fn from(_data: TimeTypeOptionPB) -> Self { + Self + } +} + +#[derive(Clone, Debug, Default, ProtoBuf)] +pub struct TimeCellDataPB { + #[pb(index = 2)] + pub time: i64, +} diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index 068eea5da4..d1bae644ea 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -222,7 +222,7 @@ impl<'a> CellBuilder<'a> { FieldType::RichText => { cells.insert(field_id, insert_text_cell(cell_str, field)); }, - FieldType::Number => { + FieldType::Number | FieldType::Time => { if let Ok(num) = cell_str.parse::() { cells.insert(field_id, insert_number_cell(num, field)); } 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 49451f3820..a6515c9db4 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 @@ -6,6 +6,7 @@ pub mod relation_type_option; pub mod selection_type_option; pub mod summary_type_option; pub mod text_type_option; +pub mod time_type_option; pub mod timestamp_type_option; pub mod translate_type_option; mod type_option; @@ -20,6 +21,7 @@ pub use number_type_option::*; pub use relation_type_option::*; pub use selection_type_option::*; pub use text_type_option::*; +pub use time_type_option::*; pub use timestamp_type_option::*; pub use type_option::*; pub use type_option_cell::*; 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 1627b04465..5cb2875de5 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 @@ -79,13 +79,14 @@ impl CellDataDecoder for RichTextTypeOption { | FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checkbox - | FieldType::URL => Some(StringCellData::from(stringify_cell(cell, field))), + | FieldType::URL + | FieldType::Summary + | FieldType::Translate + | FieldType::Time => Some(StringCellData::from(stringify_cell(cell, field))), FieldType::Checklist | FieldType::LastEditedTime | FieldType::CreatedTime | FieldType::Relation => None, - FieldType::Summary => Some(StringCellData::from(stringify_cell(cell, field))), - FieldType::Translate => Some(StringCellData::from(stringify_cell(cell, field))), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs new file mode 100644 index 0000000000..d64ecf45a3 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/mod.rs @@ -0,0 +1,6 @@ +mod time; +mod time_entities; +mod time_filter; + +pub use time::*; +pub use time_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs new file mode 100644 index 0000000000..0b7c141cb8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time.rs @@ -0,0 +1,115 @@ +use crate::entities::{TimeCellDataPB, TimeFilterPB}; +use crate::services::cell::{CellDataChangeset, CellDataDecoder}; +use crate::services::field::{ + TimeCellData, TypeOption, TypeOptionCellDataCompare, TypeOptionCellDataFilter, + TypeOptionCellDataSerde, TypeOptionTransform, +}; +use crate::services::sort::SortCondition; +use collab_database::fields::{TypeOptionData, TypeOptionDataBuilder}; +use collab_database::rows::Cell; +use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +#[derive(Clone, Debug, Serialize, Deserialize, Default)] +pub struct TimeTypeOption; + +impl TypeOption for TimeTypeOption { + type CellData = TimeCellData; + type CellChangeset = TimeCellChangeset; + type CellProtobufType = TimeCellDataPB; + type CellFilter = TimeFilterPB; +} + +impl From for TimeTypeOption { + fn from(_data: TypeOptionData) -> Self { + Self + } +} + +impl From for TypeOptionData { + fn from(_data: TimeTypeOption) -> Self { + TypeOptionDataBuilder::new().build() + } +} + +impl TypeOptionCellDataSerde for TimeTypeOption { + fn protobuf_encode( + &self, + cell_data: ::CellData, + ) -> ::CellProtobufType { + if let Some(time) = cell_data.0 { + return TimeCellDataPB { time }; + } + TimeCellDataPB { + time: i64::default(), + } + } + + fn parse_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + Ok(TimeCellData::from(cell)) + } +} + +impl TimeTypeOption { + pub fn new() -> Self { + Self + } +} + +impl TypeOptionTransform for TimeTypeOption {} + +impl CellDataDecoder for TimeTypeOption { + fn decode_cell(&self, cell: &Cell) -> FlowyResult<::CellData> { + self.parse_cell(cell) + } + + fn stringify_cell_data(&self, cell_data: ::CellData) -> String { + if let Some(time) = cell_data.0 { + return time.to_string(); + } + "".to_string() + } + + fn numeric_cell(&self, cell: &Cell) -> Option { + let time_cell_data = self.parse_cell(cell).ok()?; + Some(time_cell_data.0.unwrap() as f64) + } +} + +pub type TimeCellChangeset = String; + +impl CellDataChangeset for TimeTypeOption { + fn apply_changeset( + &self, + changeset: ::CellChangeset, + _cell: Option, + ) -> FlowyResult<(Cell, ::CellData)> { + let str = changeset.trim().to_string(); + let cell_data = TimeCellData(str.parse::().ok()); + + Ok((Cell::from(&cell_data), cell_data)) + } +} + +impl TypeOptionCellDataFilter for TimeTypeOption { + fn apply_filter( + &self, + filter: &::CellFilter, + cell_data: &::CellData, + ) -> bool { + filter.is_visible(cell_data.0) + } +} + +impl TypeOptionCellDataCompare for TimeTypeOption { + fn apply_cmp( + &self, + cell_data: &::CellData, + other_cell_data: &::CellData, + sort_condition: SortCondition, + ) -> Ordering { + let order = cell_data.0.cmp(&other_cell_data.0); + sort_condition.evaluate_order(order) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs new file mode 100644 index 0000000000..6084c80b5f --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_entities.rs @@ -0,0 +1,47 @@ +use crate::entities::FieldType; +use crate::services::field::{TypeOptionCellData, CELL_DATA}; +use collab::core::any_map::AnyMapExtension; +use collab_database::rows::{new_cell_builder, Cell}; + +#[derive(Clone, Debug, Default)] +pub struct TimeCellData(pub Option); + +impl TypeOptionCellData for TimeCellData { + fn is_cell_empty(&self) -> bool { + self.0.is_none() + } +} + +impl From<&Cell> for TimeCellData { + fn from(cell: &Cell) -> Self { + Self( + cell + .get_str_value(CELL_DATA) + .and_then(|data| data.parse::().ok()), + ) + } +} + +impl std::convert::From for TimeCellData { + fn from(s: String) -> Self { + Self(s.trim().to_string().parse::().ok()) + } +} + +impl ToString for TimeCellData { + fn to_string(&self) -> String { + if let Some(time) = self.0 { + time.to_string() + } else { + "".to_string() + } + } +} + +impl From<&TimeCellData> for Cell { + fn from(data: &TimeCellData) -> Self { + new_cell_builder(FieldType::Time) + .insert_str_value(CELL_DATA, data.to_string()) + .build() + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs new file mode 100644 index 0000000000..0620317dc0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/time_type_option/time_filter.rs @@ -0,0 +1,72 @@ +use collab_database::fields::Field; +use collab_database::rows::Cell; + +use crate::entities::{NumberFilterConditionPB, TimeFilterPB}; +use crate::services::cell::insert_text_cell; +use crate::services::filter::PreFillCellsWithFilter; + +impl TimeFilterPB { + pub fn is_visible(&self, cell_time: Option) -> bool { + if self.content.is_empty() { + match self.condition { + NumberFilterConditionPB::NumberIsEmpty => { + return cell_time.is_none(); + }, + NumberFilterConditionPB::NumberIsNotEmpty => { + return cell_time.is_some(); + }, + _ => {}, + } + } + + if cell_time.is_none() { + return false; + } + + let time = cell_time.unwrap(); + let content_time = self.content.parse::().unwrap_or_default(); + match self.condition { + NumberFilterConditionPB::Equal => time == content_time, + NumberFilterConditionPB::NotEqual => time != content_time, + NumberFilterConditionPB::GreaterThan => time > content_time, + NumberFilterConditionPB::LessThan => time < content_time, + NumberFilterConditionPB::GreaterThanOrEqualTo => time >= content_time, + NumberFilterConditionPB::LessThanOrEqualTo => time <= content_time, + _ => true, + } + } +} + +impl PreFillCellsWithFilter for TimeFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let expected_decimal = || self.content.parse::().ok(); + + let text = match self.condition { + NumberFilterConditionPB::Equal + | NumberFilterConditionPB::GreaterThanOrEqualTo + | NumberFilterConditionPB::LessThanOrEqualTo + if !self.content.is_empty() => + { + Some(self.content.clone()) + }, + NumberFilterConditionPB::GreaterThan if !self.content.is_empty() => { + expected_decimal().map(|value| { + let answer = value + 1; + answer.to_string() + }) + }, + NumberFilterConditionPB::LessThan if !self.content.is_empty() => { + expected_decimal().map(|value| { + let answer = value - 1; + answer.to_string() + }) + }, + _ => None, + }; + + let open_after_create = matches!(self.condition, NumberFilterConditionPB::NumberIsNotEmpty); + + // use `insert_text_cell` because self.content might not be a parsable i64. + (text.map(|s| insert_text_cell(s, field)), open_after_create) + } +} 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 c283d39bbc..a8b9d13b7e 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,7 +11,7 @@ use flowy_error::FlowyResult; use crate::entities::{ CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, FieldType, MultiSelectTypeOptionPB, NumberTypeOptionPB, RelationTypeOptionPB, RichTextTypeOptionPB, - SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimestampTypeOptionPB, + SingleSelectTypeOptionPB, SummarizationTypeOptionPB, TimeTypeOptionPB, TimestampTypeOptionPB, TranslateTypeOptionPB, URLTypeOptionPB, }; use crate::services::cell::CellDataDecoder; @@ -20,7 +20,7 @@ use crate::services::field::summary_type_option::summary::SummarizationTypeOptio use crate::services::field::translate_type_option::translate::TranslateTypeOption; use crate::services::field::{ CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, - RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption, + RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption, TimestampTypeOption, URLTypeOption, }; use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter}; use crate::services::sort::SortCondition; @@ -187,6 +187,7 @@ pub fn type_option_data_from_pb>( FieldType::Summary => { SummarizationTypeOptionPB::try_from(bytes).map(|pb| SummarizationTypeOption::from(pb).into()) }, + FieldType::Time => TimeTypeOptionPB::try_from(bytes).map(|pb| TimeTypeOption::from(pb).into()), FieldType::Translate => { TranslateTypeOptionPB::try_from(bytes).map(|pb| TranslateTypeOption::from(pb).into()) }, @@ -257,6 +258,10 @@ pub fn type_option_to_pb(type_option: TypeOptionData, field_type: &FieldType) -> .try_into() .unwrap() }, + FieldType::Time => { + let time_type_option: TimeTypeOption = type_option.into(); + TimeTypeOptionPB::from(time_type_option).try_into().unwrap() + }, FieldType::Translate => { let translate_type_option: TranslateTypeOption = type_option.into(); TranslateTypeOptionPB::from(translate_type_option) @@ -284,5 +289,6 @@ pub fn default_type_option_data_from_type(field_type: FieldType) -> TypeOptionDa FieldType::Relation => RelationTypeOption::default().into(), FieldType::Summary => SummarizationTypeOption::default().into(), FieldType::Translate => TranslateTypeOption::default().into(), + FieldType::Time => TimeTypeOption.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 19f7faf31b..415f694164 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 @@ -14,9 +14,9 @@ use crate::services::field::summary_type_option::summary::SummarizationTypeOptio use crate::services::field::translate_type_option::translate::TranslateTypeOption; use crate::services::field::{ CheckboxTypeOption, ChecklistTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, - RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, TypeOption, - TypeOptionCellData, TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionCellDataSerde, - TypeOptionTransform, URLTypeOption, + RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimeTypeOption, + TimestampTypeOption, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare, + TypeOptionCellDataFilter, TypeOptionCellDataSerde, TypeOptionTransform, URLTypeOption, }; use crate::services::sort::SortCondition; @@ -450,6 +450,16 @@ impl<'a> TypeOptionCellExt<'a> { self.cell_data_cache.clone(), ) }), + FieldType::Time => self + .field + .get_type_option::(field_type) + .map(|type_option| { + TypeOptionCellDataHandlerImpl::new_with_boxed( + type_option, + field_type, + self.cell_data_cache.clone(), + ) + }), FieldType::Translate => self .field .get_type_option::(field_type) @@ -563,6 +573,9 @@ fn get_type_option_transform_handler( }, FieldType::Summary => Box::new(SummarizationTypeOption::from(type_option_data)) as Box, + FieldType::Time => { + Box::new(TimeTypeOption::from(type_option_data)) as Box + }, FieldType::Translate => { Box::new(TranslateTypeOption::from(type_option_data)) as Box }, diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index 8c7ef9bcb1..975faa0995 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -303,6 +303,10 @@ impl FilterController { let filter = condition_and_content.cloned::().unwrap(); filter.get_compliant_cell(field) }, + FieldType::Time => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, _ => (None, false), }; diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs index 3b7d6444ef..718d062fbb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -12,6 +12,7 @@ use lib_infra::box_any::BoxAny; use crate::entities::{ CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType, InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB, + TimeFilterPB, }; use crate::services::field::SelectOptionIds; @@ -282,6 +283,7 @@ impl FilterInner { FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)), FieldType::Summary => BoxAny::new(TextFilterPB::parse(condition as u8, content)), FieldType::Translate => BoxAny::new(TextFilterPB::parse(condition as u8, content)), + FieldType::Time => BoxAny::new(TimeFilterPB::parse(condition as u8, content)), }; FilterInner::Data { @@ -368,6 +370,10 @@ impl<'a> From<&'a Filter> for FilterMap { let filter = condition_and_content.cloned::()?; (filter.condition as u8, filter.content) }, + FieldType::Time => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, filter.content) + }, FieldType::Translate => { let filter = condition_and_content.cloned::()?; (filter.condition as u8, filter.content) 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 2ed9db16ff..1c1f633e47 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 @@ -4,7 +4,7 @@ use flowy_database2::entities::FieldType; use flowy_database2::services::field::{ ChecklistCellChangeset, DateCellChangeset, DateCellData, MultiSelectTypeOption, RelationCellChangeset, SelectOptionCellChangeset, SingleSelectTypeOption, StringCellData, - URLCellData, + TimeCellData, URLCellData, }; use lib_infra::box_any::BoxAny; @@ -200,3 +200,20 @@ async fn update_updated_at_field_on_other_cell_update() { } } } + +#[tokio::test] +async fn time_cell_data_test() { + let test = DatabaseCellTest::new().await; + let time_field = test.get_first_field(FieldType::Time); + let cells = test + .editor + .get_cells_for_field(&test.view_id, &time_field.id) + .await; + + if let Some(cell) = cells[0].cell.as_ref() { + let cell = TimeCellData::from(cell); + + assert!(cell.0.is_some()); + assert_eq!(cell.0.unwrap_or_default(), 75); + } +} 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 e53be13266..7cd9f9f3d1 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 @@ -40,6 +40,26 @@ async fn grid_create_field() { }, ]; test.run_scripts(scripts).await; + + let (params, field) = create_time_field(&test.view_id()); + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; + + let (params, field) = create_time_field(&test.view_id()); + let scripts = vec![ + CreateField { params }, + AssertFieldTypeOptionEqual { + field_index: test.field_count(), + expected_type_option_data: field.get_any_type_option(field.field_type).unwrap(), + }, + ]; + test.run_scripts(scripts).await; } #[tokio::test] 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 a5f2703869..a648f7a442 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 @@ -4,7 +4,7 @@ use collab_database::views::OrderObjectPosition; use flowy_database2::entities::{CreateFieldParams, FieldType}; use flowy_database2::services::field::{ type_option_to_pb, DateFormat, DateTypeOption, FieldBuilder, RichTextTypeOption, SelectOption, - SingleSelectTypeOption, TimeFormat, TimestampTypeOption, + SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption, }; pub fn create_text_field(grid_id: &str) -> (CreateFieldParams, Field) { @@ -98,3 +98,21 @@ pub fn create_timestamp_field(grid_id: &str, field_type: FieldType) -> (CreateFi }; (params, field) } + +pub fn create_time_field(grid_id: &str) -> (CreateFieldParams, Field) { + let field_type = FieldType::Time; + let type_option = TimeTypeOption; + let text_field = FieldBuilder::new(field_type, type_option.clone()) + .name("Time field") + .build(); + + let type_option_data = type_option_to_pb(type_option.into(), &field_type).to_vec(); + let params = CreateFieldParams { + view_id: grid_id.to_owned(), + field_type, + type_option_data: Some(type_option_data), + field_name: None, + position: OrderObjectPosition::default(), + }; + (params, text_field) +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs index bf5d1513c9..e99cc725d5 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs @@ -6,3 +6,4 @@ mod number_filter_test; mod script; mod select_option_filter_test; mod text_filter_test; +mod time_filter_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs new file mode 100644 index 0000000000..503483a7b5 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/time_filter_test.rs @@ -0,0 +1,121 @@ +use flowy_database2::entities::{FieldType, NumberFilterConditionPB, TimeFilterPB}; +use lib_infra::box_any::BoxAny; + +use crate::database::filter_test::script::FilterScript::*; +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; + +#[tokio::test] +async fn grid_filter_time_is_equal_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.row_details.len(); + let expected = 1; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { + condition: NumberFilterConditionPB::Equal, + content: "75".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_time_is_less_than_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.row_details.len(); + let expected = 1; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + + data: BoxAny::new(TimeFilterPB { + condition: NumberFilterConditionPB::LessThan, + content: "80".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_time_is_less_than_or_equal_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.row_details.len(); + let expected = 1; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { + condition: NumberFilterConditionPB::LessThanOrEqualTo, + content: "75".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_time_is_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.row_details.len(); + let expected = 6; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { + condition: NumberFilterConditionPB::NumberIsEmpty, + content: "".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_time_is_not_empty_test() { + let mut test = DatabaseFilterTest::new().await; + let row_count = test.row_details.len(); + let expected = 1; + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Time, + data: BoxAny::new(TimeFilterPB { + condition: NumberFilterConditionPB::NumberIsNotEmpty, + content: "".to_string(), + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} 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 70d3cd77a3..d722a352f3 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 @@ -134,6 +134,12 @@ pub fn make_test_board() -> DatabaseData { .build(); fields.push(relation_field); }, + FieldType::Time => { + let time_field = FieldBuilder::from_field_type(field_type) + .name("Estimated time") + .build(); + fields.push(time_field); + }, FieldType::Translate => {}, } } 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 0666d9171c..6896e47ccb 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 @@ -10,7 +10,7 @@ use flowy_database2::services::field::translate_type_option::translate::Translat use flowy_database2::services::field::{ ChecklistTypeOption, DateFormat, DateTypeOption, FieldBuilder, MultiSelectTypeOption, NumberFormat, NumberTypeOption, RelationTypeOption, SelectOption, SelectOptionColor, - SingleSelectTypeOption, TimeFormat, TimestampTypeOption, + SingleSelectTypeOption, TimeFormat, TimeTypeOption, TimestampTypeOption, }; use flowy_database2::services::field_settings::default_field_settings_for_fields; @@ -133,6 +133,13 @@ pub fn make_test_grid() -> DatabaseData { .build(); fields.push(relation_field); }, + FieldType::Time => { + let type_option = TimeTypeOption; + let time_field = FieldBuilder::new(field_type, type_option) + .name("Estimated time") + .build(); + fields.push(time_field); + }, FieldType::Translate => { let type_option = TranslateTypeOption { auto_fill: false, @@ -168,6 +175,7 @@ pub fn make_test_grid() -> DatabaseData { FieldType::Checklist => { row_builder.insert_checklist_cell(vec![("First thing".to_string(), false)]) }, + FieldType::Time => row_builder.insert_time_cell(75), _ => "".to_owned(), }; } 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 5297ff14de..3fbb0aafe2 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 @@ -83,6 +83,7 @@ async fn export_and_then_import_meta_csv_test() { FieldType::CreatedTime => {}, FieldType::Relation => {}, FieldType::Summary => {}, + FieldType::Time => {}, FieldType::Translate => {}, } } else { @@ -167,6 +168,7 @@ async fn history_database_import_test() { FieldType::CreatedTime => {}, FieldType::Relation => {}, FieldType::Summary => {}, + FieldType::Time => {}, FieldType::Translate => {}, } } else {