Fix/select option filter (#1530)

* fix: multi select filter bugs

* fix: single select bugs

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Nathan.fooo 2022-12-05 09:54:47 +08:00 committed by GitHub
parent 806a924fff
commit 0d879a6091
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 221 additions and 91 deletions

View File

@ -1,5 +1,4 @@
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart'; import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async'; import 'dart:async';

View File

@ -3,7 +3,6 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
part 'field_service.freezed.dart'; part 'field_service.freezed.dart';

View File

@ -61,7 +61,7 @@ class _SelectOptionFilterChoicechipState
builder: (blocContext, state) { builder: (blocContext, state) {
return AppFlowyPopover( return AppFlowyPopover(
controller: PopoverController(), controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(200, 160)), constraints: BoxConstraints.loose(const Size(240, 160)),
direction: PopoverDirection.bottomWithCenterAligned, direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (BuildContext context) { popupBuilder: (BuildContext context) {
return SelectOptionFilterEditor(bloc: bloc); return SelectOptionFilterEditor(bloc: bloc);

View File

@ -3,7 +3,6 @@ import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart'
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart'; import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -35,7 +34,7 @@ class _FilterButtonState extends State<FilterButton> {
height: 26, height: 26,
child: FlowyTextButton( child: FlowyTextButton(
LocaleKeys.grid_settings_filter.tr(), LocaleKeys.grid_settings_filter.tr(),
fontSize: FontSizes.s14, fontSize: 13,
fontColor: textColor, fontColor: textColor,
fillColor: Colors.transparent, fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,

View File

@ -4,7 +4,6 @@ import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart';
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart'; import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -50,7 +49,7 @@ class _SettingButtonState extends State<SettingButton> {
triggerActions: PopoverTriggerFlags.none, triggerActions: PopoverTriggerFlags.none,
child: FlowyTextButton( child: FlowyTextButton(
LocaleKeys.settings_title.tr(), LocaleKeys.settings_title.tr(),
fontSize: FontSizes.s14, fontSize: 13,
fillColor: Colors.transparent, fillColor: Colors.transparent,
hoverColor: AFThemeExtension.of(context).lightGreyHover, hoverColor: AFThemeExtension.of(context).lightGreyHover,
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),

View File

@ -14,7 +14,7 @@ impl ChecklistFilterPB {
.map(|option| option.id.as_str()) .map(|option| option.id.as_str())
.collect::<Vec<&str>>(); .collect::<Vec<&str>>();
return match self.condition { match self.condition {
ChecklistFilterCondition::IsComplete => { ChecklistFilterCondition::IsComplete => {
if selected_option_ids.is_empty() { if selected_option_ids.is_empty() {
return false; return false;
@ -31,6 +31,6 @@ impl ChecklistFilterPB {
all_option_ids.retain(|option_id| !selected_option_ids.contains(option_id)); all_option_ids.retain(|option_id| !selected_option_ids.contains(option_id));
!all_option_ids.is_empty() !all_option_ids.is_empty()
} }
}; }
} }
} }

View File

@ -1,39 +1,77 @@
#![allow(clippy::needless_collect)] #![allow(clippy::needless_collect)]
use crate::entities::{ChecklistFilterPB, SelectOptionCondition, SelectOptionFilterPB}; use crate::entities::{ChecklistFilterPB, FieldType, SelectOptionCondition, SelectOptionFilterPB};
use crate::services::cell::{CellFilterOperation, TypeCellData}; use crate::services::cell::{CellFilterOperation, TypeCellData};
use crate::services::field::{ChecklistTypeOptionPB, MultiSelectTypeOptionPB, SingleSelectTypeOptionPB}; use crate::services::field::{ChecklistTypeOptionPB, MultiSelectTypeOptionPB, SingleSelectTypeOptionPB};
use crate::services::field::{SelectTypeOptionSharedAction, SelectedSelectOptions}; use crate::services::field::{SelectTypeOptionSharedAction, SelectedSelectOptions};
use flowy_error::FlowyResult; use flowy_error::FlowyResult;
impl SelectOptionFilterPB { impl SelectOptionFilterPB {
pub fn is_visible(&self, selected_options: &SelectedSelectOptions) -> bool { pub fn is_visible(&self, selected_options: &SelectedSelectOptions, field_type: FieldType) -> bool {
let selected_option_ids: Vec<&String> = selected_options.options.iter().map(|option| &option.id).collect(); let selected_option_ids: Vec<&String> = selected_options.options.iter().map(|option| &option.id).collect();
match self.condition { match self.condition {
SelectOptionCondition::OptionIs => { SelectOptionCondition::OptionIs => match field_type {
if self.option_ids.len() != selected_option_ids.len() { FieldType::SingleSelect => {
return false; if self.option_ids.is_empty() {
} return true;
// if selected options equal to filter's options, then the required_options will be empty. }
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
required_options.len() == selected_option_ids.len()
}
SelectOptionCondition::OptionIsNot => {
if self.option_ids.len() != selected_option_ids.len() {
return true;
}
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
required_options.len() != selected_option_ids.len() if selected_options.options.is_empty() {
} return false;
}
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
!required_options.is_empty()
}
FieldType::MultiSelect => {
if self.option_ids.is_empty() {
return true;
}
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
!required_options.is_empty()
}
_ => false,
},
SelectOptionCondition::OptionIsNot => match field_type {
FieldType::SingleSelect => {
if self.option_ids.is_empty() {
return true;
}
if selected_options.options.is_empty() {
return false;
}
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
required_options.is_empty()
}
FieldType::MultiSelect => {
let required_options = self
.option_ids
.iter()
.filter(|id| selected_option_ids.contains(id))
.collect::<Vec<_>>();
required_options.is_empty()
}
_ => false,
},
SelectOptionCondition::OptionIsEmpty => selected_option_ids.is_empty(), SelectOptionCondition::OptionIsEmpty => selected_option_ids.is_empty(),
SelectOptionCondition::OptionIsNotEmpty => !selected_option_ids.is_empty(), SelectOptionCondition::OptionIsNotEmpty => !selected_option_ids.is_empty(),
} }
@ -47,7 +85,7 @@ impl CellFilterOperation<SelectOptionFilterPB> for MultiSelectTypeOptionPB {
} }
let selected_options = SelectedSelectOptions::from(self.get_selected_options(any_cell_data.into())); let selected_options = SelectedSelectOptions::from(self.get_selected_options(any_cell_data.into()));
Ok(filter.is_visible(&selected_options)) Ok(filter.is_visible(&selected_options, FieldType::MultiSelect))
} }
} }
@ -57,7 +95,7 @@ impl CellFilterOperation<SelectOptionFilterPB> for SingleSelectTypeOptionPB {
return Ok(true); return Ok(true);
} }
let selected_options = SelectedSelectOptions::from(self.get_selected_options(any_cell_data.into())); let selected_options = SelectedSelectOptions::from(self.get_selected_options(any_cell_data.into()));
Ok(filter.is_visible(&selected_options)) Ok(filter.is_visible(&selected_options, FieldType::SingleSelect))
} }
} }
@ -74,7 +112,7 @@ impl CellFilterOperation<ChecklistFilterPB> for ChecklistTypeOptionPB {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::all)] #![allow(clippy::all)]
use crate::entities::{SelectOptionCondition, SelectOptionFilterPB}; use crate::entities::{FieldType, SelectOptionCondition, SelectOptionFilterPB};
use crate::services::field::selection_type_option::{SelectOptionPB, SelectedSelectOptions}; use crate::services::field::selection_type_option::{SelectOptionPB, SelectedSelectOptions};
#[test] #[test]
@ -85,10 +123,26 @@ mod tests {
option_ids: vec![], option_ids: vec![],
}; };
assert_eq!(filter.is_visible(&SelectedSelectOptions { options: vec![] }), true); assert_eq!(
filter.is_visible(&SelectedSelectOptions { options: vec![] }, FieldType::SingleSelect),
true
);
assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option.clone()]
},
FieldType::SingleSelect
),
false,
);
assert_eq!( assert_eq!(
filter.is_visible(&SelectedSelectOptions { options: vec![option] }), filter.is_visible(&SelectedSelectOptions { options: vec![] }, FieldType::MultiSelect),
true
);
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options: vec![option] }, FieldType::MultiSelect),
false, false,
); );
} }
@ -103,52 +157,127 @@ mod tests {
}; };
assert_eq!( assert_eq!(
filter.is_visible(&SelectedSelectOptions { filter.is_visible(
options: vec![option_1] &SelectedSelectOptions {
}), options: vec![option_1.clone()]
},
FieldType::SingleSelect
),
true true
); );
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options: vec![] }, FieldType::SingleSelect),
false,
);
assert_eq!(filter.is_visible(&SelectedSelectOptions { options: vec![] }), false,); assert_eq!(
filter.is_visible(
&SelectedSelectOptions {
options: vec![option_1.clone()]
},
FieldType::MultiSelect
),
true
);
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options: vec![] }, FieldType::MultiSelect),
false,
);
} }
#[test] #[test]
fn select_option_filter_is_not_test() { fn single_select_option_filter_is_not_test() {
let option_1 = SelectOptionPB::new("A"); let option_1 = SelectOptionPB::new("A");
let option_2 = SelectOptionPB::new("B"); let option_2 = SelectOptionPB::new("B");
let option_3 = SelectOptionPB::new("C"); let option_3 = SelectOptionPB::new("C");
let filter = SelectOptionFilterPB { let filter = SelectOptionFilterPB {
condition: SelectOptionCondition::OptionIsNot, condition: SelectOptionCondition::OptionIsNot,
option_ids: vec![option_1.id.clone(), option_2.id.clone()], option_ids: vec![option_1.id.clone(), option_2.id.clone()],
}; };
assert_eq!( for (options, is_visible) in vec![
filter.is_visible(&SelectedSelectOptions { (vec![option_2.clone()], false),
options: vec![option_1.clone(), option_2.clone()], (vec![option_1.clone()], false),
}), (vec![option_3.clone()], true),
false (vec![option_1.clone(), option_2.clone()], false),
); ] {
assert_eq!(
assert_eq!( filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
filter.is_visible(&SelectedSelectOptions { is_visible
options: vec![option_1.clone(), option_2.clone(), option_3.clone()], );
}), }
true
);
assert_eq!(
filter.is_visible(&SelectedSelectOptions {
options: vec![option_1.clone(), option_3.clone()],
}),
true
);
assert_eq!(filter.is_visible(&SelectedSelectOptions { options: vec![] }), true);
} }
#[test] #[test]
fn select_option_filter_is_test() { fn single_select_option_filter_is_test() {
let option_1 = SelectOptionPB::new("A");
let option_2 = SelectOptionPB::new("B");
let option_3 = SelectOptionPB::new("c");
let filter = SelectOptionFilterPB {
condition: SelectOptionCondition::OptionIs,
option_ids: vec![option_1.id.clone()],
};
for (options, is_visible) in vec![
(vec![option_1.clone()], true),
(vec![option_2.clone()], false),
(vec![option_3.clone()], false),
(vec![option_1.clone(), option_2.clone()], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
is_visible
);
}
}
#[test]
fn single_select_option_filter_is_test2() {
let option_1 = SelectOptionPB::new("A");
let option_2 = SelectOptionPB::new("B");
let filter = SelectOptionFilterPB {
condition: SelectOptionCondition::OptionIs,
option_ids: vec![],
};
for (options, is_visible) in vec![
(vec![option_1.clone()], true),
(vec![option_2.clone()], true),
(vec![option_1.clone(), option_2.clone()], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::SingleSelect),
is_visible
);
}
}
#[test]
fn multi_select_option_filter_not_contains_test() {
let option_1 = SelectOptionPB::new("A");
let option_2 = SelectOptionPB::new("B");
let option_3 = SelectOptionPB::new("C");
let filter = SelectOptionFilterPB {
condition: SelectOptionCondition::OptionIsNot,
option_ids: vec![option_1.id.clone(), option_2.id.clone()],
};
for (options, is_visible) in vec![
(vec![option_1.clone(), option_2.clone()], false),
(vec![option_1.clone()], false),
(vec![option_2.clone()], false),
(vec![option_3.clone()], true),
(vec![option_1.clone(), option_2.clone(), option_3.clone()], false),
(vec![], true),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
is_visible
);
}
}
#[test]
fn multi_select_option_filter_contains_test() {
let option_1 = SelectOptionPB::new("A"); let option_1 = SelectOptionPB::new("A");
let option_2 = SelectOptionPB::new("B"); let option_2 = SelectOptionPB::new("B");
let option_3 = SelectOptionPB::new("C"); let option_3 = SelectOptionPB::new("C");
@ -157,28 +286,33 @@ mod tests {
condition: SelectOptionCondition::OptionIs, condition: SelectOptionCondition::OptionIs,
option_ids: vec![option_1.id.clone(), option_2.id.clone()], option_ids: vec![option_1.id.clone(), option_2.id.clone()],
}; };
for (options, is_visible) in vec![
(vec![option_1.clone(), option_2.clone(), option_3.clone()], true),
(vec![option_2.clone(), option_1.clone()], true),
(vec![option_2.clone()], true),
(vec![option_1.clone(), option_3.clone()], true),
(vec![option_3.clone()], false),
] {
assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
is_visible
);
}
}
assert_eq!( #[test]
filter.is_visible(&SelectedSelectOptions { fn multi_select_option_filter_contains_test2() {
options: vec![option_1.clone(), option_2.clone()], let option_1 = SelectOptionPB::new("A");
}),
true
);
assert_eq!( let filter = SelectOptionFilterPB {
filter.is_visible(&SelectedSelectOptions { condition: SelectOptionCondition::OptionIs,
options: vec![option_1.clone(), option_2.clone(), option_3.clone()], option_ids: vec![],
}), };
false for (options, is_visible) in vec![(vec![option_1.clone()], true), (vec![], true)] {
); assert_eq!(
filter.is_visible(&SelectedSelectOptions { options }, FieldType::MultiSelect),
assert_eq!( is_visible
filter.is_visible(&SelectedSelectOptions { );
options: vec![option_1.clone(), option_3.clone()], }
}),
false
);
assert_eq!(filter.is_visible(&SelectedSelectOptions { options: vec![] }), false);
} }
} }

View File

@ -38,7 +38,7 @@ async fn grid_filter_multi_select_is_test() {
condition: SelectOptionCondition::OptionIs, condition: SelectOptionCondition::OptionIs,
option_ids: vec![options.remove(0).id, options.remove(0).id], option_ids: vec![options.remove(0).id, options.remove(0).id],
}, },
AssertNumberOfVisibleRows { expected: 2 }, AssertNumberOfVisibleRows { expected: 3 },
]; ];
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }
@ -53,7 +53,7 @@ async fn grid_filter_multi_select_is_test2() {
condition: SelectOptionCondition::OptionIs, condition: SelectOptionCondition::OptionIs,
option_ids: vec![options.remove(1).id], option_ids: vec![options.remove(1).id],
}, },
AssertNumberOfVisibleRows { expected: 1 }, AssertNumberOfVisibleRows { expected: 3 },
]; ];
test.run_scripts(scripts).await; test.run_scripts(scripts).await;
} }