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:
parent
c47f755155
commit
bd32ce5543
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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/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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user