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:
Richard Shiue 2023-11-28 10:43:22 +08:00 committed by GitHub
parent 3595de5e12
commit 9d61ca0278
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 476 additions and 187 deletions

View File

@ -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 {

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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(),
};
}

View File

@ -71,7 +71,6 @@ class HiddenGroupsColumn extends StatelessWidget {
LocaleKeys
.board_hiddenGroupSection_sectionTitle
.tr(),
fontSize: 14,
overflow: TextOverflow.ellipsis,
color: Theme.of(context).hintColor,
),

View File

@ -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,
};
}

View File

@ -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),

View File

@ -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,
),

View File

@ -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"
}
}
}

View File

@ -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)

View File

@ -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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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");
}

View File

@ -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(&current_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(&current_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(&current_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(&current_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(&current_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(&current_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() {

View File

@ -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 })
}
}

View File

@ -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)]

View File

@ -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(&params.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>>,

View File

@ -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,

View File

@ -174,12 +174,16 @@ impl DatabaseEditor {
}
pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
self
.database
.lock()
.delete_group_setting(&params.view_id, &params.group_id);
let view_editor = self.database_views.get_view_editor(&params.view_id).await?;
view_editor.v_delete_group(params).await?;
let changes = view_editor.v_delete_group(&params.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() })

View File

@ -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<()> {

View File

@ -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>>;

View File

@ -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);
}
}

View File

@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {