mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: delete kanban board groups (#3925)
* feat: hide/unhide ui * chore: implement collapsible side bar and adjust group header (#2) * refactor: hidden columns into own file * chore: adjust new group button position * fix: flowy icon buton secondary color bleed * chore: some UI adjustments * fix: some regressions * chore: proper group is_visible fetching * chore: use a bloc to manage hidden groups * fix: hiding groups not working * chore: implement hidden group popups * chore: proper ungrouped item column management * chore: remove ungrouped items button * chore: flowy hover build * fix: clean up code * test: integration tests * fix: not null promise on null value * fix: hide and unhide multiple groups * chore: i18n and code review * chore: missed review * fix: rust-lib-test * fix: dont completely remove flowyiconhovercolor * chore: apply suggest * fix: number of rows inside hidden groups not updating properly * fix: hidden groups disappearing after collapse * fix: hidden group title alignment * fix: insert newly unhidden groups into the correct position * chore: adjust padding all around * feat: reorder hidden groups * chore: adjust padding * chore: collapse hidden groups section persist * chore: no status group at beginning * fix: hiding groups when grouping with other types * chore: disable rename groups that arent supported * chore: update appflowy board ref * chore: better naming * feat: delete kanban groups * chore: forgot to save * chore: fix build and small ui adjustments * chore: add a confirm dialog when deleting a column * fix: flutter lint * test: add integration test * chore: fix some design review issues * chore: apply suggestions from Nathan * fix: write lock on group controller --------- Co-authored-by: Mathias Mogensen <mathias@appflowy.io>
This commit is contained in:
parent
3595de5e12
commit
9d61ca0278
@ -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 {
|
||||
|
@ -48,4 +48,14 @@ class GroupBackendService {
|
||||
|
||||
return DatabaseEventCreateGroup(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> deleteGroup({
|
||||
required String groupId,
|
||||
}) {
|
||||
final payload = DeleteGroupPayloadPB.create()
|
||||
..viewId = viewId
|
||||
..groupId = groupId;
|
||||
|
||||
return DatabaseEventDeleteGroup(payload).send();
|
||||
}
|
||||
}
|
||||
|
@ -99,6 +99,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
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;
|
||||
|
@ -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<BoardColumnHeader> {
|
||||
Widget title = Expanded(
|
||||
child: FlowyText.medium(
|
||||
widget.groupData.headerData.groupName,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
@ -92,7 +92,6 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
.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<BoardColumnHeader> {
|
||||
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<BoardColumnHeader> {
|
||||
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<BoardColumnHeader> {
|
||||
),
|
||||
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<BoardColumnHeader> {
|
||||
|
||||
enum GroupOptions {
|
||||
rename,
|
||||
hide;
|
||||
hide,
|
||||
delete;
|
||||
|
||||
void call(BuildContext context, GroupPB group) {
|
||||
switch (this) {
|
||||
@ -258,16 +261,28 @@ enum GroupOptions {
|
||||
.read<BoardBloc>()
|
||||
.add(BoardEvent.toggleGroupVisibility(group, false));
|
||||
break;
|
||||
case delete:
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.board_column_deleteColumnConfirmation.tr(),
|
||||
confirm: () {
|
||||
context
|
||||
.read<BoardBloc>()
|
||||
.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(),
|
||||
};
|
||||
}
|
||||
|
@ -71,7 +71,6 @@ class HiddenGroupsColumn extends StatelessWidget {
|
||||
LocaleKeys
|
||||
.board_hiddenGroupSection_sectionTitle
|
||||
.tr(),
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -139,9 +139,13 @@ class _TextCellState extends State<TextCardCell> {
|
||||
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),
|
||||
|
@ -133,6 +133,7 @@ class _CreateFlowyAlertDialog extends State<NavigatorAlertDialog> {
|
||||
child: FlowyText.medium(
|
||||
widget.title,
|
||||
fontSize: FontSizes.s16,
|
||||
textAlign: TextAlign.center,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
maxLines: null,
|
||||
),
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -426,6 +426,18 @@ impl EventIntegrationTest {
|
||||
.error()
|
||||
}
|
||||
|
||||
pub async fn delete_group(&self, view_id: &str, group_id: &str) -> Option<FlowyError> {
|
||||
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<FlowyError> {
|
||||
EventBuilder::new(self.clone())
|
||||
.event(DatabaseEvent::UpdateDatabaseSetting)
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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<CreateGroupPayloadPB> 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<DeleteGroupPayloadPB> for DeleteGroupParams {
|
||||
type Error = ErrorCode;
|
||||
|
||||
fn try_from(value: DeleteGroupPayloadPB) -> Result<Self, Self::Error> {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
@ -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)]
|
||||
|
@ -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<DeleteGroupPayloadPB>,
|
||||
manager: AFPluginState<Weak<DatabaseManager>>,
|
||||
) -> 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<Weak<DatabaseManager>>,
|
||||
|
@ -61,6 +61,7 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> 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,
|
||||
|
@ -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<Row> {
|
||||
self.database.lock().remove_row(row_id)
|
||||
}
|
||||
|
||||
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>> {
|
||||
let cells = self.database.lock().get_cells_for_field(view_id, field_id);
|
||||
to_fut(async move { cells.into_iter().map(Arc::new).collect() })
|
||||
|
@ -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<RowsChangePB> {
|
||||
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<()> {
|
||||
|
@ -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<Vec<Arc<RowDetail>>>;
|
||||
|
||||
fn remove_row(&self, row_id: &RowId) -> Option<Row>;
|
||||
|
||||
fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut<Vec<Arc<RowCell>>>;
|
||||
|
||||
fn get_cell_in_row(&self, field_id: &str, row_id: &RowId) -> Fut<Arc<RowCell>>;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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<TypeOptionData>, Option<InsertedGroupPB>)> {
|
||||
Ok((None, None))
|
||||
}
|
||||
|
||||
fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult<Option<TypeOptionData>>;
|
||||
}
|
||||
|
||||
/// 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<Option<GroupChangesPB>>;
|
||||
|
||||
/// 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<RowId>, Option<TypeOptionData>)>;
|
||||
|
||||
/// Updates the name and/or visibility of groups.
|
||||
///
|
||||
/// Returns a non-empty `TypeOptionData` when the changes require a change
|
||||
|
@ -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<RowId>, Option<TypeOptionData>)> {
|
||||
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,
|
||||
|
@ -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<Option<TypeOptionData>> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupController for CheckboxGroupController {
|
||||
|
@ -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<Option<TypeOptionData>> {
|
||||
self.context.delete_group(group_id)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupController for DateGroupController {
|
||||
|
@ -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<RowId>, Option<TypeOptionData>)> {
|
||||
Ok((vec![], None))
|
||||
}
|
||||
|
||||
async fn apply_group_changeset(
|
||||
&mut self,
|
||||
_changeset: &GroupChangesets,
|
||||
|
@ -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<Option<TypeOptionData>> {
|
||||
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 {
|
||||
|
@ -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<Option<TypeOptionData>> {
|
||||
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 {
|
||||
|
@ -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<Option<TypeOptionData>> {
|
||||
self.context.delete_group(group_id)?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl GroupController for URLGroupController {
|
||||
|
Loading…
Reference in New Issue
Block a user