mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -191,6 +191,18 @@
|
|||||||
"is": "is"
|
"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": {
|
"field": {
|
||||||
"hide": "Hide",
|
"hide": "Hide",
|
||||||
"insertLeft": "Insert Left",
|
"insertLeft": "Insert Left",
|
||||||
|
@ -531,7 +531,7 @@ class FieldInfo {
|
|||||||
case FieldType.Checkbox:
|
case FieldType.Checkbox:
|
||||||
// case FieldType.MultiSelect:
|
// case FieldType.MultiSelect:
|
||||||
case FieldType.RichText:
|
case FieldType.RichText:
|
||||||
// case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
|
@ -99,9 +99,10 @@ class GridCreateFilterBloc
|
|||||||
timestamp: timestamp,
|
timestamp: timestamp,
|
||||||
);
|
);
|
||||||
case FieldType.MultiSelect:
|
case FieldType.MultiSelect:
|
||||||
return _ffiService.insertSingleSelectFilter(
|
return _ffiService.insertSelectOptionFilter(
|
||||||
fieldId: fieldId,
|
fieldId: fieldId,
|
||||||
condition: SelectOptionCondition.OptionIs,
|
condition: SelectOptionCondition.OptionIs,
|
||||||
|
fieldType: FieldType.MultiSelect,
|
||||||
);
|
);
|
||||||
case FieldType.Number:
|
case FieldType.Number:
|
||||||
return _ffiService.insertNumberFilter(
|
return _ffiService.insertNumberFilter(
|
||||||
@ -116,9 +117,10 @@ class GridCreateFilterBloc
|
|||||||
content: '',
|
content: '',
|
||||||
);
|
);
|
||||||
case FieldType.SingleSelect:
|
case FieldType.SingleSelect:
|
||||||
return _ffiService.insertSingleSelectFilter(
|
return _ffiService.insertSelectOptionFilter(
|
||||||
fieldId: fieldId,
|
fieldId: fieldId,
|
||||||
condition: SelectOptionCondition.OptionIs,
|
condition: SelectOptionCondition.OptionIs,
|
||||||
|
fieldType: FieldType.SingleSelect,
|
||||||
);
|
);
|
||||||
case FieldType.URL:
|
case FieldType.URL:
|
||||||
return _ffiService.insertURLFilter(
|
return _ffiService.insertURLFilter(
|
||||||
|
@ -126,10 +126,11 @@ class FilterFFIService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> insertSingleSelectFilter({
|
Future<Either<Unit, FlowyError>> insertSelectOptionFilter({
|
||||||
required String fieldId,
|
required String fieldId,
|
||||||
String? filterId,
|
required FieldType fieldType,
|
||||||
required SelectOptionCondition condition,
|
required SelectOptionCondition condition,
|
||||||
|
String? filterId,
|
||||||
List<String> optionIds = const [],
|
List<String> optionIds = const [],
|
||||||
}) {
|
}) {
|
||||||
final filter = SelectOptionFilterPB()
|
final filter = SelectOptionFilterPB()
|
||||||
@ -139,25 +140,7 @@ class FilterFFIService {
|
|||||||
return insertFilter(
|
return insertFilter(
|
||||||
fieldId: fieldId,
|
fieldId: fieldId,
|
||||||
filterId: filterId,
|
filterId: filterId,
|
||||||
fieldType: FieldType.SingleSelect,
|
fieldType: fieldType,
|
||||||
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,
|
|
||||||
data: filter.writeToBuffer(),
|
data: filter.writeToBuffer(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -168,8 +151,6 @@ class FilterFFIService {
|
|||||||
required FieldType fieldType,
|
required FieldType fieldType,
|
||||||
required List<int> data,
|
required List<int> data,
|
||||||
}) {
|
}) {
|
||||||
TextFilterCondition.DoesNotContain.value;
|
|
||||||
|
|
||||||
var insertFilterPayload = AlterFilterPayloadPB.create()
|
var insertFilterPayload = AlterFilterPayloadPB.create()
|
||||||
..fieldId = fieldId
|
..fieldId = fieldId
|
||||||
..fieldType = fieldType
|
..fieldType = fieldType
|
||||||
|
@ -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: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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/checkbox_filter.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_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/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/text_filter.pb.dart';
|
||||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||||
|
|
||||||
@ -40,4 +41,13 @@ class FilterInfo {
|
|||||||
}
|
}
|
||||||
return CheckboxFilterPB.fromBuffer(filter.data);
|
return CheckboxFilterPB.fromBuffer(filter.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SelectOptionFilterPB? selectOptionFilter() {
|
||||||
|
if (filter.fieldType == FieldType.SingleSelect ||
|
||||||
|
filter.fieldType == FieldType.MultiSelect) {
|
||||||
|
return SelectOptionFilterPB.fromBuffer(filter.data);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'choicechip/checkbox.dart';
|
import 'choicechip/checkbox.dart';
|
||||||
import 'choicechip/date.dart';
|
import 'choicechip/date.dart';
|
||||||
import 'choicechip/number.dart';
|
import 'choicechip/number.dart';
|
||||||
import 'choicechip/select_option.dart';
|
import 'choicechip/select_option/select_option.dart';
|
||||||
import 'choicechip/text.dart';
|
import 'choicechip/text.dart';
|
||||||
import 'choicechip/url.dart';
|
import 'choicechip/url.dart';
|
||||||
import 'filter_info.dart';
|
import 'filter_info.dart';
|
||||||
|
@ -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>
|
TypeOptionContext<T>
|
||||||
makeTypeOptionContextWithDataController<T extends GeneratedMessage>({
|
makeTypeOptionContextWithDataController<T extends GeneratedMessage>({
|
||||||
required String gridId,
|
required String gridId,
|
||||||
|
@ -17,7 +17,7 @@ void main() {
|
|||||||
viewId: context.gridView.id, fieldController: context.fieldController)
|
viewId: context.gridView.id, fieldController: context.fieldController)
|
||||||
..add(const GridFilterMenuEvent.initial());
|
..add(const GridFilterMenuEvent.initial());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
assert(menuBloc.state.creatableFields.length == 2);
|
assert(menuBloc.state.creatableFields.length == 3);
|
||||||
|
|
||||||
final service = FilterFFIService(viewId: context.gridView.id);
|
final service = FilterFFIService(viewId: context.gridView.id);
|
||||||
final textField = context.textFieldContext();
|
final textField = context.textFieldContext();
|
||||||
@ -26,7 +26,7 @@ void main() {
|
|||||||
condition: TextFilterCondition.TextIsEmpty,
|
condition: TextFilterCondition.TextIsEmpty,
|
||||||
content: "");
|
content: "");
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
assert(menuBloc.state.creatableFields.length == 1);
|
assert(menuBloc.state.creatableFields.length == 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test filter menu after update existing text filter)', () async {
|
test('test filter menu after update existing text filter)', () async {
|
||||||
|
@ -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}");
|
||||||
|
});
|
||||||
|
}
|
@ -145,6 +145,14 @@ class GridTestContext {
|
|||||||
await makeCellController(field.id, rowIndex) as GridCellController;
|
await makeCellController(field.id, rowIndex) as GridCellController;
|
||||||
return cellController;
|
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
|
/// Create a empty Grid for test
|
||||||
|
Reference in New Issue
Block a user