diff --git a/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart b/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart index 46d91766d9..f7eeb5d44b 100644 --- a/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart +++ b/frontend/appflowy_flutter/integration_test/board/board_hide_groups_test.dart @@ -3,15 +3,17 @@ import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_ import 'package:appflowy/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart'; import 'package:appflowy_backend/protobuf/flowy-folder2/view.pb.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; +import '../util/database_test_op.dart'; import '../util/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('board hide groups test', () { + group('board group options:', () { testWidgets('expand/collapse hidden groups', (tester) async { await tester.initializeAppFlowy(); await tester.tapGoButton(); @@ -76,6 +78,47 @@ void main() { expect(shownGroups, 4); }); }); + + testWidgets('delete a group', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + await tester.createNewPageWithName(layout: ViewLayoutPB.Board); + + expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 4); + + // tap group option button for the first group. Delete shouldn't show up + await tester.tapButton( + find + .descendant( + of: find.byType(BoardColumnHeader), + matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), + ) + .first, + ); + expect(find.byFlowySvg(FlowySvgs.delete_s), findsNothing); + + // dismiss the popup + await tester.sendKeyEvent(LogicalKeyboardKey.escape); + await tester.pumpAndSettle(); + + // tap group option button for the first group. Delete should show up + await tester.tapButton( + find + .descendant( + of: find.byType(BoardColumnHeader), + matching: find.byFlowySvg(FlowySvgs.details_horizontal_s), + ) + .at(1), + ); + expect(find.byFlowySvg(FlowySvgs.delete_s), findsOneWidget); + + // Tap the delete button and confirm + await tester.tapButton(find.byFlowySvg(FlowySvgs.delete_s)); + await tester.tapDialogOkButton(); + + // Expect number of groups to decrease by one + expect(tester.widgetList(find.byType(BoardColumnHeader)).length, 3); + }); } extension FlowySvgFinder on CommonFinders { 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 26233317b0..a153a49ae6 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 @@ -48,4 +48,14 @@ class GroupBackendService { return DatabaseEventCreateGroup(payload).send(); } + + Future> deleteGroup({ + required String groupId, + }) { + final payload = DeleteGroupPayloadPB.create() + ..viewId = viewId + ..groupId = groupId; + + return DatabaseEventDeleteGroup(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 bdbcf0114a..eea5388cb9 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 @@ -99,6 +99,10 @@ class BoardBloc extends Bloc { final result = await groupBackendSvc.createGroup(name: name); result.fold((_) {}, (err) => Log.error(err)); }, + deleteGroup: (groupId) async { + final result = await groupBackendSvc.deleteGroup(groupId: groupId); + result.fold((_) {}, (err) => Log.error(err)); + }, didCreateRow: (group, row, int? index) { emit( state.copyWith( @@ -496,6 +500,7 @@ class BoardEvent with _$BoardEvent { ) = _ToggleGroupVisibility; const factory BoardEvent.toggleHiddenSectionVisibility(bool isVisible) = _ToggleHiddenSectionVisibility; + const factory BoardEvent.deleteGroup(String groupId) = _DeleteGroup; const factory BoardEvent.reorderGroup(String fromGroupId, String toGroupId) = _ReorderGroup; const factory BoardEvent.didReceiveError(FlowyError error) = _DidReceiveError; diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart index 70db26df3d..01be3f0817 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_column_header.dart @@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/board/application/board_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/group.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; @@ -73,7 +74,6 @@ class _BoardColumnHeaderState extends State { Widget title = Expanded( child: FlowyText.medium( widget.groupData.headerData.groupName, - fontSize: 14, overflow: TextOverflow.ellipsis, ), ); @@ -92,7 +92,6 @@ class _BoardColumnHeaderState extends State { .add(BoardEvent.startEditingHeader(widget.groupData.id)), child: FlowyText.medium( widget.groupData.headerData.groupName, - fontSize: 14, overflow: TextOverflow.ellipsis, ), ), @@ -119,6 +118,7 @@ class _BoardColumnHeaderState extends State { const HSpace(4), FlowyTooltip( message: LocaleKeys.board_column_addToColumnTopTooltip.tr(), + preferBelow: false, child: FlowyIconButton( width: 20, icon: const FlowySvg(FlowySvgs.add_s), @@ -199,7 +199,7 @@ class _BoardColumnHeaderState extends State { Widget _groupOptionsButton(BuildContext context) { return AppFlowyPopover( clickHandler: PopoverClickHandler.gestureDetector, - margin: const EdgeInsets.fromLTRB(8, 8, 8, 4), + margin: const EdgeInsets.all(8), constraints: BoxConstraints.loose(const Size(168, 300)), direction: PopoverDirection.bottomWithLeftAligned, child: FlowyIconButton( @@ -209,29 +209,31 @@ class _BoardColumnHeaderState extends State { ), popupBuilder: (popoverContext) { final customGroupData = widget.groupData.customData as GroupData; + final isDefault = customGroupData.group.isDefault; final menuItems = GroupOptions.values.toList(); - if (!customGroupData.fieldType.canEditHeader) { + if (!customGroupData.fieldType.canEditHeader || isDefault) { menuItems.remove(GroupOptions.rename); } - return Column( + if (!customGroupData.fieldType.canDeleteGroup || isDefault) { + menuItems.remove(GroupOptions.delete); + } + return SeparatedColumn( mainAxisSize: MainAxisSize.min, + separatorBuilder: () => const VSpace(4), children: [ ...menuItems.map( (action) => SizedBox( height: GridSize.popoverItemHeight, - child: Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: FlowyButton( - leftIcon: FlowySvg(action.icon), - text: FlowyText.medium( - action.text, - overflow: TextOverflow.ellipsis, - ), - onTap: () { - action.call(context, customGroupData.group); - PopoverContainer.of(popoverContext).close(); - }, + child: FlowyButton( + leftIcon: FlowySvg(action.icon), + text: FlowyText.medium( + action.text, + overflow: TextOverflow.ellipsis, ), + onTap: () { + action.call(context, customGroupData.group); + PopoverContainer.of(popoverContext).close(); + }, ), ), ), @@ -244,7 +246,8 @@ class _BoardColumnHeaderState extends State { enum GroupOptions { rename, - hide; + hide, + delete; void call(BuildContext context, GroupPB group) { switch (this) { @@ -258,16 +261,28 @@ enum GroupOptions { .read() .add(BoardEvent.toggleGroupVisibility(group, false)); break; + case delete: + NavigatorAlertDialog( + title: LocaleKeys.board_column_deleteColumnConfirmation.tr(), + confirm: () { + context + .read() + .add(BoardEvent.deleteGroup(group.groupId)); + }, + ).show(context); + break; } } FlowySvgData get icon => switch (this) { rename => FlowySvgs.edit_s, hide => FlowySvgs.hide_s, + delete => FlowySvgs.delete_s, }; String get text => switch (this) { rename => LocaleKeys.board_column_renameColumn.tr(), hide => LocaleKeys.board_column_hideColumn.tr(), + delete => LocaleKeys.board_column_deleteColumn.tr(), }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart index ec2cc4e05b..7d1f451341 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/board/presentation/widgets/board_hidden_groups.dart @@ -71,7 +71,6 @@ class HiddenGroupsColumn extends StatelessWidget { LocaleKeys .board_hiddenGroupSection_sectionTitle .tr(), - fontSize: 14, overflow: TextOverflow.ellipsis, color: Theme.of(context).hintColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart index a1a573341d..fbe9d761ab 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/grid/presentation/widgets/header/field_type_extension.dart @@ -65,4 +65,13 @@ extension FieldTypeListExtension on FieldType { FieldType.SingleSelect => true, _ => false, }; + + bool get canDeleteGroup => switch (this) { + FieldType.URL || + FieldType.SingleSelect || + FieldType.MultiSelect || + FieldType.DateTime => + true, + _ => false, + }; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart index 8e01c4165e..53a3347f60 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/text_card_cell.dart @@ -139,9 +139,13 @@ class _TextCellState extends State { return Padding( padding: CardSizes.cardCellPadding, child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showNotes) ...[ - const FlowySvg(FlowySvgs.notes_s), + FlowySvg( + FlowySvgs.notes_s, + color: Theme.of(context).hintColor, + ), const HSpace(4), ], Expanded(child: child), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index fc4778fe9f..844ee5d7e1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -133,6 +133,7 @@ class _CreateFlowyAlertDialog extends State { child: FlowyText.medium( widget.title, fontSize: FontSizes.s16, + textAlign: TextAlign.center, color: Theme.of(context).colorScheme.tertiary, maxLines: null, ), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index d6ba54d815..7d9d9bceac 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -810,7 +810,9 @@ "createNewColumn": "Add a new group", "addToColumnTopTooltip": "Add a new card at the top", "renameColumn": "Rename", - "hideColumn": "Hide" + "hideColumn": "Hide", + "deleteColumn": "Delete", + "deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?" }, "hiddenGroupSection": { "sectionTitle": "Hidden Groups", @@ -1118,4 +1120,4 @@ "font": "Font", "actions": "Actions" } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/event-integration/src/database_event.rs b/frontend/rust-lib/event-integration/src/database_event.rs index 7cd14aa178..8d819a6cbc 100644 --- a/frontend/rust-lib/event-integration/src/database_event.rs +++ b/frontend/rust-lib/event-integration/src/database_event.rs @@ -426,6 +426,18 @@ impl EventIntegrationTest { .error() } + pub async fn delete_group(&self, view_id: &str, group_id: &str) -> Option { + EventBuilder::new(self.clone()) + .event(DatabaseEvent::DeleteGroup) + .payload(DeleteGroupPayloadPB { + view_id: view_id.to_string(), + group_id: group_id.to_string(), + }) + .async_send() + .await + .error() + } + pub async fn update_setting(&self, changeset: DatabaseSettingChangesetPB) -> Option { EventBuilder::new(self.clone()) .event(DatabaseEvent::UpdateDatabaseSetting) 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 753c4d5ede..3d1280fbbe 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 @@ -1,5 +1,139 @@ use event_integration::EventIntegrationTest; +// The number of groups should be 0 if there is no group by field in grid +#[tokio::test] +async fn get_groups_event_with_grid_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await; + let grid_view = test + .create_grid(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + let groups = test.get_groups(&grid_view.id).await; + assert_eq!(groups.len(), 0); +} + +#[tokio::test] +async fn get_groups_event_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 4); +} + +#[tokio::test] +async fn move_group_event_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 4); + let group_1 = groups[0].group_id.clone(); + let group_2 = groups[1].group_id.clone(); + let group_3 = groups[2].group_id.clone(); + let group_4 = groups[3].group_id.clone(); + + let error = test.move_group(&board_view.id, &group_2, &group_3).await; + assert!(error.is_none()); + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups[0].group_id, group_1); + assert_eq!(groups[1].group_id, group_3); + assert_eq!(groups[2].group_id, group_2); + assert_eq!(groups[3].group_id, group_4); + + let error = test.move_group(&board_view.id, &group_1, &group_4).await; + assert!(error.is_none()); + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups[0].group_id, group_3); + assert_eq!(groups[1].group_id, group_2); + assert_eq!(groups[2].group_id, group_4); + assert_eq!(groups[3].group_id, group_1); +} + +#[tokio::test] +async fn move_group_event_with_invalid_id_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + // Empty to group id + let groups = test.get_groups(&board_view.id).await; + let error = test + .move_group(&board_view.id, &groups[0].group_id, "") + .await; + assert!(error.is_some()); + + // empty from group id + let error = test + .move_group(&board_view.id, "", &groups[1].group_id) + .await; + assert!(error.is_some()); +} + +#[tokio::test] +async fn rename_group_event_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + // Empty to group id + let groups = test.get_groups(&board_view.id).await; + let error = test + .update_group( + &board_view.id, + &groups[1].group_id, + &groups[1].field_id, + Some("new name".to_owned()), + None, + ) + .await; + assert!(error.is_none()); + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups[1].group_name, "new name".to_owned()); +} + +#[tokio::test] +async fn hide_group_event_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + // Empty to group id + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 4); + + let error = test + .update_group( + &board_view.id, + &groups[0].group_id, + &groups[0].field_id, + None, + Some(false), + ) + .await; + assert!(error.is_none()); + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 4); + assert_eq!(groups[0].is_visible, false); +} + #[tokio::test] async fn update_group_name_test() { let test = EventIntegrationTest::new_with_guest_user().await; @@ -29,3 +163,25 @@ async fn update_group_name_test() { assert_eq!(groups[1].group_name, "To Do?"); assert_eq!(groups[2].group_name, "Doing"); } + +#[tokio::test] +async fn delete_group_test() { + let test = EventIntegrationTest::new_with_guest_user().await; + let current_workspace = test.get_current_workspace().await; + let board_view = test + .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) + .await; + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 4); + assert_eq!(groups[1].group_name, "To Do"); + assert_eq!(groups[2].group_name, "Doing"); + assert_eq!(groups[3].group_name, "Done"); + + test.delete_group(&board_view.id, &groups[1].group_id).await; + + let groups = test.get_groups(&board_view.id).await; + assert_eq!(groups.len(), 3); + assert_eq!(groups[1].group_name, "Doing"); + assert_eq!(groups[2].group_name, "Done"); +} diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index 266d41f0cd..ffc49dfb06 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -656,140 +656,6 @@ async fn update_checklist_cell_test() { assert_eq!(cell.percentage, 0.67); } -// The number of groups should be 0 if there is no group by field in grid -#[tokio::test] -async fn get_groups_event_with_grid_test() { - let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await; - let grid_view = test - .create_grid(¤t_workspace.id, "my board view".to_owned(), vec![]) - .await; - - let groups = test.get_groups(&grid_view.id).await; - assert_eq!(groups.len(), 0); -} - -#[tokio::test] -async fn get_groups_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await; - let board_view = test - .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) - .await; - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups.len(), 4); -} - -#[tokio::test] -async fn move_group_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await; - let board_view = test - .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) - .await; - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups.len(), 4); - let group_1 = groups[0].group_id.clone(); - let group_2 = groups[1].group_id.clone(); - let group_3 = groups[2].group_id.clone(); - let group_4 = groups[3].group_id.clone(); - - let error = test.move_group(&board_view.id, &group_2, &group_3).await; - assert!(error.is_none()); - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups[0].group_id, group_1); - assert_eq!(groups[1].group_id, group_3); - assert_eq!(groups[2].group_id, group_2); - assert_eq!(groups[3].group_id, group_4); - - let error = test.move_group(&board_view.id, &group_1, &group_4).await; - assert!(error.is_none()); - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups[0].group_id, group_3); - assert_eq!(groups[1].group_id, group_2); - assert_eq!(groups[2].group_id, group_4); - assert_eq!(groups[3].group_id, group_1); -} - -#[tokio::test] -async fn move_group_event_with_invalid_id_test() { - let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await; - let board_view = test - .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) - .await; - - // Empty to group id - let groups = test.get_groups(&board_view.id).await; - let error = test - .move_group(&board_view.id, &groups[0].group_id, "") - .await; - assert!(error.is_some()); - - // empty from group id - let error = test - .move_group(&board_view.id, "", &groups[1].group_id) - .await; - assert!(error.is_some()); -} - -#[tokio::test] -async fn rename_group_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await; - let board_view = test - .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) - .await; - - // Empty to group id - let groups = test.get_groups(&board_view.id).await; - let error = test - .update_group( - &board_view.id, - &groups[1].group_id, - &groups[1].field_id, - Some("new name".to_owned()), - None, - ) - .await; - assert!(error.is_none()); - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups[1].group_name, "new name".to_owned()); -} - -#[tokio::test] -async fn hide_group_event_test() { - let test = EventIntegrationTest::new_with_guest_user().await; - let current_workspace = test.get_current_workspace().await; - let board_view = test - .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) - .await; - - // Empty to group id - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups.len(), 4); - - let error = test - .update_group( - &board_view.id, - &groups[0].group_id, - &groups[0].field_id, - None, - Some(false), - ) - .await; - assert!(error.is_none()); - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups.len(), 4); - assert!(!groups[0].is_visible); -} - // Update the database layout type from grid to board #[tokio::test] async fn update_database_layout_event_test() { 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 4553cee745..d1fe052697 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 @@ -4,7 +4,7 @@ use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; use crate::entities::parser::NotEmptyStr; -use crate::entities::{FieldType, RowMetaPB}; +use crate::entities::RowMetaPB; use crate::services::group::{GroupChangeset, GroupData, GroupSetting}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -130,13 +130,6 @@ pub struct GroupByFieldParams { pub view_id: String, } -pub struct DeleteGroupParams { - pub view_id: String, - pub field_id: String, - pub group_id: String, - pub field_type: FieldType, -} - #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct UpdateGroupPB { #[pb(index = 1)] @@ -230,3 +223,32 @@ impl TryFrom for CreateGroupParams { }) } } + +#[derive(Debug, Default, ProtoBuf)] +pub struct DeleteGroupPayloadPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub group_id: String, +} + +pub struct DeleteGroupParams { + pub view_id: String, + pub group_id: String, +} + +impl TryFrom for DeleteGroupParams { + type Error = ErrorCode; + + fn try_from(value: DeleteGroupPayloadPB) -> Result { + let view_id = NotEmptyStr::parse(value.view_id) + .map_err(|_| ErrorCode::ViewIdIsInvalid)? + .0; + let group_id = NotEmptyStr::parse(value.group_id) + .map_err(|_| ErrorCode::GroupIdIsEmpty)? + .0; + + Ok(Self { view_id, group_id }) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs index c32dc4b124..9809516bed 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/view_entities.rs @@ -57,6 +57,10 @@ impl RowsChangePB { ..Default::default() } } + + pub fn is_empty(&self) -> bool { + self.deleted_rows.is_empty() && self.inserted_rows.is_empty() && self.updated_rows.is_empty() + } } #[derive(Debug, Default, ProtoBuf)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index ccebbb8fa5..e6e437f922 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -755,6 +755,18 @@ pub(crate) async fn create_group_handler( Ok(()) } +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn delete_group_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> FlowyResult<()> { + let manager = upgrade_manager(manager)?; + let params: DeleteGroupParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + database_editor.delete_group(params).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 436a9ec814..3c31e6e40a 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -61,6 +61,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::GetGroup, get_group_handler) .event(DatabaseEvent::UpdateGroup, update_group_handler) .event(DatabaseEvent::CreateGroup, create_group_handler) + .event(DatabaseEvent::DeleteGroup, delete_group_handler) // Database .event(DatabaseEvent::GetDatabases, get_databases_handler) // Calendar @@ -288,6 +289,9 @@ pub enum DatabaseEvent { #[event(input = "CreateGroupPayloadPB")] CreateGroup = 114, + #[event(input = "DeleteGroupPayloadPB")] + DeleteGroup = 115, + /// 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 7a3eb5c488..fd1e8a3a0c 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 @@ -174,12 +174,16 @@ impl DatabaseEditor { } pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> { - self - .database - .lock() - .delete_group_setting(¶ms.view_id, ¶ms.group_id); let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; - view_editor.v_delete_group(params).await?; + let changes = view_editor.v_delete_group(¶ms.group_id).await?; + + if !changes.is_empty() { + for view in self.database_views.editors().await { + send_notification(&view.view_id, DatabaseNotification::DidUpdateViewRows) + .payload(changes.clone()) + .send(); + } + } Ok(()) } @@ -819,7 +823,7 @@ impl DatabaseEditor { }; for option in options { - type_option.delete_option(option.into()); + type_option.delete_option(&option.id); } self .database @@ -1317,6 +1321,10 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { }) } + fn remove_row(&self, row_id: &RowId) -> Option { + self.database.lock().remove_row(row_id) + } + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>> { let cells = self.database.lock().get_cells_for_field(view_id, field_id); to_fut(async move { cells.into_iter().map(Arc::new).collect() }) 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 f3052006bf..f79c328a69 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, - InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB, + DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, + LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, }; use crate::notification::{send_notification, DatabaseNotification}; @@ -391,8 +391,43 @@ impl DatabaseViewEditor { Ok(()) } - pub async fn v_delete_group(&self, _params: DeleteGroupParams) -> FlowyResult<()> { - Ok(()) + pub async fn v_delete_group(&self, group_id: &str) -> FlowyResult { + let mut group_controller = self.group_controller.write().await; + let controller = match group_controller.as_mut() { + Some(controller) => controller, + None => return Ok(RowsChangePB::default()), + }; + + let old_field = self.delegate.get_field(controller.field_id()); + let (row_ids, type_option_data) = controller.delete_group(group_id)?; + + drop(group_controller); + + let mut changes = RowsChangePB::default(); + + if let Some(field) = old_field { + let deleted_rows = row_ids + .iter() + .filter_map(|row_id| self.delegate.remove_row(row_id)) + .map(|row| row.id.into_inner()); + + changes.deleted_rows.extend(deleted_rows); + + if let Some(type_option) = type_option_data { + self + .delegate + .update_field(&self.view_id, type_option, field) + .await?; + } + let notification = GroupChangesPB { + view_id: self.view_id.clone(), + deleted_groups: vec![group_id.to_string()], + ..Default::default() + }; + notify_did_update_num_of_groups(&self.view_id, notification).await; + } + + Ok(changes) } pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> { diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs index 99df59b512..a57f9f7459 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use collab_database::database::MutexDatabase; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{RowCell, RowDetail, RowId}; +use collab_database::rows::{Row, RowCell, RowDetail, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting}; use tokio::sync::RwLock; @@ -57,6 +57,8 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { /// Returns all the rows in the view fn get_rows(&self, view_id: &str) -> Fut>>; + fn remove_row(&self, row_id: &RowId) -> Option; + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut>; 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 26af4036d9..dcc3c87a2d 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 @@ -49,12 +49,9 @@ pub trait SelectTypeOptionSharedAction: Send + Sync { } } - fn delete_option(&mut self, delete_option: SelectOption) { + fn delete_option(&mut self, option_id: &str) { let options = self.mut_options(); - if let Some(index) = options - .iter() - .position(|option| option.id == delete_option.id) - { + if let Some(index) = options.iter().position(|option| option.id == option_id) { options.remove(index); } } 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 9af51f167b..11bd169591 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Row, RowDetail}; +use collab_database::rows::{Cell, Row, RowDetail, RowId}; use flowy_error::FlowyResult; @@ -78,6 +78,8 @@ pub trait GroupCustomize: Send + Sync { ) -> FlowyResult<(Option, Option)> { Ok((None, None)) } + + fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult>; } /// Defines the shared actions any group controller can perform. @@ -159,6 +161,14 @@ pub trait GroupControllerOperation: Send + Sync { /// * `field`: new changeset fn did_update_group_field(&mut self, field: &Field) -> FlowyResult>; + /// Delete a group from the group configuration. + /// + /// Return a list of deleted row ids and/or a new `TypeOptionData` if + /// successful. + /// + /// * `group_id`: the id of the group to be deleted + fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec, Option)>; + /// Updates the name and/or visibility of groups. /// /// Returns a non-empty `TypeOptionData` when the changes require a change 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 fd049759ac..11bcce8364 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cells, Row, RowDetail}; +use collab_database::rows::{Cells, Row, RowDetail, RowId}; use futures::executor::block_on; use serde::de::DeserializeOwned; use serde::Serialize; @@ -396,6 +396,27 @@ where Ok(None) } + fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec, Option)> { + let group = if group_id != self.field_id() { + self.get_group(group_id) + } else { + None + }; + + match group { + Some((_index, group_data)) => { + let row_ids = group_data + .rows + .iter() + .map(|row| row.row.id.clone()) + .collect(); + let type_option_data = self.delete_group_custom(group_id)?; + Ok((row_ids, type_option_data)) + }, + None => Ok((vec![], None)), + } + } + async fn apply_group_changeset( &mut self, changeset: &GroupChangesets, 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 accd8d3f31..d5cb30fd00 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 @@ -1,6 +1,7 @@ use async_trait::async_trait; -use collab_database::fields::Field; +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, GroupPB, GroupRowsNotificationPB, InsertedRowPB, RowMetaPB}; @@ -138,6 +139,10 @@ impl GroupCustomize for CheckboxGroupController { }); group_changeset } + + fn delete_group_custom(&mut self, _group_id: &str) -> FlowyResult> { + Ok(None) + } } impl GroupController for CheckboxGroupController { 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 1a3f2ee371..812fbba5c3 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 @@ -7,7 +7,7 @@ use chrono::{ }; use chrono_tz::Tz; use collab_database::database::timestamp; -use collab_database::fields::Field; +use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; @@ -248,6 +248,11 @@ impl GroupCustomize for DateGroupController { } deleted_group } + + fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { + self.context.delete_group(group_id)?; + Ok(None) + } } impl GroupController for DateGroupController { 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 b3ba30127e..021615b359 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 @@ -2,7 +2,7 @@ use std::sync::Arc; use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cells, Row, RowDetail}; +use collab_database::rows::{Cells, Row, RowDetail, RowId}; use flowy_error::FlowyResult; @@ -129,6 +129,10 @@ impl GroupControllerOperation for DefaultGroupController { Ok(None) } + fn delete_group(&mut self, _group_id: &str) -> FlowyResult<(Vec, Option)> { + Ok((vec![], None)) + } + async fn apply_group_changeset( &mut self, _changeset: &GroupChangesets, 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 f7794a9624..dfc7ce8ce9 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 @@ -107,6 +107,22 @@ impl GroupCustomize for MultiSelectGroupController { Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } + + fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { + if let Some(option_index) = self + .type_option + .options + .iter() + .position(|option| option.id == group_id) + { + // Remove the option if the group is found + let mut new_type_option = self.type_option.clone(); + new_type_option.options.remove(option_index); + Ok(Some(new_type_option.into())) + } else { + Ok(None) + } + } } impl GroupController for MultiSelectGroupController { 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 a92c79c624..0129715e3a 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 @@ -111,6 +111,23 @@ impl GroupCustomize for SingleSelectGroupController { Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } + + fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { + if let Some(option_index) = self + .type_option + .options + .iter() + .position(|option| option.id == group_id) + { + // Remove the option if the group is found + let mut new_type_option = self.type_option.clone(); + new_type_option.options.remove(option_index); + Ok(Some(new_type_option.into())) + } else { + // Return None if no matching group is found + Ok(None) + } + } } impl GroupController for SingleSelectGroupController { 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 0b5b3539c7..c141811a59 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 @@ -1,7 +1,7 @@ use std::sync::Arc; use async_trait::async_trait; -use collab_database::fields::Field; +use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; use serde::{Deserialize, Serialize}; @@ -186,6 +186,11 @@ impl GroupCustomize for URLGroupController { } deleted_group } + + fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { + self.context.delete_group(group_id)?; + Ok(None) + } } impl GroupController for URLGroupController {