Feat: support single select option filter (#1494)

* feat: support select option filter

* chore: select option filter ui

* chore: support edit single select filter

* chore: add flutter tests

Co-authored-by: nathan <nathan@appflowy.io>
This commit is contained in:
Nathan.fooo 2022-11-28 19:41:57 +08:00 committed by GitHub
parent c47f755155
commit bd32ce5543
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 815 additions and 44 deletions

View File

@ -191,6 +191,18 @@
"is": "is"
}
},
"singleSelectOptionFilter": {
"is": "Is",
"isNot": "Is not",
"isEmpty": "Is empty",
"isNotEmpty": "Is not empty"
},
"multiSelectOptionFilter": {
"contains": "Contains",
"doesNotContain": "Does not contain",
"isEmpty": "Is empty",
"isNotEmpty": "Is not empty"
},
"field": {
"hide": "Hide",
"insertLeft": "Insert Left",

View File

@ -531,7 +531,7 @@ class FieldInfo {
case FieldType.Checkbox:
// case FieldType.MultiSelect:
case FieldType.RichText:
// case FieldType.SingleSelect:
case FieldType.SingleSelect:
return true;
default:
return false;

View File

@ -99,9 +99,10 @@ class GridCreateFilterBloc
timestamp: timestamp,
);
case FieldType.MultiSelect:
return _ffiService.insertSingleSelectFilter(
return _ffiService.insertSelectOptionFilter(
fieldId: fieldId,
condition: SelectOptionCondition.OptionIs,
fieldType: FieldType.MultiSelect,
);
case FieldType.Number:
return _ffiService.insertNumberFilter(
@ -116,9 +117,10 @@ class GridCreateFilterBloc
content: '',
);
case FieldType.SingleSelect:
return _ffiService.insertSingleSelectFilter(
return _ffiService.insertSelectOptionFilter(
fieldId: fieldId,
condition: SelectOptionCondition.OptionIs,
fieldType: FieldType.SingleSelect,
);
case FieldType.URL:
return _ffiService.insertURLFilter(

View File

@ -126,10 +126,11 @@ class FilterFFIService {
);
}
Future<Either<Unit, FlowyError>> insertSingleSelectFilter({
Future<Either<Unit, FlowyError>> insertSelectOptionFilter({
required String fieldId,
String? filterId,
required FieldType fieldType,
required SelectOptionCondition condition,
String? filterId,
List<String> optionIds = const [],
}) {
final filter = SelectOptionFilterPB()
@ -139,25 +140,7 @@ class FilterFFIService {
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.SingleSelect,
data: filter.writeToBuffer(),
);
}
Future<Either<Unit, FlowyError>> insertMultiSelectFilter({
required String fieldId,
String? filterId,
required SelectOptionCondition condition,
List<String> optionIds = const [],
}) {
final filter = SelectOptionFilterPB()
..condition = condition
..optionIds.addAll(optionIds);
return insertFilter(
fieldId: fieldId,
filterId: filterId,
fieldType: FieldType.MultiSelect,
fieldType: fieldType,
data: filter.writeToBuffer(),
);
}
@ -168,8 +151,6 @@ class FilterFFIService {
required FieldType fieldType,
required List<int> data,
}) {
TextFilterCondition.DoesNotContain.value;
var insertFilterPayload = AlterFilterPayloadPB.create()
..fieldId = fieldId
..fieldType = fieldType

View File

@ -0,0 +1,148 @@
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbserver.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'dart:async';
import 'filter_listener.dart';
import 'filter_service.dart';
part 'select_option_filter_bloc.freezed.dart';
class SelectOptionFilterEditorBloc
extends Bloc<SelectOptionFilterEditorEvent, SelectOptionFilterEditorState> {
final FilterInfo filterInfo;
final FilterFFIService _ffiService;
final FilterListener _listener;
final SingleSelectTypeOptionContext typeOptionContext;
SelectOptionFilterEditorBloc({required this.filterInfo})
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
_listener = FilterListener(
viewId: filterInfo.viewId,
filterId: filterInfo.filter.id,
),
typeOptionContext = makeSingleSelectTypeOptionContext(
gridId: filterInfo.viewId,
fieldPB: filterInfo.field.field,
),
super(SelectOptionFilterEditorState.initial(filterInfo)) {
on<SelectOptionFilterEditorEvent>(
(event, emit) async {
event.when(
initial: () async {
_startListening();
_loadOptions();
},
updateCondition: (SelectOptionCondition condition) {
_ffiService.insertSelectOptionFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.field.id,
condition: condition,
optionIds: state.filter.optionIds,
fieldType: state.filterInfo.field.fieldType,
);
},
updateContent: (List<String> optionIds) {
_ffiService.insertSelectOptionFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.field.id,
condition: state.filter.condition,
optionIds: optionIds,
fieldType: state.filterInfo.field.fieldType,
);
},
delete: () {
_ffiService.deleteFilter(
fieldId: filterInfo.field.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.field.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
final filterInfo = state.filterInfo.copyWith(filter: filter);
final selectOptionFilter = filterInfo.selectOptionFilter()!;
emit(state.copyWith(
filterInfo: filterInfo,
filter: selectOptionFilter,
));
},
updateFilterDescription: (String desc) {
emit(state.copyWith(filterDesc: desc));
},
);
},
);
}
void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) add(const SelectOptionFilterEditorEvent.delete());
},
onUpdated: (filter) {
if (!isClosed) {
add(SelectOptionFilterEditorEvent.didReceiveFilter(filter));
}
},
);
}
void _loadOptions() {
typeOptionContext.loadTypeOptionData(
onCompleted: (value) {
if (!isClosed) {
String filterDesc = '';
for (final option in value.options) {
if (state.filter.optionIds.contains(option.id)) {
filterDesc += "${option.name} ";
}
}
add(SelectOptionFilterEditorEvent.updateFilterDescription(
filterDesc));
}
},
onError: (error) => Log.error(error),
);
}
@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
}
@freezed
class SelectOptionFilterEditorEvent with _$SelectOptionFilterEditorEvent {
const factory SelectOptionFilterEditorEvent.initial() = _Initial;
const factory SelectOptionFilterEditorEvent.didReceiveFilter(
FilterPB filter) = _DidReceiveFilter;
const factory SelectOptionFilterEditorEvent.updateCondition(
SelectOptionCondition condition) = _UpdateCondition;
const factory SelectOptionFilterEditorEvent.updateContent(
List<String> optionIds) = _UpdateContent;
const factory SelectOptionFilterEditorEvent.updateFilterDescription(
String desc) = _UpdateDesc;
const factory SelectOptionFilterEditorEvent.delete() = _Delete;
}
@freezed
class SelectOptionFilterEditorState with _$SelectOptionFilterEditorState {
const factory SelectOptionFilterEditorState({
required FilterInfo filterInfo,
required SelectOptionFilterPB filter,
required String filterDesc,
}) = _GridFilterState;
factory SelectOptionFilterEditorState.initial(FilterInfo filterInfo) {
return SelectOptionFilterEditorState(
filterInfo: filterInfo,
filter: filterInfo.selectOptionFilter()!,
filterDesc: '',
);
}
}

