From 2c9064f892d0da5ba44c536d91cb3d9c05b481e0 Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Thu, 2 May 2024 12:09:00 +0330 Subject: [PATCH 1/5] feat: enable group condition As part of AppFlowy-IO#2734 this enables group configuration so grouping by a field can also have more options such as group condition. For example when grouping by a date field type the user can choose to group by day, month, week, etc. Currently group condition is available only for date field type. --- .../application/field/field_controller.dart | 2 + .../application/field/field_info.dart | 10 ++ .../application/setting/group_bloc.dart | 25 +++- .../board/application/board_bloc.dart | 79 ++++++++---- .../database/domain/group_service.dart | 6 +- .../widgets/group/database_group.dart | 81 +++++++++++-- .../board_test/group_by_date_test.dart | 114 ++++++++++++++++++ frontend/resources/translations/en.json | 1 + .../src/database_event.rs | 8 +- .../tests/database/local_test/test.rs | 4 +- .../entities/group_entities/configuration.rs | 80 +++++++++++- .../src/entities/group_entities/group.rs | 19 ++- .../flowy-database2/src/event_handler.rs | 2 +- .../src/services/database/database_editor.rs | 23 +++- .../src/services/database_view/view_editor.rs | 13 +- .../group/controller_impls/date_controller.rs | 8 +- 16 files changed, 406 insertions(+), 69 deletions(-) create mode 100644 frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart index 7fa5c81d3df5..ad0d1b46530d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart @@ -137,6 +137,8 @@ class FieldController { List get fieldInfos => [..._fieldNotifier.fieldInfos]; List get filterInfos => [..._filterNotifier?.filters ?? []]; List get sortInfos => [..._sortNotifier?.sorts ?? []]; + List get groupSettings => + _groupConfigurationByFieldId.entries.map((e) => e.value).toList(); FieldInfo? getField(String fieldId) { return _fieldNotifier.fieldInfos 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 1022b1f83968..e071c6edaed9 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 @@ -1,5 +1,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; part 'field_info.freezed.dart'; @@ -89,4 +90,13 @@ class FieldInfo with _$FieldInfo { return false; } } + + List get groupConditions { + switch (field.fieldType) { + case FieldType.DateTime: + return DateConditionPB.values; + default: + return []; + } + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart index cef3ad6d7f4e..7b28a696ee7f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/setting/group_bloc.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database/domain/group_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/board_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -22,6 +23,7 @@ class DatabaseGroupBloc extends Bloc { viewId, databaseController.fieldController.fieldInfos, databaseController.databaseLayoutSetting!.board, + databaseController.fieldController.groupSettings, ), ) { _dispatch(); @@ -51,11 +53,22 @@ class DatabaseGroupBloc extends Bloc { _startListening(); }, didReceiveFieldUpdate: (fieldInfos) { - emit(state.copyWith(fieldInfos: fieldInfos)); + emit( + state.copyWith( + fieldInfos: fieldInfos, + groupSettings: + _databaseController.fieldController.groupSettings, + ), + ); }, - setGroupByField: (String fieldId, FieldType fieldType) async { + setGroupByField: ( + String fieldId, + FieldType fieldType, [ + List? settingContent, + ]) async { final result = await _groupBackendSvc.groupByField( fieldId: fieldId, + settingContent: settingContent ?? [], ); result.fold((l) => null, (err) => Log.error(err)); }, @@ -96,8 +109,9 @@ class DatabaseGroupEvent with _$DatabaseGroupEvent { const factory DatabaseGroupEvent.initial() = _Initial; const factory DatabaseGroupEvent.setGroupByField( String fieldId, - FieldType fieldType, - ) = _DatabaseGroupEvent; + FieldType fieldType, [ + @Default([]) List settingContent, + ]) = _DatabaseGroupEvent; const factory DatabaseGroupEvent.didReceiveFieldUpdate( List fields, ) = _DidReceiveFieldUpdate; @@ -112,16 +126,19 @@ class DatabaseGroupState with _$DatabaseGroupState { required String viewId, required List fieldInfos, required BoardLayoutSettingPB layoutSettings, + required List groupSettings, }) = _DatabaseGroupState; factory DatabaseGroupState.initial( String viewId, List fieldInfos, BoardLayoutSettingPB layoutSettings, + List groupSettings, ) => DatabaseGroupState( viewId: viewId, fieldInfos: fieldInfos, layoutSettings: layoutSettings, + groupSettings: groupSettings, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart index 7b816bbafc4d..cd0f36957a21 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart @@ -520,6 +520,9 @@ class BoardBloc extends Bloc { return "No ${field.name}"; } + final groupSettings = databaseController.fieldController.groupSettings + .firstWhereOrNull((gs) => gs.fieldId == field.id); + switch (field.fieldType) { case FieldType.SingleSelect: final options = @@ -540,33 +543,61 @@ class BoardBloc extends Bloc { case FieldType.URL: return group.groupId; case FieldType.DateTime: - // Assume DateCondition::Relative as there isn't an option for this - // right now. + final config = groupSettings?.content != null + ? DateGroupConfigurationPB.fromBuffer(groupSettings!.content) + : DateGroupConfigurationPB(); final dateFormat = DateFormat("y/MM/dd"); try { final targetDateTime = dateFormat.parseLoose(group.groupId); - final targetDateTimeDay = DateTime( - targetDateTime.year, - targetDateTime.month, - targetDateTime.day, - ); - final now = DateTime.now(); - final nowDay = DateTime( - now.year, - now.month, - now.day, - ); - final diff = targetDateTimeDay.difference(nowDay).inDays; - return switch (diff) { - 0 => "Today", - -1 => "Yesterday", - 1 => "Tomorrow", - -7 => "Last 7 days", - 2 => "Next 7 days", - -30 => "Last 30 days", - 8 => "Next 30 days", - _ => DateFormat("MMM y").format(targetDateTimeDay) - }; + switch (config.condition) { + case DateConditionPB.Day: + return DateFormat("MMM dd, y").format(targetDateTime); + case DateConditionPB.Week: + final beginningOfWeek = targetDateTime + .subtract(Duration(days: targetDateTime.weekday - 1)); + final endOfWeek = targetDateTime.add( + Duration(days: DateTime.daysPerWeek - targetDateTime.weekday), + ); + + final beginningOfWeekFormat = + beginningOfWeek.year != endOfWeek.year + ? "MMM dd y" + : "MMM dd"; + final endOfWeekFormat = beginningOfWeek.month != endOfWeek.month + ? "MMM dd y" + : "dd y"; + + return 'Week of ${DateFormat(beginningOfWeekFormat).format(beginningOfWeek)} - ${DateFormat(endOfWeekFormat).format(endOfWeek)}'; + case DateConditionPB.Month: + return DateFormat("MMM y").format(targetDateTime); + case DateConditionPB.Year: + return DateFormat("y").format(targetDateTime); + case DateConditionPB.Relative: + final targetDateTimeDay = DateTime( + targetDateTime.year, + targetDateTime.month, + targetDateTime.day, + ); + final now = DateTime.now(); + final nowDay = DateTime( + now.year, + now.month, + now.day, + ); + final diff = targetDateTimeDay.difference(nowDay).inDays; + return switch (diff) { + 0 => "Today", + -1 => "Yesterday", + 1 => "Tomorrow", + -7 => "Last 7 days", + 2 => "Next 7 days", + -30 => "Last 30 days", + 8 => "Next 30 days", + _ => DateFormat("MMM y").format(targetDateTimeDay) + }; + default: + return ""; + } } on FormatException { return ""; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart index 69703d7748a8..934bbba8d163 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/group_service.dart @@ -1,5 +1,5 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; @@ -10,10 +10,12 @@ class GroupBackendService { Future> groupByField({ required String fieldId, + required List settingContent, }) { final payload = GroupByFieldPayloadPB.create() ..viewId = viewId - ..fieldId = fieldId; + ..fieldId = fieldId + ..settingContent = settingContent; return DatabaseEventSetGroupByField(payload).send(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart index ff2ecef03dde..5ff1e404075a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart @@ -8,8 +8,7 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_ import 'package:appflowy/util/field_type_extension.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/board_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -17,6 +16,7 @@ import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; @@ -42,12 +42,11 @@ class DatabaseGroupList extends StatelessWidget { )..add(const DatabaseGroupEvent.initial()), child: BlocBuilder( builder: (context, state) { - final showHideUngroupedToggle = state.fieldInfos.any( - (field) => - field.canBeGroup && - field.isGroupField && - field.fieldType != FieldType.Checkbox, + final field = state.fieldInfos.firstWhereOrNull( + (field) => field.canBeGroup && field.isGroupField, ); + final showHideUngroupedToggle = + field?.fieldType != FieldType.Checkbox; final children = [ if (showHideUngroupedToggle) ...[ SizedBox( @@ -90,10 +89,46 @@ class DatabaseGroupList extends StatelessWidget { ...state.fieldInfos.where((fieldInfo) => fieldInfo.canBeGroup).map( (fieldInfo) => _GridGroupCell( fieldInfo: fieldInfo, + name: fieldInfo.name, + icon: fieldInfo.fieldType.svgData, + checked: fieldInfo.isGroupField, onSelected: onDismissed, key: ValueKey(fieldInfo.id), ), ), + if (field?.groupConditions.isNotEmpty ?? false) ...[ + const TypeOptionSeparator(spacing: 0), + SizedBox( + height: GridSize.popoverItemHeight, + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: FlowyText.medium( + LocaleKeys.board_groupCondition.tr(), + textAlign: TextAlign.left, + color: Theme.of(context).hintColor, + ), + ), + ), + ...field!.groupConditions.map( + (condition) => _GridGroupCell( + fieldInfo: field, + name: condition.name, + condition: condition.value, + onSelected: onDismissed, + checked: () { + final gs = state.groupSettings + .firstWhereOrNull((gs) => gs.fieldId == field.id); + if (gs == null) { + return false; + } + final config = + DateGroupConfigurationPB.fromBuffer(gs.content); + return config.condition == condition; + }(), + ), + ), + ], ]; return ListView.separated( @@ -128,15 +163,23 @@ class _GridGroupCell extends StatelessWidget { super.key, required this.fieldInfo, required this.onSelected, + required this.checked, + required this.name, + this.condition = 0, + this.icon, }); final FieldInfo fieldInfo; final VoidCallback onSelected; + final bool checked; + final int condition; + final String name; + final FlowySvgData? icon; @override Widget build(BuildContext context) { Widget? rightIcon; - if (fieldInfo.isGroupField) { + if (checked) { rightIcon = const Padding( padding: EdgeInsets.all(2.0), child: FlowySvg(FlowySvgs.check_s), @@ -150,19 +193,31 @@ class _GridGroupCell extends StatelessWidget { child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( - fieldInfo.name, + name, color: AFThemeExtension.of(context).textColor, ), - leftIcon: FlowySvg( - fieldInfo.fieldType.svgData, - color: Theme.of(context).iconTheme.color, - ), + leftIcon: icon != null + ? FlowySvg( + icon!, + color: Theme.of(context).iconTheme.color, + ) + : null, rightIcon: rightIcon, onTap: () { + List settingContent = []; + switch (fieldInfo.fieldType) { + case FieldType.DateTime: + final config = DateGroupConfigurationPB() + ..condition = DateConditionPB.values[condition]; + settingContent = config.writeToBuffer(); + break; + default: + } context.read().add( DatabaseGroupEvent.setGroupByField( fieldInfo.id, fieldInfo.fieldType, + settingContent, ), ); onSelected(); diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart new file mode 100644 index 000000000000..71576174cad4 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart @@ -0,0 +1,114 @@ +import 'package:appflowy/plugins/database/application/cell/bloc/date_cell_editor_bloc.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/application/setting/group_bloc.dart'; +import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'util.dart'; + +void main() { + late AppFlowyBoardTest boardTest; + + setUpAll(() async { + boardTest = await AppFlowyBoardTest.ensureInitialized(); + }); + + test('group by date field test', () async { + final context = await boardTest.createTestBoard(); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); + await boardResponseFuture(); + + // assert the initial values + assert(boardBloc.groupControllers.values.length == 4); + assert(context.fieldContexts.length == 2); + + await context.createField(FieldType.DateTime); + await boardResponseFuture(); + assert(context.fieldContexts.length == 3); + + final dateField = context.fieldContexts.last.field; + final cellController = context.makeCellControllerFromFieldId(dateField.id) + as DateCellController; + final bloc = DateCellEditorBloc( + cellController: cellController, + reminderBloc: getIt(), + ); + await boardResponseFuture(); + + bloc.add(DateCellEditorEvent.selectDay(DateTime.now())); + await boardResponseFuture(); + + final gridGroupBloc = DatabaseGroupBloc( + viewId: context.gridView.id, + databaseController: context.databaseController, + )..add(const DatabaseGroupEvent.initial()); + gridGroupBloc.add( + DatabaseGroupEvent.setGroupByField( + dateField.id, + dateField.fieldType, + ), + ); + await boardResponseFuture(); + + assert(boardBloc.groupControllers.values.length == 2); + assert( + boardBloc.boardController.groupDatas.last.headerData.groupName == "Today", + ); + }); + + test('group by date field with condition', () async { + final context = await boardTest.createTestBoard(); + final boardBloc = BoardBloc( + view: context.gridView, + databaseController: DatabaseController(view: context.gridView), + )..add(const BoardEvent.initial()); + await boardResponseFuture(); + + // assert the initial values + assert(boardBloc.groupControllers.values.length == 4); + assert(context.fieldContexts.length == 2); + + await context.createField(FieldType.DateTime); + await boardResponseFuture(); + assert(context.fieldContexts.length == 3); + + final dateField = context.fieldContexts.last.field; + final cellController = context.makeCellControllerFromFieldId(dateField.id) + as DateCellController; + final bloc = DateCellEditorBloc( + cellController: cellController, + reminderBloc: getIt(), + ); + await boardResponseFuture(); + + bloc.add(DateCellEditorEvent.selectDay(DateTime.now())); + await boardResponseFuture(); + + final gridGroupBloc = DatabaseGroupBloc( + viewId: context.gridView.id, + databaseController: context.databaseController, + )..add(const DatabaseGroupEvent.initial()); + final settingContent = DateGroupConfigurationPB() + ..condition = DateConditionPB.Year; + gridGroupBloc.add( + DatabaseGroupEvent.setGroupByField( + dateField.id, + dateField.fieldType, + settingContent.writeToBuffer(), + ), + ); + await boardResponseFuture(); + + assert(boardBloc.groupControllers.values.length == 2); + assert( + boardBloc.boardController.groupDatas.last.headerData.groupName == "2024", + ); + }); +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 5cd0c7ec07e5..af5f8b5ffbf0 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1120,6 +1120,7 @@ "ungroupedButtonTooltip": "Contains cards that don't belong in any group", "ungroupedItemsTitle": "Click to add to the board", "groupBy": "Group by", + "groupCondition": "Group condition", "referencedBoardPrefix": "View of", "notesTooltip": "Notes inside", "mobile": { 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 424936b6a8d1..f178a0887cd7 100644 --- a/frontend/rust-lib/event-integration-test/src/database_event.rs +++ b/frontend/rust-lib/event-integration-test/src/database_event.rs @@ -417,12 +417,18 @@ impl EventIntegrationTest { .error() } - pub async fn set_group_by_field(&self, view_id: &str, field_id: &str) -> Option { + pub async fn set_group_by_field( + &self, + view_id: &str, + field_id: &str, + setting_content: Vec, + ) -> Option { EventBuilder::new(self.clone()) .event(DatabaseEvent::SetGroupByField) .payload(GroupByFieldPayloadPB { field_id: field_id.to_string(), view_id: view_id.to_string(), + setting_content, }) .async_send() .await diff --git a/frontend/rust-lib/event-integration-test/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration-test/tests/database/local_test/test.rs index f0c78d15b38f..9a13b2b23f02 100644 --- a/frontend/rust-lib/event-integration-test/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration-test/tests/database/local_test/test.rs @@ -694,7 +694,7 @@ async fn update_database_layout_event_test2() { .find(|field| field.field_type == FieldType::Checkbox) .unwrap(); test - .set_group_by_field(&grid_view.id, &checkbox_field.id) + .set_group_by_field(&grid_view.id, &checkbox_field.id, vec![]) .await; let error = test @@ -722,7 +722,7 @@ async fn set_group_by_checkbox_field_test() { let checkbox_field = test.create_field(&board_view.id, FieldType::Checkbox).await; test - .set_group_by_field(&board_view.id, &checkbox_field.id) + .set_group_by_field(&board_view.id, &checkbox_field.id, vec![]) .await; let groups = test.get_groups(&board_view.id).await; diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs index a27e67ff1a49..bc6c528809a9 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/configuration.rs @@ -1,5 +1,10 @@ -use crate::services::group::Group; +use crate::{ + entities::FieldType, + services::group::{DateCondition, DateGroupConfiguration, Group}, +}; +use bytes::Bytes; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use flowy_error::FlowyResult; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct URLGroupConfigurationPB { @@ -46,16 +51,33 @@ pub struct NumberGroupConfigurationPB { #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct DateGroupConfigurationPB { #[pb(index = 1)] - pub condition: DateCondition, + pub condition: DateConditionPB, #[pb(index = 2)] hide_empty: bool, } -#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +impl From for DateGroupConfiguration { + fn from(data: DateGroupConfigurationPB) -> Self { + Self { + condition: data.condition.into(), + hide_empty: data.hide_empty, + } + } +} + +impl From for DateGroupConfigurationPB { + fn from(data: DateGroupConfiguration) -> Self { + Self { + condition: data.condition.into(), + hide_empty: data.hide_empty, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum, Default)] #[repr(u8)] -#[derive(Default)] -pub enum DateCondition { +pub enum DateConditionPB { #[default] Relative = 0, Day = 1, @@ -64,8 +86,56 @@ pub enum DateCondition { Year = 4, } +impl From for DateCondition { + fn from(data: DateConditionPB) -> Self { + match data { + DateConditionPB::Relative => DateCondition::Relative, + DateConditionPB::Day => DateCondition::Day, + DateConditionPB::Week => DateCondition::Week, + DateConditionPB::Month => DateCondition::Month, + DateConditionPB::Year => DateCondition::Year, + } + } +} + +impl From for DateConditionPB { + fn from(data: DateCondition) -> Self { + match data { + DateCondition::Relative => DateConditionPB::Relative, + DateCondition::Day => DateConditionPB::Day, + DateCondition::Week => DateConditionPB::Week, + DateCondition::Month => DateConditionPB::Month, + DateCondition::Year => DateConditionPB::Year, + } + } +} + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct CheckboxGroupConfigurationPB { #[pb(index = 1)] pub(crate) hide_empty: bool, } + +pub fn group_config_pb_to_json_str>( + bytes: T, + field_type: &FieldType, +) -> FlowyResult { + let bytes = bytes.into(); + match field_type { + FieldType::DateTime => DateGroupConfigurationPB::try_from(bytes) + .map(|pb| DateGroupConfiguration::from(pb).to_json())?, + _ => Ok("".to_string()), + } +} + +pub fn group_config_json_to_pb(setting_content: String, field_type: &FieldType) -> Bytes { + match field_type { + FieldType::DateTime => { + let date_group_config = DateGroupConfiguration::from_json(setting_content.as_ref()).unwrap(); + DateGroupConfigurationPB::from(date_group_config) + .try_into() + .unwrap() + }, + _ => Bytes::new(), + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs index 9f4068570265..ba61bb53dc38 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -5,9 +5,11 @@ use flowy_error::ErrorCode; use validator::Validate; use crate::entities::parser::NotEmptyStr; -use crate::entities::RowMetaPB; +use crate::entities::{FieldType, RowMetaPB}; use crate::services::group::{GroupChangeset, GroupData, GroupSetting}; +use super::group_config_json_to_pb; + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct GroupSettingPB { #[pb(index = 1)] @@ -15,13 +17,18 @@ pub struct GroupSettingPB { #[pb(index = 2)] pub field_id: String, + + #[pb(index = 3)] + pub content: Vec, } impl std::convert::From<&GroupSetting> for GroupSettingPB { fn from(rev: &GroupSetting) -> Self { + let field_type = FieldType::from(rev.field_type); GroupSettingPB { id: rev.id.clone(), field_id: rev.field_id.clone(), + content: group_config_json_to_pb(rev.content.clone(), &field_type).to_vec(), } } } @@ -105,6 +112,9 @@ pub struct GroupByFieldPayloadPB { #[pb(index = 2)] pub view_id: String, + + #[pb(index = 3)] + pub setting_content: Vec, } impl TryInto for GroupByFieldPayloadPB { @@ -118,13 +128,18 @@ impl TryInto for GroupByFieldPayloadPB { .map_err(|_| ErrorCode::ViewIdIsInvalid)? .0; - Ok(GroupByFieldParams { field_id, view_id }) + Ok(GroupByFieldParams { + field_id, + view_id, + setting_content: self.setting_content, + }) } } pub struct GroupByFieldParams { pub field_id: String, pub view_id: String, + pub setting_content: Vec, } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone, Validate)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index c8ae34433e2c..ae2d2e09ac4a 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -651,7 +651,7 @@ pub(crate) async fn set_group_by_field_handler( let params: GroupByFieldParams = data.into_inner().try_into()?; let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor - .set_group_by_field(¶ms.view_id, ¶ms.field_id) + .set_group_by_field(¶ms.view_id, ¶ms.field_id, params.setting_content) .await?; Ok(()) } 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 9067ebfe955b..c7e9db19019c 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 @@ -136,20 +136,37 @@ impl DatabaseEditor { self.database.lock().fields.get_field(field_id) } - pub async fn set_group_by_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { + pub async fn set_group_by_field( + &self, + view_id: &str, + field_id: &str, + data: Vec, + ) -> FlowyResult<()> { + let old_group_settings: Vec; + let mut setting_content = "".to_string(); { let database = self.database.lock(); let field = database.fields.get_field(field_id); + old_group_settings = database.get_all_group_setting(view_id); if let Some(field) = field { - let group_setting = default_group_setting(&field); + let field_type = FieldType::from(field.field_type); + setting_content = group_config_pb_to_json_str(data, &field_type)?; + let mut group_setting = default_group_setting(&field); + group_setting.content = setting_content.clone(); database.views.update_database_view(view_id, |view| { view.set_groups(vec![group_setting.into()]); }); } } + let old_group_setting = old_group_settings.iter().find(|g| g.field_id == field_id); + let has_same_content = + old_group_setting.is_some() && old_group_setting.unwrap().content == setting_content; + let view_editor = self.database_views.get_view_editor(view_id).await?; - view_editor.v_initialize_new_group(field_id).await?; + if !view_editor.is_grouping_field(field_id).await || !has_same_content { + view_editor.v_initialize_new_group(field_id).await?; + } Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 0e2d886655fb..1d5d8cf1b4e3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -401,15 +401,12 @@ impl DatabaseViewEditor { /// Called when the user changes the grouping field pub async fn v_initialize_new_group(&self, field_id: &str) -> FlowyResult<()> { - let is_grouping_field = self.is_grouping_field(field_id).await; - if !is_grouping_field { - self.v_group_by_field(field_id).await?; - - if let Some(view) = self.delegate.get_view(&self.view_id).await { - let setting = database_view_setting_pb_from_view(view); - notify_did_update_setting(&self.view_id, setting).await; - } + if let Some(view) = self.delegate.get_view(&self.view_id).await { + let setting = database_view_setting_pb_from_view(view); + notify_did_update_setting(&self.view_id, setting).await; } + + self.v_group_by_field(field_id).await?; Ok(()) } 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 8a2827a1078c..1402793264f7 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 @@ -6,7 +6,7 @@ use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use flowy_error::FlowyResult; +use flowy_error::{internal_error, FlowyResult}; use crate::entities::{ FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, @@ -27,13 +27,13 @@ pub struct DateGroupConfiguration { } impl DateGroupConfiguration { - fn from_json(s: &str) -> Result { + pub fn from_json(s: &str) -> Result { serde_json::from_str(s) } #[allow(dead_code)] - fn to_json(&self) -> Result { - serde_json::to_string(self) + pub fn to_json(&self) -> FlowyResult { + serde_json::to_string(self).map_err(internal_error) } } From e6f5e4e2aa2a0df7cbf448736102a216301886aa Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Sat, 11 May 2024 13:46:01 +0330 Subject: [PATCH 2/5] style: added i18n for date field group conditions --- .../database/board/application/board_bloc.dart | 17 +++++++++-------- frontend/resources/translations/en.json | 12 +++++++++++- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart index 94ae9dd305bf..c02b4eaead8c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart @@ -16,8 +16,9 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:intl/intl.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; import '../../application/database_controller.dart'; import '../../application/field/field_controller.dart'; @@ -593,13 +594,13 @@ class BoardBloc extends Bloc { ); final diff = targetDateTimeDay.difference(nowDay).inDays; return switch (diff) { - 0 => "Today", - -1 => "Yesterday", - 1 => "Tomorrow", - -7 => "Last 7 days", - 2 => "Next 7 days", - -30 => "Last 30 days", - 8 => "Next 30 days", + 0 => LocaleKeys.board_dateCondition_today.tr(), + -1 => LocaleKeys.board_dateCondition_yesterday.tr(), + 1 => LocaleKeys.board_dateCondition_tomorrow.tr(), + -7 => LocaleKeys.board_dateCondition_lastSevenDays.tr(), + 2 => LocaleKeys.board_dateCondition_nextSevenDays.tr(), + -30 => LocaleKeys.board_dateCondition_lastThirtyDays.tr(), + 8 => LocaleKeys.board_dateCondition_nextThirtyDays.tr(), _ => DateFormat("MMM y").format(targetDateTimeDay) }; default: diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 7e6af1b193ad..0403e5682c10 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1206,6 +1206,15 @@ "showGroup": "Show group", "showGroupContent": "Are you sure you want to show this group on the board?", "failedToLoad": "Failed to load board view" + }, + "dateCondition": { + "today": "Today", + "yesterday": "Yesterday", + "tomorrow": "Tomorrow", + "lastSevenDays": "Last 7 days", + "nextSevenDays": "Next 7 days", + "lastThirtyDays": "Last 30 days", + "nextThirtyDays": "Next 30 days" } }, "calendar": { @@ -1624,4 +1633,5 @@ "betaTooltip": "We currently only support searching for pages", "fromTrashHint": "From trash" } -} \ No newline at end of file +} + From f2f87b32482769e01efebaaba712913c30979fb8 Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Sat, 11 May 2024 14:13:23 +0330 Subject: [PATCH 3/5] fix: flutter analyze --- .../test/bloc_test/board_test/group_by_date_test.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart index 71576174cad4..3a572c82b224 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart @@ -20,7 +20,6 @@ void main() { test('group by date field test', () async { final context = await boardTest.createTestBoard(); final boardBloc = BoardBloc( - view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); @@ -66,7 +65,6 @@ void main() { test('group by date field with condition', () async { final context = await boardTest.createTestBoard(); final boardBloc = BoardBloc( - view: context.gridView, databaseController: DatabaseController(view: context.gridView), )..add(const BoardEvent.initial()); await boardResponseFuture(); From 55f9a575ae3d99151dbeb371d9e4ace9bf10e49f Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Sat, 11 May 2024 14:54:17 +0330 Subject: [PATCH 4/5] fix: test use i18n --- .../test/bloc_test/board_test/group_by_date_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart index 3a572c82b224..8208501f06e4 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_date_test.dart @@ -7,6 +7,8 @@ import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'util.dart'; @@ -58,7 +60,8 @@ void main() { assert(boardBloc.groupControllers.values.length == 2); assert( - boardBloc.boardController.groupDatas.last.headerData.groupName == "Today", + boardBloc.boardController.groupDatas.last.headerData.groupName == + LocaleKeys.board_dateCondition_today.tr(), ); }); From d96bdee4165efd22b5eef98c103f5e15f7a8d91d Mon Sep 17 00:00:00 2001 From: Mohammad Zolfaghari Date: Sun, 19 May 2024 18:57:25 +0330 Subject: [PATCH 5/5] fix: more localization --- .../board/application/board_bloc.dart | 15 ++++++------- .../widgets/group/database_group.dart | 21 ++++++++++--------- frontend/resources/translations/en.json | 1 + 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart index c02b4eaead8c..0fa07a69c82c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart @@ -19,6 +19,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:calendar_view/calendar_view.dart'; import '../../application/database_controller.dart'; import '../../application/field/field_controller.dart'; @@ -575,7 +576,12 @@ class BoardBloc extends Bloc { ? "MMM dd y" : "dd y"; - return 'Week of ${DateFormat(beginningOfWeekFormat).format(beginningOfWeek)} - ${DateFormat(endOfWeekFormat).format(endOfWeek)}'; + return LocaleKeys.board_dateCondition_weekOf.tr( + args: [ + DateFormat(beginningOfWeekFormat).format(beginningOfWeek), + DateFormat(endOfWeekFormat).format(endOfWeek), + ], + ); case DateConditionPB.Month: return DateFormat("MMM y").format(targetDateTime); case DateConditionPB.Year: @@ -586,12 +592,7 @@ class BoardBloc extends Bloc { targetDateTime.month, targetDateTime.day, ); - final now = DateTime.now(); - final nowDay = DateTime( - now.year, - now.month, - now.day, - ); + final nowDay = DateTime.now().withoutTime; final diff = targetDateTimeDay.difference(nowDay).inDays; return switch (diff) { 0 => LocaleKeys.board_dateCondition_today.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart index 5ff1e404075a..f8320aa3361a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart @@ -47,6 +47,16 @@ class DatabaseGroupList extends StatelessWidget { ); final showHideUngroupedToggle = field?.fieldType != FieldType.Checkbox; + + DateGroupConfigurationPB? config; + if (field != null) { + final gs = state.groupSettings + .firstWhereOrNull((gs) => gs.fieldId == field.id); + config = gs != null + ? DateGroupConfigurationPB.fromBuffer(gs.content) + : null; + } + final children = [ if (showHideUngroupedToggle) ...[ SizedBox( @@ -116,16 +126,7 @@ class DatabaseGroupList extends StatelessWidget { name: condition.name, condition: condition.value, onSelected: onDismissed, - checked: () { - final gs = state.groupSettings - .firstWhereOrNull((gs) => gs.fieldId == field.id); - if (gs == null) { - return false; - } - final config = - DateGroupConfigurationPB.fromBuffer(gs.content); - return config.condition == condition; - }(), + checked: config?.condition == condition, ), ), ], diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 9ea659082998..fb1835d5eb8e 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -1254,6 +1254,7 @@ "failedToLoad": "Failed to load board view" }, "dateCondition": { + "weekOf": "Week of {} - {}", "today": "Today", "yesterday": "Yesterday", "tomorrow": "Tomorrow",