diff --git a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart b/frontend/appflowy_flutter/integration_test/board/board_row_test.dart index a576dbe1c2..32abba1888 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_row_test.dart +++ b/frontend/appflowy_flutter/integration_test/board/board_row_test.dart @@ -1,11 +1,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/widgets/card/card.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import '../util/util.dart'; +import '../util/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -46,5 +49,56 @@ void main() { await tester.tapButtonWithName(LocaleKeys.button_duplicate.tr()); expect(find.textContaining(name, findRichText: true), findsNWidgets(2)); }); + + testWidgets('add new group', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Board); + + // assert number of groups + tester.assertNumberOfGroups(4); + + // scroll the board horizontally to ensure add new group button appears + await tester.scrollBoardToEnd(); + + // assert and click on add new group button + tester.assertNewGroupTextField(false); + await tester.tapNewGroupButton(); + tester.assertNewGroupTextField(true); + + // enter new group name and submit + await tester.enterNewGroupName('needs design', submit: true); + + // assert number of groups has increased + tester.assertNumberOfGroups(5); + + // assert text field has disappeared + await tester.scrollBoardToEnd(); + tester.assertNewGroupTextField(false); + + // click on add new group button + await tester.tapNewGroupButton(); + tester.assertNewGroupTextField(true); + + // type some things + await tester.enterNewGroupName('needs planning', submit: false); + + // click on clear button and assert empty contents + await tester.clearNewGroupTextField(); + + // press escape to cancel + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + tester.assertNewGroupTextField(false); + + // click on add new group button + await tester.tapNewGroupButton(); + tester.assertNewGroupTextField(true); + + // press elsewhere to cancel + await tester.tap(find.byType(AppFlowyBoard)); + await tester.pumpAndSettle(); + tester.assertNewGroupTextField(false); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index b91511e5d3..907f41b57b 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart'; +import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_day.dart'; import 'package:appflowy/plugins/database_view/calendar/presentation/calendar_event_card.dart'; @@ -59,6 +60,7 @@ import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; +import 'package:appflowy_board/appflowy_board.dart'; import 'package:calendar_view/calendar_view.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -1390,6 +1392,84 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(findCreateButton); } + void assertNumberOfGroups(int number) { + final groups = find.byType(BoardColumnHeader, skipOffstage: false); + expect(groups, findsNWidgets(number)); + } + + Future scrollBoardToEnd() async { + final scrollable = find + .descendant( + of: find.byType(AppFlowyBoard), + matching: find.byWidgetPredicate( + (widget) => widget is Scrollable && widget.axis == Axis.horizontal, + ), + ) + .first; + await scrollUntilVisible( + find.byType(BoardTrailing), + 300, + scrollable: scrollable, + ); + } + + Future tapNewGroupButton() async { + final button = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byWidgetPredicate( + (widget) => widget is FlowySvg && widget.svg == FlowySvgs.add_s, + ), + ); + expect(button, findsOneWidget); + await tapButton(button); + } + + void assertNewGroupTextField(bool isVisible) { + final textField = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byType(TextField), + ); + if (isVisible) { + expect(textField, findsOneWidget); + } else { + expect(textField, findsNothing); + } + } + + Future enterNewGroupName(String name, {required bool submit}) async { + final textField = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byType(TextField), + ); + await enterText(textField, name); + await pumpAndSettle(); + if (submit) { + await testTextInput.receiveAction(TextInputAction.done); + await pumpAndSettle(); + } + } + + Future clearNewGroupTextField() async { + final textField = find.descendant( + of: find.byType(BoardTrailing), + matching: find.byType(TextField), + ); + await tapButton( + find.descendant( + of: textField, + matching: find.byWidgetPredicate( + (widget) => + widget is FlowySvg && widget.svg == FlowySvgs.close_filled_m, + ), + ), + ); + final textFieldWidget = widget(textField); + assert( + textFieldWidget.controller != null && + textFieldWidget.controller!.text.isEmpty, + ); + } + Future tapTabBarLinkedViewByViewName(String name) async { final viewButton = findTabBarLinkViewByViewName(name); await tapButton(viewButton); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart index 7b14af8886..26233317b0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/application/group/group_service.dart @@ -37,4 +37,15 @@ class GroupBackendService { } return DatabaseEventUpdateGroup(payload).send(); } + + Future> createGroup({ + required String name, + String groupConfigId = "", + }) { + final payload = CreateGroupPayloadPB.create() + ..viewId = viewId + ..name = name; + + return DatabaseEventCreateGroup(payload).send(); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart index f9cfb22c02..30694c03d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/application/board_bloc.dart @@ -98,6 +98,10 @@ class BoardBloc extends Bloc { (err) => Log.error(err), ); }, + createGroup: (name) async { + final result = await groupBackendSvc.createGroup(name: name); + result.fold((_) {}, (err) => Log.error(err)); + }, didCreateRow: (group, row, int? index) { emit( state.copyWith( @@ -346,6 +350,7 @@ class BoardEvent with _$BoardEvent { const factory BoardEvent.initial() = _InitialBoard; const factory BoardEvent.createBottomRow(String groupId) = _CreateBottomRow; const factory BoardEvent.createHeaderRow(String groupId) = _CreateHeaderRow; + const factory BoardEvent.createGroup(String name) = _CreateGroup; const factory BoardEvent.startEditingHeader(String groupId) = _StartEditingHeader; const factory BoardEvent.endEditingHeader(String groupId, String groupName) = diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart index f2d584d9be..c2260a2ff0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/board_page.dart @@ -16,12 +16,12 @@ import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; -import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart' hide Card; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../widgets/card/cells/card_cell.dart'; @@ -127,6 +127,7 @@ class BoardContent extends StatefulWidget { class _BoardContentState extends State { late AppFlowyBoardScrollController scrollManager; + late final ScrollController scrollController; final renderHook = RowCardRenderHook(); final config = const AppFlowyBoardConfig( @@ -138,6 +139,7 @@ class _BoardContentState extends State { super.initState(); scrollManager = AppFlowyBoardScrollController(); + scrollController = ScrollController(); renderHook.addSelectOptionHook((options, groupId, _) { // The cell should hide if the option id is equal to the groupId. final isInGroup = @@ -172,7 +174,7 @@ class _BoardContentState extends State { Expanded( child: AppFlowyBoard( boardScrollController: scrollManager, - scrollController: ScrollController(), + scrollController: scrollController, controller: context.read().boardController, headerBuilder: (_, groupData) => BlocProvider.value( @@ -183,6 +185,7 @@ class _BoardContentState extends State { ), ), footerBuilder: _buildFooter, + trailing: BoardTrailing(scrollController: scrollController), cardBuilder: (_, column, columnItem) => _buildCard( context, column, @@ -348,3 +351,109 @@ class _BoardContentState extends State { ); } } + +class BoardTrailing extends StatefulWidget { + final ScrollController scrollController; + const BoardTrailing({required this.scrollController, super.key}); + + @override + State createState() => _BoardTrailingState(); +} + +class _BoardTrailingState extends State { + bool isEditing = false; + late final TextEditingController _textController; + late final FocusNode _focusNode; + + void _cancelAddNewGroup() { + _textController.clear(); + setState(() { + isEditing = false; + }); + } + + @override + void initState() { + super.initState(); + _textController = TextEditingController(); + _focusNode = FocusNode( + onKeyEvent: (node, event) { + if (_focusNode.hasFocus && + event.logicalKey == LogicalKeyboardKey.escape) { + _cancelAddNewGroup(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + )..addListener(() { + if (!_focusNode.hasFocus) { + _cancelAddNewGroup(); + } + }); + } + + @override + Widget build(BuildContext context) { + // call after every setState + WidgetsBinding.instance.addPostFrameCallback((_) { + if (isEditing) { + _focusNode.requestFocus(); + widget.scrollController.jumpTo( + widget.scrollController.position.maxScrollExtent, + ); + } + }); + + return Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Align( + alignment: AlignmentDirectional.topStart, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: isEditing + ? SizedBox( + width: 256, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _textController, + focusNode: _focusNode, + decoration: InputDecoration( + suffixIcon: Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8.0), + child: FlowyIconButton( + icon: const FlowySvg(FlowySvgs.close_filled_m), + hoverColor: Colors.transparent, + onPressed: () => _textController.clear(), + ), + ), + suffixIconConstraints: + BoxConstraints.loose(const Size(20, 24)), + border: const UnderlineInputBorder(), + contentPadding: const EdgeInsets.fromLTRB(8, 4, 8, 8), + isDense: true, + ), + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + onSubmitted: (groupName) => context + .read() + .add(BoardEvent.createGroup(groupName)), + ), + ), + ) + : FlowyTooltip( + message: LocaleKeys.board_column_createNewColumn.tr(), + child: FlowyIconButton( + width: 26, + icon: const FlowySvg(FlowySvgs.add_s), + iconColorOnHover: Theme.of(context).colorScheme.onSurface, + onPressed: () => setState(() { + isEditing = true; + }), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index 74658d9c67..bede73fec4 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -45,8 +45,8 @@ packages: dependency: "direct main" description: path: "." - ref: "6aba8dd" - resolved-ref: "6aba8ddd86839ca09b997cb2457f013236e0c337" + ref: "1a329c2" + resolved-ref: "1a329c21921c0d19871bea3237b7d80fe131f2ed" url: "https://github.com/AppFlowy-IO/appflowy-board.git" source: git version: "0.1.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 24f060e392..8e38938dda 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -43,7 +43,7 @@ dependencies: # path: packages/appflowy_board git: url: https://github.com/AppFlowy-IO/appflowy-board.git - ref: 6aba8dd + ref: 1a329c2 appflowy_editor: ^1.5.1 appflowy_popover: path: packages/appflowy_popover diff --git a/frontend/resources/flowy_icons/24x/close_filled.svg b/frontend/resources/flowy_icons/24x/close_filled.svg new file mode 100644 index 0000000000..e6dffe953a --- /dev/null +++ b/frontend/resources/flowy_icons/24x/close_filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 09ff958b53..b38493cf76 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -755,7 +755,8 @@ "board": { "column": { "createNewCard": "New", - "renameGroupTooltip": "Press to rename group" + "renameGroupTooltip": "Press to rename group", + "createNewColumn": "Add a new group" }, "menuName": "Board", "showUngrouped": "Show ungrouped items", diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs index 033d2cb81a..bd46d337b3 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs @@ -3,7 +3,7 @@ use event_integration::EventIntegrationTest; #[tokio::test] async fn update_group_name_test() { let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await.workspace; + let current_workspace = test.get_current_workspace().await; let board_view = test .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; 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 7200c2987e..4553cee745 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 @@ -197,3 +197,36 @@ impl From for GroupChangeset { } } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct CreateGroupPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub group_config_id: String, + + #[pb(index = 3)] + pub name: String, +} + +#[derive(Debug, Clone)] +pub struct CreateGroupParams { + pub view_id: String, + pub group_config_id: String, + pub name: String, +} + +impl TryFrom for CreateGroupParams { + type Error = ErrorCode; + + fn try_from(value: CreateGroupPayloadPB) -> Result { + let view_id = NotEmptyStr::parse(value.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + let name = NotEmptyStr::parse(value.name).map_err(|_| ErrorCode::ViewIdIsInvalid)?; + Ok(CreateGroupParams { + view_id: view_id.0, + group_config_id: value.group_config_id, + name: name.0, + }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index c49c783339..ccebbb8fa5 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -741,6 +741,20 @@ pub(crate) async fn move_group_row_handler( Ok(()) } +#[tracing::instrument(level = "debug", skip(manager), err)] +pub(crate) async fn create_group_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> FlowyResult<()> { + let manager = upgrade_manager(manager)?; + let params: CreateGroupParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + database_editor + .create_group(¶ms.view_id, ¶ms.name) + .await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip(manager), err)] pub(crate) async fn get_databases_handler( manager: AFPluginState>, diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 4bb000afae..436a9ec814 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -60,6 +60,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::GetGroups, get_groups_handler) .event(DatabaseEvent::GetGroup, get_group_handler) .event(DatabaseEvent::UpdateGroup, update_group_handler) + .event(DatabaseEvent::CreateGroup, create_group_handler) // Database .event(DatabaseEvent::GetDatabases, get_databases_handler) // Calendar @@ -284,6 +285,9 @@ pub enum DatabaseEvent { #[event(input = "UpdateGroupPB")] UpdateGroup = 113, + #[event(input = "CreateGroupPayloadPB")] + CreateGroup = 114, + /// Returns all the databases #[event(output = "RepeatedDatabaseDescriptionPB")] GetDatabases = 120, 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 ed2851b6fa..1ff4e8d552 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 @@ -442,7 +442,7 @@ impl DatabaseEditor { let row_detail = self.database.lock().get_row_detail(&row_order.id); if let Some(row_detail) = row_detail { for view in self.database_views.editors().await { - view.v_did_create_row(&row_detail, &group_id, index).await; + view.v_did_create_row(&row_detail, index).await; } return Ok(Some(row_detail)); } @@ -961,6 +961,12 @@ impl DatabaseEditor { Ok(()) } + pub async fn create_group(&self, view_id: &str, name: &str) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(view_id).await?; + view_editor.v_create_group(name).await?; + Ok(()) + } + #[tracing::instrument(level = "trace", skip_all)] pub async fn set_layout_setting( &self, 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 0c63a64d73..11bc2a4976 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 @@ -14,8 +14,8 @@ use lib_dispatch::prelude::af_spawn; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, DeleteGroupParams, DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, - GroupRowsNotificationPB, InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, - RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, + InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB, + SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::cell::CellCache; @@ -126,39 +126,22 @@ impl DatabaseViewEditor { .send(); } - pub async fn v_did_create_row( - &self, - row_detail: &RowDetail, - group_id: &Option, - index: usize, - ) { - let changes: RowsChangePB; + pub async fn v_did_create_row(&self, row_detail: &RowDetail, index: usize) { // Send the group notification if the current view has groups - match group_id.as_ref() { - None => { - let row = InsertedRowPB::new(RowMetaPB::from(row_detail)).with_index(index as i32); - changes = RowsChangePB::from_insert(row); - }, - Some(group_id) => { - self - .mut_group_controller(|group_controller, _| { - group_controller.did_create_row(row_detail, group_id); - Ok(()) - }) - .await; + if let Some(controller) = self.group_controller.write().await.as_mut() { + let changesets = controller.did_create_row(row_detail, index); - let inserted_row = InsertedRowPB { - row_meta: RowMetaPB::from(row_detail), - index: Some(index as i32), - is_new: true, - }; - let changeset = - GroupRowsNotificationPB::insert(group_id.clone(), vec![inserted_row.clone()]); + for changeset in changesets { notify_did_update_group_rows(changeset).await; - changes = RowsChangePB::from_insert(inserted_row); - }, + } } + let inserted_row = InsertedRowPB { + row_meta: RowMetaPB::from(row_detail), + index: Some(index as i32), + is_new: true, + }; + let changes = RowsChangePB::from_insert(inserted_row); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); @@ -168,16 +151,22 @@ impl DatabaseViewEditor { pub async fn v_did_delete_row(&self, row: &Row) { // Send the group notification if the current view has groups; let result = self - .mut_group_controller(|group_controller, field| { - group_controller.did_delete_delete_row(row, &field) - }) + .mut_group_controller(|group_controller, _| group_controller.did_delete_row(row)) .await; if let Some(result) = result { - tracing::trace!("Delete row in view changeset: {:?}", result.row_changesets); + tracing::trace!("Delete row in view changeset: {:?}", result); for changeset in result.row_changesets { notify_did_update_group_rows(changeset).await; } + if let Some(deleted_group) = result.deleted_group { + let payload = GroupChangesPB { + view_id: self.view_id.clone(), + deleted_groups: vec![deleted_group.group_id], + ..Default::default() + }; + notify_did_update_num_of_groups(&self.view_id, payload).await; + } } let changes = RowsChangePB::from_delete(row.id.clone().into_inner()); send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) @@ -319,7 +308,7 @@ impl DatabaseViewEditor { .read() .await .as_ref()? - .groups() + .get_all_groups() .into_iter() .filter(|group| group.is_visible) .map(|group_data| GroupPB::from(group_data.clone())) @@ -371,6 +360,36 @@ impl DatabaseViewEditor { Ok(()) } + pub async fn v_create_group(&self, name: &str) -> FlowyResult<()> { + let mut old_field: Option = None; + let result = if let Some(controller) = self.group_controller.write().await.as_mut() { + let create_group_results = controller.create_group(name.to_string())?; + old_field = self.delegate.get_field(controller.field_id()); + create_group_results + } else { + (None, None) + }; + + if let Some(old_field) = old_field { + if let (Some(type_option_data), Some(payload)) = result { + self + .delegate + .update_field(&self.view_id, type_option_data, old_field) + .await?; + + let group_changes = GroupChangesPB { + view_id: self.view_id.clone(), + inserted_groups: vec![payload], + ..Default::default() + }; + + notify_did_update_num_of_groups(&self.view_id, group_changes).await; + } + } + + Ok(()) + } + pub async fn v_delete_group(&self, _params: DeleteGroupParams) -> FlowyResult<()> { Ok(()) } @@ -671,7 +690,7 @@ impl DatabaseViewEditor { .await?; let new_groups = new_group_controller - .groups() + .get_all_groups() .into_iter() .map(|group| GroupPB::from(group.clone())) .collect(); diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs index 666ff41792..26af4036d9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs @@ -25,15 +25,27 @@ pub trait SelectTypeOptionSharedAction: Send + Sync { /// If the option already exists, it will be updated. /// If the option does not exist, it will be inserted at the beginning. fn insert_option(&mut self, new_option: SelectOption) { + self.insert_option_at_index(new_option, None); + } + + fn insert_option_at_index(&mut self, new_option: SelectOption, new_index: Option) { let options = self.mut_options(); + let safe_new_index = new_index.map(|index| { + if index > options.len() { + options.len() + } else { + index + } + }); + if let Some(index) = options .iter() .position(|option| option.id == new_option.id || option.name == new_option.name) { options.remove(index); - options.insert(index, new_option); + options.insert(safe_new_index.unwrap_or(index), new_option); } else { - options.insert(0, new_option); + options.insert(safe_new_index.unwrap_or(0), new_option); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 1c28fce8ad..dbc11f26bc 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -54,7 +54,7 @@ pub trait GroupCustomize: Send + Sync { &mut self, row: &Row, cell_data: &::CellData, - ) -> Vec; + ) -> (Option, Vec); /// Move row from one group to another fn move_row( @@ -71,27 +71,71 @@ pub trait GroupCustomize: Send + Sync { ) -> Option { None } + + fn generate_new_group( + &mut self, + _name: String, + ) -> FlowyResult<(Option, Option)> { + Ok((None, None)) + } } /// Defines the shared actions any group controller can perform. #[async_trait] pub trait GroupControllerOperation: Send + Sync { - /// The field that is used for grouping the rows + /// Returns the id of field that is being used to group the rows fn field_id(&self) -> &str; - /// Returns number of groups the current field has - fn groups(&self) -> Vec<&GroupData>; + /// Returns all of the groups currently managed by the controller + fn get_all_groups(&self) -> Vec<&GroupData>; - /// Returns the index and the group data with group_id + /// Returns the index and the group data with the given group id if it exists. + /// + /// * `group_id` - A string slice that is used to match the group fn get_group(&self, group_id: &str) -> Option<(usize, GroupData)>; - /// Separates the rows into different groups + /// Sort the rows into the different groups. + /// + /// * `rows`: rows to be inserted + /// * `field`: reference to the field being sorted (currently unused) fn fill_groups(&mut self, rows: &[&RowDetail], field: &Field) -> FlowyResult<()>; - /// Remove the group with from_group_id and insert it to the index with to_group_id + /// Create a new group, currently only supports single and multi-select. + /// + /// Returns a new type option data for the grouping field if it's altered. + /// + /// * `name`: name of the new group + fn create_group( + &mut self, + name: String, + ) -> FlowyResult<(Option, Option)>; + + /// Reorders the group in the group controller. + /// + /// * `from_group_id`: id of the group being moved + /// * `to_group_id`: id of the group whose index is the one at which the + /// reordered group will be placed fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()>; - /// Insert/Remove the row to the group if the corresponding cell data is changed + /// Adds a newly-created row to one or more suitable groups. + /// + /// Returns a changeset payload to be sent as a notification. + /// + /// * `row_detail`: the newly-created row + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec; + + /// Called after a row's cell data is changed, this moves the row to the + /// correct group. It may also insert a new group and/or remove an old group. + /// + /// Returns the inserted and removed groups if necessary for notification. + /// + /// * `old_row_detail`: + /// * `row_detail`: + /// * `field`: fn did_update_group_row( &mut self, old_row_detail: &Option, @@ -99,22 +143,31 @@ pub trait GroupControllerOperation: Send + Sync { field: &Field, ) -> FlowyResult; - /// Remove the row from the group if the row gets deleted - fn did_delete_delete_row( - &mut self, - row: &Row, - field: &Field, - ) -> FlowyResult; + /// Called after the row is deleted, this removes the row from the group. + /// A group could be deleted as a result. + /// + /// Returns a the removed group when this occurs. + fn did_delete_row(&mut self, row: &Row) -> FlowyResult; - /// Move the row from one group to another group + /// Reorders a row within the current group or move the row to another group. + /// + /// * `context`: information about the row being moved and its destination fn move_group_row(&mut self, context: MoveGroupRowContext) -> FlowyResult; - /// Update the group if the corresponding field is changed + /// Updates the groups after a field change. (currently never does anything) + /// + /// * `field`: new changeset fn did_update_group_field(&mut self, field: &Field) -> FlowyResult>; + /// Updates the name and/or visibility of groups. + /// + /// Returns a non-empty `TypeOptionData` when the changes require a change + /// in the field type option data. + /// + /// * `changesets`: list of changesets to be made to one or more groups async fn apply_group_changeset( &mut self, - changeset: &GroupChangesets, + changesets: &GroupChangesets, ) -> FlowyResult; } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index b71cdddf37..97821aac59 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -124,6 +124,7 @@ where /// Returns the no `status` group /// /// We take the `id` of the `field` as the no status group id + #[allow(dead_code)] pub(crate) fn get_no_status_group(&self) -> Option<&GroupData> { self.group_by_id.get(&self.field.id) } @@ -249,7 +250,7 @@ where /// /// # Arguments /// - /// * `generated_group_configs`: the generated groups contains a list of [GeneratedGroupConfig]. + /// * `generated_groups`: the generated groups contains a list of [GeneratedGroupConfig]. /// /// Each [FieldType] can implement the [GroupGenerator] trait in order to generate different /// groups. For example, the FieldType::Checkbox has the [CheckboxGroupGenerator] that implements diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index ab847755b2..239e291036 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -11,7 +11,7 @@ use serde::Serialize; use flowy_error::FlowyResult; use crate::entities::{ - FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB, + FieldType, GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, RowMetaPB, }; use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser}; use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData}; @@ -38,9 +38,6 @@ pub trait GroupController: GroupControllerOperation + Send + Sync { /// Called before the row was created. fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str); - - /// Called after the row was created. - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str); } #[async_trait] @@ -184,7 +181,7 @@ where &self.grouping_field_id } - fn groups(&self) -> Vec<&GroupData> { + fn get_all_groups(&self) -> Vec<&GroupData> { self.context.groups() } @@ -233,10 +230,70 @@ where Ok(()) } + fn create_group( + &mut self, + name: String, + ) -> FlowyResult<(Option, Option)> { + self.generate_new_group(name) + } + fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { self.context.move_group(from_group_id, to_group_id) } + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec { + let cell = match row_detail.row.cells.get(&self.grouping_field_id) { + None => self.placeholder_cell(), + Some(cell) => Some(cell.clone()), + }; + + let mut changesets: Vec = vec![]; + if let Some(cell) = cell { + let cell_data = ::CellData::from(&cell); + + let mut suitable_group_ids = vec![]; + + for group in self.get_all_groups() { + if self.can_group(&group.filter_content, &cell_data) { + suitable_group_ids.push(group.id.clone()); + let changeset = GroupRowsNotificationPB::insert( + group.id.clone(), + vec![InsertedRowPB { + row_meta: row_detail.into(), + index: Some(index as i32), + is_new: true, + }], + ); + changesets.push(changeset); + } + } + if !suitable_group_ids.is_empty() { + for group_id in suitable_group_ids.iter() { + if let Some(group) = self.context.get_mut_group(group_id) { + group.add_row(row_detail.clone()); + } + } + } else if let Some(no_status_group) = self.context.get_mut_no_status_group() { + no_status_group.add_row(row_detail.clone()); + let changeset = GroupRowsNotificationPB::insert( + no_status_group.id.clone(), + vec![InsertedRowPB { + row_meta: row_detail.into(), + index: Some(index as i32), + is_new: true, + }], + ); + changesets.push(changeset); + } + } + + changesets + } + fn did_update_group_row( &mut self, old_row_detail: &Option, @@ -278,26 +335,21 @@ where Ok(result) } - fn did_delete_delete_row( - &mut self, - row: &Row, - _field: &Field, - ) -> FlowyResult { - // if the cell_rev is none, then the row must in the default group. + fn did_delete_row(&mut self, row: &Row) -> FlowyResult { let mut result = DidMoveGroupRowResult { deleted_group: None, row_changesets: vec![], }; + // early return if the row is not in the default group if let Some(cell) = row.cells.get(&self.grouping_field_id) { let cell_data = ::CellData::from(cell); if !cell_data.is_cell_empty() { - tracing::error!("did_delete_delete_row {:?}", cell); - result.row_changesets = self.delete_row(row, &cell_data); + (result.deleted_group, result.row_changesets) = self.delete_row(row, &cell_data); return Ok(result); } } - match self.context.get_no_status_group() { + match self.context.get_mut_no_status_group() { None => { tracing::error!("Unexpected None value. It should have the no status group"); }, @@ -305,6 +357,7 @@ where if !no_status_group.contains_row(&row.id) { tracing::error!("The row: {:?} should be in the no status group", row.id); } + no_status_group.remove_row(&row.id); result.row_changesets = vec![GroupRowsNotificationPB::delete( no_status_group.id.clone(), vec![row.id.clone().into_inner()], diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index 490b80dca5..64a7706cc2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -3,7 +3,7 @@ use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; +use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; use crate::services::cell::insert_checkbox_cell; use crate::services::field::{ CheckboxCellDataParser, CheckboxTypeOption, TypeOption, CHECK, UNCHECK, @@ -109,7 +109,7 @@ impl GroupCustomize for CheckboxGroupController { &mut self, row: &Row, _cell_data: &::CellData, - ) -> Vec { + ) -> (Option, Vec) { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -122,7 +122,7 @@ impl GroupCustomize for CheckboxGroupController { changesets.push(changeset); } }); - changesets + (None, changesets) } fn move_row( @@ -155,12 +155,6 @@ impl GroupController for CheckboxGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct CheckboxGroupBuilder(); 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 1797a270b9..316d628e28 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 @@ -178,8 +178,8 @@ impl GroupCustomize for DateGroupController { fn delete_row( &mut self, row: &Row, - _cell_data: &::CellData, - ) -> Vec { + cell_data: &::CellData, + ) -> (Option, Vec) { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -192,7 +192,23 @@ impl GroupCustomize for DateGroupController { changesets.push(changeset); } }); - changesets + + let setting_content = self.context.get_setting_content(); + let deleted_group = + match self + .context + .get_group(&group_id(cell_data, &self.type_option, &setting_content)) + { + Some((_, group)) if group.rows.len() == 1 => Some(group.clone()), + _ => None, + }; + + let deleted_group = deleted_group.map(|group| { + let _ = self.context.delete_group(&group.id); + group.into() + }); + + (deleted_group, changesets) } fn move_row( @@ -247,12 +263,6 @@ impl GroupController for DateGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct DateGroupBuilder(); diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index 19d02c0285..1de5741312 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -6,7 +6,7 @@ use collab_database::rows::{Cells, Row, RowDetail}; use flowy_error::FlowyResult; -use crate::entities::GroupChangesPB; +use crate::entities::{GroupChangesPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB}; use crate::services::group::action::{ DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, }; @@ -44,7 +44,7 @@ impl GroupControllerOperation for DefaultGroupController { &self.field_id } - fn groups(&self) -> Vec<&GroupData> { + fn get_all_groups(&self) -> Vec<&GroupData> { vec![&self.group] } @@ -59,10 +59,34 @@ impl GroupControllerOperation for DefaultGroupController { Ok(()) } + fn create_group( + &mut self, + _name: String, + ) -> FlowyResult<(Option, Option)> { + Ok((None, None)) + } + fn move_group(&mut self, _from_group_id: &str, _to_group_id: &str) -> FlowyResult<()> { Ok(()) } + fn did_create_row( + &mut self, + row_detail: &RowDetail, + index: usize, + ) -> Vec { + self.group.add_row(row_detail.clone()); + + vec![GroupRowsNotificationPB::insert( + self.group.id.clone(), + vec![InsertedRowPB { + row_meta: row_detail.into(), + index: Some(index as i32), + is_new: true, + }], + )] + } + fn did_update_group_row( &mut self, _old_row_detail: &Option, @@ -76,14 +100,15 @@ impl GroupControllerOperation for DefaultGroupController { }) } - fn did_delete_delete_row( - &mut self, - _row: &Row, - _field: &Field, - ) -> FlowyResult { + fn did_delete_row(&mut self, row: &Row) -> FlowyResult { + let mut changeset = GroupRowsNotificationPB::new(self.group.id.clone()); + if self.group.contains_row(&row.id) { + self.group.remove_row(&row.id); + changeset.deleted_rows.push(row.id.clone().into_inner()); + } Ok(DidMoveGroupRowResult { deleted_group: None, - row_changesets: vec![], + row_changesets: vec![changeset], }) } @@ -115,6 +140,4 @@ impl GroupController for DefaultGroupController { } fn will_create_row(&mut self, _cells: &mut Cells, _field: &Field, _group_id: &str) {} - - fn did_create_row(&mut self, _row_detail: &RowDetail, _group_id: &str) {} } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index c90c5df3d3..f7794a9624 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; +use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB}; +use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ MultiSelectTypeOption, SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, @@ -13,7 +14,7 @@ use crate::services::group::action::GroupCustomize; use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::{ add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group, - move_group_row, remove_select_option_row, GeneratedGroups, GroupChangeset, GroupContext, + move_group_row, remove_select_option_row, GeneratedGroups, Group, GroupChangeset, GroupContext, GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; @@ -69,14 +70,14 @@ impl GroupCustomize for MultiSelectGroupController { &mut self, row: &Row, cell_data: &::CellData, - ) -> Vec { + ) -> (Option, Vec) { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { if let Some(changeset) = remove_select_option_row(group, cell_data, row) { changesets.push(changeset); } }); - changesets + (None, changesets) } fn move_row( @@ -92,6 +93,20 @@ impl GroupCustomize for MultiSelectGroupController { }); group_changeset } + + fn generate_new_group( + &mut self, + name: String, + ) -> FlowyResult<(Option, Option)> { + let mut new_type_option = self.type_option.clone(); + let new_select_option = self.type_option.create_option(&name); + new_type_option.insert_option(new_select_option.clone()); + + let new_group = Group::new(new_select_option.id, new_select_option.name); + let inserted_group_pb = self.context.add_new_group(new_group)?; + + Ok((Some(new_type_option.into()), Some(inserted_group_pb))) + } } impl GroupController for MultiSelectGroupController { @@ -106,12 +121,6 @@ impl GroupController for MultiSelectGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct MultiSelectGroupBuilder; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index de94d8e1d0..a92c79c624 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,9 +1,10 @@ use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; +use flowy_error::FlowyResult; use serde::{Deserialize, Serialize}; -use crate::entities::{FieldType, GroupRowsNotificationPB}; +use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::cell::insert_select_option_cell; use crate::services::field::{ SelectOption, SelectOptionCellDataParser, SelectTypeOptionSharedAction, SingleSelectTypeOption, @@ -14,8 +15,8 @@ use crate::services::group::controller::{BaseGroupController, GroupController}; use crate::services::group::controller_impls::select_option_controller::util::*; use crate::services::group::entities::GroupData; use crate::services::group::{ - make_no_status_group, GeneratedGroups, GroupChangeset, GroupContext, GroupOperationInterceptor, - GroupsBuilder, MoveGroupRowContext, + make_no_status_group, GeneratedGroups, Group, GroupChangeset, GroupContext, + GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -70,14 +71,14 @@ impl GroupCustomize for SingleSelectGroupController { &mut self, row: &Row, cell_data: &::CellData, - ) -> Vec { + ) -> (Option, Vec) { let mut changesets = vec![]; self.context.iter_mut_status_groups(|group| { if let Some(changeset) = remove_select_option_row(group, cell_data, row) { changesets.push(changeset); } }); - changesets + (None, changesets) } fn move_row( @@ -93,6 +94,23 @@ impl GroupCustomize for SingleSelectGroupController { }); group_changeset } + + fn generate_new_group( + &mut self, + name: String, + ) -> FlowyResult<(Option, Option)> { + let mut new_type_option = self.type_option.clone(); + let new_select_option = self.type_option.create_option(&name); + new_type_option.insert_option_at_index( + new_select_option.clone(), + Some(new_type_option.options.len()), + ); + + let new_group = Group::new(new_select_option.id, new_select_option.name); + let inserted_group_pb = self.context.add_new_group(new_group)?; + + Ok((Some(new_type_option.into()), Some(inserted_group_pb))) + } } impl GroupController for SingleSelectGroupController { @@ -108,12 +126,6 @@ impl GroupController for SingleSelectGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct SingleSelectGroupBuilder(); diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index e68b1be0ed..4ed745cad5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -185,6 +185,7 @@ pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option { }, } } + pub fn generate_select_option_groups( _field_id: &str, options: &[SelectOption], diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index 4e85557101..475b871ffc 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -128,8 +128,8 @@ impl GroupCustomize for URLGroupController { fn delete_row( &mut self, row: &Row, - _cell_data: &::CellData, - ) -> Vec { + cell_data: &::CellData, + ) -> (Option, Vec) { let mut changesets = vec![]; self.context.iter_mut_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); @@ -142,7 +142,18 @@ impl GroupCustomize for URLGroupController { changesets.push(changeset); } }); - changesets + + let deleted_group = match self.context.get_group(&cell_data.data) { + Some((_, group)) if group.rows.len() == 1 => Some(group.clone()), + _ => None, + }; + + let deleted_group = deleted_group.map(|group| { + let _ = self.context.delete_group(&group.id); + group.into() + }); + + (deleted_group, changesets) } fn move_row( @@ -190,12 +201,6 @@ impl GroupController for URLGroupController { }, } } - - fn did_create_row(&mut self, row_detail: &RowDetail, group_id: &str) { - if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()) - } - } } pub struct URLGroupGenerator(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index a8a39c645a..330ecc9044 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -67,6 +67,9 @@ pub enum GroupScript { group_id: String, group_name: String, }, + CreateGroup { + name: String, + }, } pub struct DatabaseGroupTest { @@ -269,6 +272,11 @@ impl DatabaseGroupTest { assert_eq!(group_id, group.group_id, "group index: {}", group_index); assert_eq!(group_name, group.group_name, "group index: {}", group_index); }, + GroupScript::CreateGroup { name } => self + .editor + .create_group(&self.view_id, &name) + .await + .unwrap(), } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs index d9fb97a865..119986b04b 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs @@ -486,3 +486,19 @@ async fn group_group_by_other_field() { ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn group_manual_create_new_group() { + let mut test = DatabaseGroupTest::new().await; + let new_group_name = "Resumed"; + let scripts = vec![ + AssertGroupCount(4), + CreateGroup { + name: new_group_name.to_string(), + }, + AssertGroupCount(5), + ]; + test.run_scripts(scripts).await; + let new_group = test.group_at_index(4).await; + assert_eq!(new_group.group_name, new_group_name); +}