View File

@ -0,0 +1,156 @@
import 'dart:async';
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart';
import 'package:flowy_sdk/log.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'select_option_filter_list_bloc.freezed.dart';
class SelectOptionFilterListBloc<T>
extends Bloc<SelectOptionFilterListEvent, SelectOptionFilterListState> {
final SingleSelectTypeOptionContext typeOptionContext;
SelectOptionFilterListBloc({
required String viewId,
required FieldPB fieldPB,
required List<String> selectedOptionIds,
}) : typeOptionContext = makeSingleSelectTypeOptionContext(
gridId: viewId,
fieldPB: fieldPB,
),
super(SelectOptionFilterListState.initial(selectedOptionIds)) {
on<SelectOptionFilterListEvent>(
(event, emit) async {
await event.when(
initial: () async {
_startListening();
_loadOptions();
},
selectOption: (option) {
final selectedOptionIds = Set<String>.from(state.selectedOptionIds);
selectedOptionIds.add(option.id);
_updateSelectOptions(
selectedOptionIds: selectedOptionIds,
emit: emit,
);
},
unselectOption: (option) {
final selectedOptionIds = Set<String>.from(state.selectedOptionIds);
selectedOptionIds.remove(option.id);
_updateSelectOptions(
selectedOptionIds: selectedOptionIds,
emit: emit,
);
},
didReceiveOptions: (newOptions) {
List<SelectOptionPB> options = List.from(newOptions);
options.retainWhere(
(element) => element.name.contains(state.predicate));
final visibleOptions = options.map((option) {
return VisibleSelectOption(
option, state.selectedOptionIds.contains(option.id));
}).toList();
emit(state.copyWith(
options: options, visibleOptions: visibleOptions));
},
filterOption: (optionName) {
_updateSelectOptions(predicate: optionName, emit: emit);
},
);
},
);
}
@override
Future<void> close() async {
return super.close();
}
void _updateSelectOptions({
String? predicate,
Set<String>? selectedOptionIds,
required Emitter<SelectOptionFilterListState> emit,
}) {
final List<VisibleSelectOption> visibleOptions = _makeVisibleOptions(
predicate ?? state.predicate,
selectedOptionIds ?? state.selectedOptionIds,
);
emit(state.copyWith(
predicate: predicate ?? state.predicate,
visibleOptions: visibleOptions,
selectedOptionIds: selectedOptionIds ?? state.selectedOptionIds,
));
}
List<VisibleSelectOption> _makeVisibleOptions(
String predicate,
Set<String> selectedOptionIds,
) {
List<SelectOptionPB> options = List.from(state.options);
options.retainWhere((element) => element.name.contains(predicate));
return options.map((option) {
return VisibleSelectOption(option, selectedOptionIds.contains(option.id));
}).toList();
}
void _loadOptions() {
typeOptionContext.loadTypeOptionData(
onCompleted: (value) {
if (!isClosed) {
add(SelectOptionFilterListEvent.didReceiveOptions(value.options));
}
},
onError: (error) => Log.error(error),
);
}
void _startListening() {}
}
@freezed
class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent {
const factory SelectOptionFilterListEvent.initial() = _Initial;
const factory SelectOptionFilterListEvent.selectOption(
SelectOptionPB option) = _SelectOption;
const factory SelectOptionFilterListEvent.unselectOption(
SelectOptionPB option) = _UnSelectOption;
const factory SelectOptionFilterListEvent.didReceiveOptions(
List<SelectOptionPB> options) = _DidReceiveOptions;
const factory SelectOptionFilterListEvent.filterOption(String optionName) =
_SelectOptionFilter;
}
@freezed
class SelectOptionFilterListState with _$SelectOptionFilterListState {
const factory SelectOptionFilterListState({
required List<SelectOptionPB> options,
required List<VisibleSelectOption> visibleOptions,
required Set<String> selectedOptionIds,
required String predicate,
}) = _SelectOptionFilterListState;
factory SelectOptionFilterListState.initial(List<String> selectedOptionIds) {
return SelectOptionFilterListState(
options: [],
predicate: '',
visibleOptions: [],
selectedOptionIds: selectedOptionIds.toSet(),
);
}
}
class VisibleSelectOption {
final SelectOptionPB optionPB;
final bool isSelected;
VisibleSelectOption(this.optionPB, this.isSelected);
}

View File

@ -1,15 +0,0 @@
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:flutter/material.dart';
import 'choicechip.dart';
class SelectOptionFilterChoicechip extends StatelessWidget {
final FilterInfo filterInfo;
const SelectOptionFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo);
}
}

View File

@ -0,0 +1,117 @@
import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/condition_button.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pb.dart';
import 'package:flutter/material.dart';
class SelectOptionFilterConditionList extends StatelessWidget {
final FilterInfo filterInfo;
final PopoverMutex popoverMutex;
final Function(SelectOptionCondition) onCondition;
const SelectOptionFilterConditionList({
required this.filterInfo,
required this.popoverMutex,
required this.onCondition,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final selectOptionFilter = filterInfo.selectOptionFilter()!;
return PopoverActionList<ConditionWrapper>(
asBarrier: true,
mutex: popoverMutex,
direction: PopoverDirection.bottomWithCenterAligned,
actions: SelectOptionCondition.values
.map(
(action) => ConditionWrapper(
action,
selectOptionFilter.condition == action,
filterInfo.field.fieldType,
),
)
.toList(),
buildChild: (controller) {
return ConditionButton(
conditionName: filterName(selectOptionFilter),
onTap: () => controller.show(),
);
},
onSelected: (action, controller) async {
onCondition(action.inner);
controller.close();
},
);
}
String filterName(SelectOptionFilterPB filter) {
if (filterInfo.field.fieldType == FieldType.SingleSelect) {
return filter.condition.singleSelectFilterName;
} else {
return filter.condition.multiSelectFilterName;
}
}
}
class ConditionWrapper extends ActionCell {
final SelectOptionCondition inner;
final bool isSelected;
final FieldType fieldType;
ConditionWrapper(this.inner, this.isSelected, this.fieldType);
@override
Widget? rightIcon(Color iconColor) {
if (isSelected) {
return svgWidget("grid/checkmark");
} else {
return null;
}
}
@override
String get name {
if (fieldType == FieldType.SingleSelect) {
return inner.singleSelectFilterName;
} else {
return inner.multiSelectFilterName;
}
}
}
extension SelectOptionConditionExtension on SelectOptionCondition {
String get singleSelectFilterName {
switch (this) {
case SelectOptionCondition.OptionIs:
return LocaleKeys.grid_singleSelectOptionFilter_is.tr();
case SelectOptionCondition.OptionIsEmpty:
return LocaleKeys.grid_singleSelectOptionFilter_isEmpty.tr();
case SelectOptionCondition.OptionIsNot:
return LocaleKeys.grid_singleSelectOptionFilter_isNot.tr();
case SelectOptionCondition.OptionIsNotEmpty:
return LocaleKeys.grid_singleSelectOptionFilter_isNotEmpty.tr();
default:
return "";
}
}
String get multiSelectFilterName {
switch (this) {
case SelectOptionCondition.OptionIs:
return LocaleKeys.grid_multiSelectOptionFilter_contains.tr();
case SelectOptionCondition.OptionIsEmpty:
return LocaleKeys.grid_multiSelectOptionFilter_isEmpty.tr();
case SelectOptionCondition.OptionIsNot:
return LocaleKeys.grid_multiSelectOptionFilter_doesNotContain.tr();
case SelectOptionCondition.OptionIsNotEmpty:
return LocaleKeys.grid_multiSelectOptionFilter_isNotEmpty.tr();
default:
return "";
}
}
}

View File

@ -0,0 +1,108 @@
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:app_flowy/plugins/grid/application/filter/select_option_filter_list_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart';
import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SelectOptionFilterList extends StatelessWidget {
final String viewId;
final FieldInfo fieldInfo;
final List<String> selectedOptionIds;
final Function(List<String>) onSelectedOptions;
const SelectOptionFilterList({
required this.viewId,
required this.fieldInfo,
required this.selectedOptionIds,
required this.onSelectedOptions,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => SelectOptionFilterListBloc(
viewId: viewId,
fieldPB: fieldInfo.field,
selectedOptionIds: selectedOptionIds,
)..add(const SelectOptionFilterListEvent.initial()),
child:
BlocListener<SelectOptionFilterListBloc, SelectOptionFilterListState>(
listenWhen: (previous, current) =>
previous.selectedOptionIds != current.selectedOptionIds,
listener: (context, state) {
onSelectedOptions(state.selectedOptionIds.toList());
},
child: BlocBuilder<SelectOptionFilterListBloc,
SelectOptionFilterListState>(
builder: (context, state) {
return ListView.separated(
shrinkWrap: true,
controller: ScrollController(),
itemCount: state.visibleOptions.length,
separatorBuilder: (context, index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
physics: StyledScrollPhysics(),
itemBuilder: (BuildContext context, int index) {
final option = state.visibleOptions[index];
return _SelectOptionFilterCell(
option: option.optionPB,
isSelected: option.isSelected,
);
},
);
},
),
),
);
}
}
class _SelectOptionFilterCell extends StatefulWidget {
final SelectOptionPB option;
final bool isSelected;
const _SelectOptionFilterCell({
required this.option,
required this.isSelected,
Key? key,
}) : super(key: key);
@override
State<_SelectOptionFilterCell> createState() =>
_SelectOptionFilterCellState();
}
class _SelectOptionFilterCellState extends State<_SelectOptionFilterCell> {
@override
Widget build(BuildContext context) {
return SizedBox(
height: GridSize.typeOptionItemHeight,
child: SelectOptionTagCell(
option: widget.option,
onSelected: (option) {
if (widget.isSelected) {
context
.read<SelectOptionFilterListBloc>()
.add(SelectOptionFilterListEvent.unselectOption(option));
} else {
context
.read<SelectOptionFilterListBloc>()
.add(SelectOptionFilterListEvent.selectOption(option));
}
},
children: [
if (widget.isSelected)
Padding(
padding: const EdgeInsets.only(right: 6),
child: svgWidget("grid/checkmark"),
),
],
),
);
}
}

View File

@ -0,0 +1,159 @@
import 'package:app_flowy/plugins/grid/application/filter/select_option_filter_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/disclosure_button.dart';
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../choicechip.dart';
import 'condition_list.dart';
import 'option_list.dart';
class SelectOptionFilterChoicechip extends StatefulWidget {
final FilterInfo filterInfo;
const SelectOptionFilterChoicechip({required this.filterInfo, Key? key})
: super(key: key);
@override
State<SelectOptionFilterChoicechip> createState() =>
_SelectOptionFilterChoicechipState();
}
class _SelectOptionFilterChoicechipState
extends State<SelectOptionFilterChoicechip> {
late SelectOptionFilterEditorBloc bloc;
@override
void initState() {
bloc = SelectOptionFilterEditorBloc(filterInfo: widget.filterInfo)
..add(const SelectOptionFilterEditorEvent.initial());
super.initState();
}
@override
void dispose() {
bloc.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: bloc,
child: BlocBuilder<SelectOptionFilterEditorBloc,
SelectOptionFilterEditorState>(
builder: (blocContext, state) {
return AppFlowyPopover(
controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(200, 160)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (BuildContext context) {
return SelectOptionFilterEditor(bloc: bloc);
},
child: ChoiceChipButton(
filterInfo: widget.filterInfo,
filterDesc: state.filterDesc,
),
);
},
),
);
}
}
class SelectOptionFilterEditor extends StatefulWidget {
final SelectOptionFilterEditorBloc bloc;
const SelectOptionFilterEditor({required this.bloc, Key? key})
: super(key: key);
@override
State<SelectOptionFilterEditor> createState() =>
_SelectOptionFilterEditorState();
}
class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
final popoverMutex = PopoverMutex();
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: widget.bloc,
child: BlocBuilder<SelectOptionFilterEditorBloc,
SelectOptionFilterEditorState>(
builder: (context, state) {
List<Widget> slivers = [
SliverToBoxAdapter(child: _buildFilterPannel(context, state)),
];
if (state.filter.condition != SelectOptionCondition.OptionIsEmpty &&
state.filter.condition !=
SelectOptionCondition.OptionIsNotEmpty) {
slivers.add(const SliverToBoxAdapter(child: VSpace(4)));
slivers.add(
SliverToBoxAdapter(
child: SelectOptionFilterList(
viewId: state.filterInfo.viewId,
fieldInfo: state.filterInfo.field,
selectedOptionIds: state.filter.optionIds,
onSelectedOptions: (optionIds) {
context.read<SelectOptionFilterEditorBloc>().add(
SelectOptionFilterEditorEvent.updateContent(optionIds));
},
),
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: CustomScrollView(
shrinkWrap: true,
slivers: slivers,
controller: ScrollController(),
physics: StyledScrollPhysics(),
),
);
},
),
);
}
Widget _buildFilterPannel(
BuildContext context, SelectOptionFilterEditorState state) {
return SizedBox(
height: 20,
child: Row(
children: [
FlowyText(state.filterInfo.field.name),
const HSpace(4),
SelectOptionFilterConditionList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
context.read<SelectOptionFilterEditorBloc>().add(
SelectOptionFilterEditorEvent.updateCondition(condition));
},
),
const Spacer(),
DisclosureButton(
popoverMutex: popoverMutex,
onAction: (action) {
switch (action) {
case FilterDisclosureAction.delete:
context
.read<SelectOptionFilterEditorBloc>()
.add(const SelectOptionFilterEditorEvent.delete());
break;
}
},
),
],
),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbserver.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
@ -40,4 +41,13 @@ class FilterInfo {
}
return CheckboxFilterPB.fromBuffer(filter.data);
}
SelectOptionFilterPB? selectOptionFilter() {
if (filter.fieldType == FieldType.SingleSelect ||
filter.fieldType == FieldType.MultiSelect) {
return SelectOptionFilterPB.fromBuffer(filter.data);
} else {
return null;
}
}
}

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'choicechip/checkbox.dart';
import 'choicechip/date.dart';
import 'choicechip/number.dart';
import 'choicechip/select_option.dart';
import 'choicechip/select_option/select_option.dart';
import 'choicechip/text.dart';
import 'choicechip/url.dart';
import 'filter_info.dart';

View File

@ -145,6 +145,40 @@ TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
);
}
TypeOptionContext<SingleSelectTypeOptionPB> makeSingleSelectTypeOptionContext({
required String gridId,
required FieldPB fieldPB,
}) {
return makeSelectTypeOptionContext(gridId: gridId, fieldPB: fieldPB);
}
TypeOptionContext<MultiSelectTypeOptionPB> makeMultiSelectTypeOptionContext({
required String gridId,
required FieldPB fieldPB,
}) {
return makeSelectTypeOptionContext(gridId: gridId, fieldPB: fieldPB);
}
TypeOptionContext<T> makeSelectTypeOptionContext<T extends GeneratedMessage>({
required String gridId,
required FieldPB fieldPB,
}) {
final loader = FieldTypeOptionLoader(
gridId: gridId,
field: fieldPB,
);
final dataController = TypeOptionDataController(
gridId: gridId,
loader: loader,
);
final typeOptionContext = makeTypeOptionContextWithDataController<T>(
gridId: gridId,
fieldType: fieldPB.fieldType,
dataController: dataController,
);
return typeOptionContext;
}
TypeOptionContext<T>
makeTypeOptionContextWithDataController<T extends GeneratedMessage>({
required String gridId,

View File

@ -17,7 +17,7 @@ void main() {
viewId: context.gridView.id, fieldController: context.fieldController)
..add(const GridFilterMenuEvent.initial());
await gridResponseFuture();
assert(menuBloc.state.creatableFields.length == 2);
assert(menuBloc.state.creatableFields.length == 3);
final service = FilterFFIService(viewId: context.gridView.id);
final textField = context.textFieldContext();
@ -26,7 +26,7 @@ void main() {
condition: TextFilterCondition.TextIsEmpty,
content: "");
await gridResponseFuture();
assert(menuBloc.state.creatableFields.length == 1);
assert(menuBloc.state.creatableFields.length == 2);
});
test('test filter menu after update existing text filter)', () async {

View File

@ -0,0 +1,51 @@
import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
import 'package:flutter_test/flutter_test.dart';
import '../util.dart';
import 'filter_util.dart';
void main() {
late AppFlowyGridTest gridTest;
setUpAll(() async {
gridTest = await AppFlowyGridTest.ensureInitialized();
});
test('filter rows by checkbox is check condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterFFIService(viewId: context.gridView.id);
final controller = await context.makeCheckboxCellController(0);
controller.saveCellData("Yes");
await gridResponseFuture();
// create a new filter
final checkboxField = context.checkboxFieldContext();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterCondition.IsChecked,
);
await gridResponseFuture();
assert(context.rowInfos.length == 1,
"expect 1 but receive ${context.rowInfos.length}");
});
test('filter rows by checkbox is uncheck condition)', () async {
final context = await createTestFilterGrid(gridTest);
final service = FilterFFIService(viewId: context.gridView.id);
final controller = await context.makeCheckboxCellController(0);
controller.saveCellData("Yes");
await gridResponseFuture();
// create a new filter
final checkboxField = context.checkboxFieldContext();
await service.insertCheckboxFilter(
fieldId: checkboxField.id,
condition: CheckboxFilterCondition.IsUnChecked,
);
await gridResponseFuture();
assert(context.rowInfos.length == 2,
"expect 2 but receive ${context.rowInfos.length}");
});
}

View File

@ -145,6 +145,14 @@ class GridTestContext {
await makeCellController(field.id, rowIndex) as GridCellController;
return cellController;
}
Future<GridCellController> makeCheckboxCellController(int rowIndex) async {
final field = fieldContexts
.firstWhere((element) => element.fieldType == FieldType.Checkbox);
final cellController =
await makeCellController(field.id, rowIndex) as GridCellController;
return cellController;
}
}
/// Create a empty Grid for test