Skip to content

Commit c47f755

Browse files
authored
feat: support checkbox filter (#1492)
* feat: support checkbox filter * fix: unit test Co-authored-by: nathan <[email protected]>
1 parent d6cbbf3 commit c47f755

File tree

11 files changed

+417
-30
lines changed

11 files changed

+417
-30
lines changed

frontend/app_flowy/assets/translations/en.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,13 @@
184184
"isNotEmpty": "is not empty"
185185
}
186186
},
187+
"checkboxFilter": {
188+
"isChecked": "Checked",
189+
"isUnchecked": "Unchecked",
190+
"choicechipPrefix": {
191+
"is": "is"
192+
}
193+
},
187194
"field": {
188195
"hide": "Hide",
189196
"insertLeft": "Insert Left",

frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -527,9 +527,15 @@ class FieldInfo {
527527
bool get canCreateFilter {
528528
if (hasFilter) return false;
529529

530-
if (_field.fieldType != FieldType.RichText) return false;
531-
532-
return true;
530+
switch (_field.fieldType) {
531+
case FieldType.Checkbox:
532+
// case FieldType.MultiSelect:
533+
case FieldType.RichText:
534+
// case FieldType.SingleSelect:
535+
return true;
536+
default:
537+
return false;
538+
}
533539
}
534540

535541
FieldInfo({required FieldPB field}) : _field = field;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
2+
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pb.dart';
3+
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
4+
import 'package:flutter_bloc/flutter_bloc.dart';
5+
import 'package:freezed_annotation/freezed_annotation.dart';
6+
import 'dart:async';
7+
import 'filter_listener.dart';
8+
import 'filter_service.dart';
9+
10+
part 'checkbox_filter_editor_bloc.freezed.dart';
11+
12+
class CheckboxFilterEditorBloc
13+
extends Bloc<CheckboxFilterEditorEvent, CheckboxFilterEditorState> {
14+
final FilterInfo filterInfo;
15+
final FilterFFIService _ffiService;
16+
final FilterListener _listener;
17+
18+
CheckboxFilterEditorBloc({required this.filterInfo})
19+
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
20+
_listener = FilterListener(
21+
viewId: filterInfo.viewId,
22+
filterId: filterInfo.filter.id,
23+
),
24+
super(CheckboxFilterEditorState.initial(filterInfo)) {
25+
on<CheckboxFilterEditorEvent>(
26+
(event, emit) async {
27+
event.when(
28+
initial: () async {
29+
_startListening();
30+
},
31+
updateCondition: (CheckboxFilterCondition condition) {
32+
_ffiService.insertCheckboxFilter(
33+
filterId: filterInfo.filter.id,
34+
fieldId: filterInfo.field.id,
35+
condition: condition,
36+
);
37+
},
38+
delete: () {
39+
_ffiService.deleteFilter(
40+
fieldId: filterInfo.field.id,
41+
filterId: filterInfo.filter.id,
42+
fieldType: filterInfo.field.fieldType,
43+
);
44+
},
45+
didReceiveFilter: (FilterPB filter) {
46+
final filterInfo = state.filterInfo.copyWith(filter: filter);
47+
final checkboxFilter = filterInfo.checkboxFilter()!;
48+
emit(state.copyWith(
49+
filterInfo: filterInfo,
50+
filter: checkboxFilter,
51+
));
52+
},
53+
);
54+
},
55+
);
56+
}
57+
58+
void _startListening() {
59+
_listener.start(
60+
onDeleted: () {
61+
if (!isClosed) add(const CheckboxFilterEditorEvent.delete());
62+
},
63+
onUpdated: (filter) {
64+
if (!isClosed) add(CheckboxFilterEditorEvent.didReceiveFilter(filter));
65+
},
66+
);
67+
}
68+
69+
@override
70+
Future<void> close() async {
71+
await _listener.stop();
72+
return super.close();
73+
}
74+
}
75+
76+
@freezed
77+
class CheckboxFilterEditorEvent with _$CheckboxFilterEditorEvent {
78+
const factory CheckboxFilterEditorEvent.initial() = _Initial;
79+
const factory CheckboxFilterEditorEvent.didReceiveFilter(FilterPB filter) =
80+
_DidReceiveFilter;
81+
const factory CheckboxFilterEditorEvent.updateCondition(
82+
CheckboxFilterCondition condition) = _UpdateCondition;
83+
const factory CheckboxFilterEditorEvent.delete() = _Delete;
84+
}
85+
86+
@freezed
87+
class CheckboxFilterEditorState with _$CheckboxFilterEditorState {
88+
const factory CheckboxFilterEditorState({
89+
required FilterInfo filterInfo,
90+
required CheckboxFilterPB filter,
91+
}) = _GridFilterState;
92+
93+
factory CheckboxFilterEditorState.initial(FilterInfo filterInfo) {
94+
return CheckboxFilterEditorState(
95+
filterInfo: filterInfo,
96+
filter: filterInfo.checkboxFilter()!,
97+
);
98+
}
99+
}
Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,210 @@
1+
import 'package:app_flowy/generated/locale_keys.g.dart';
2+
import 'package:app_flowy/plugins/grid/application/filter/checkbox_filter_editor_bloc.dart';
3+
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/condition_button.dart';
4+
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/disclosure_button.dart';
15
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
6+
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
7+
import 'package:appflowy_popover/appflowy_popover.dart';
8+
import 'package:easy_localization/easy_localization.dart';
9+
import 'package:flowy_infra/image.dart';
10+
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
11+
import 'package:flowy_infra_ui/style_widget/text.dart';
12+
import 'package:flowy_infra_ui/widget/spacing.dart';
13+
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
214
import 'package:flutter/material.dart';
15+
import 'package:flutter_bloc/flutter_bloc.dart';
316

417
import 'choicechip.dart';
518

6-
class CheckboxFilterChoicechip extends StatelessWidget {
19+
class CheckboxFilterChoicechip extends StatefulWidget {
720
final FilterInfo filterInfo;
821
const CheckboxFilterChoicechip({required this.filterInfo, Key? key})
922
: super(key: key);
1023

24+
@override
25+
State<CheckboxFilterChoicechip> createState() =>
26+
_CheckboxFilterChoicechipState();
27+
}
28+
29+
class _CheckboxFilterChoicechipState extends State<CheckboxFilterChoicechip> {
30+
late CheckboxFilterEditorBloc bloc;
31+
32+
@override
33+
void initState() {
34+
bloc = CheckboxFilterEditorBloc(filterInfo: widget.filterInfo)
35+
..add(const CheckboxFilterEditorEvent.initial());
36+
super.initState();
37+
}
38+
39+
@override
40+
void dispose() {
41+
bloc.close();
42+
super.dispose();
43+
}
44+
45+
@override
46+
Widget build(BuildContext context) {
47+
return BlocProvider.value(
48+
value: bloc,
49+
child: BlocBuilder<CheckboxFilterEditorBloc, CheckboxFilterEditorState>(
50+
builder: (blocContext, state) {
51+
return AppFlowyPopover(
52+
controller: PopoverController(),
53+
constraints: BoxConstraints.loose(const Size(200, 76)),
54+
direction: PopoverDirection.bottomWithCenterAligned,
55+
popupBuilder: (BuildContext context) {
56+
return CheckboxFilterEditor(bloc: bloc);
57+
},
58+
child: ChoiceChipButton(
59+
filterInfo: widget.filterInfo,
60+
filterDesc: _makeFilterDesc(state),
61+
),
62+
);
63+
},
64+
),
65+
);
66+
}
67+
68+
String _makeFilterDesc(CheckboxFilterEditorState state) {
69+
final prefix = LocaleKeys.grid_checkboxFilter_choicechipPrefix_is.tr();
70+
return "$prefix ${state.filter.condition.filterName}";
71+
}
72+
}
73+
74+
class CheckboxFilterEditor extends StatefulWidget {
75+
final CheckboxFilterEditorBloc bloc;
76+
const CheckboxFilterEditor({required this.bloc, Key? key}) : super(key: key);
77+
78+
@override
79+
State<CheckboxFilterEditor> createState() => _CheckboxFilterEditorState();
80+
}
81+
82+
class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
83+
final popoverMutex = PopoverMutex();
84+
1185
@override
1286
Widget build(BuildContext context) {
13-
return ChoiceChipButton(filterInfo: filterInfo);
87+
return BlocProvider.value(
88+
value: widget.bloc,
89+
child: BlocBuilder<CheckboxFilterEditorBloc, CheckboxFilterEditorState>(
90+
builder: (context, state) {
91+
final List<Widget> children = [
92+
_buildFilterPannel(context, state),
93+
];
94+
95+
return Padding(
96+
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
97+
child: IntrinsicHeight(child: Column(children: children)),
98+
);
99+
},
100+
),
101+
);
102+
}
103+
104+
Widget _buildFilterPannel(
105+
BuildContext context, CheckboxFilterEditorState state) {
106+
return SizedBox(
107+
height: 20,
108+
child: Row(
109+
children: [
110+
FlowyText(state.filterInfo.field.name),
111+
const HSpace(4),
112+
CheckboxFilterConditionList(
113+
filterInfo: state.filterInfo,
114+
popoverMutex: popoverMutex,
115+
onCondition: (condition) {
116+
context
117+
.read<CheckboxFilterEditorBloc>()
118+
.add(CheckboxFilterEditorEvent.updateCondition(condition));
119+
},
120+
),
121+
const Spacer(),
122+
DisclosureButton(
123+
popoverMutex: popoverMutex,
124+
onAction: (action) {
125+
switch (action) {
126+
case FilterDisclosureAction.delete:
127+
context
128+
.read<CheckboxFilterEditorBloc>()
129+
.add(const CheckboxFilterEditorEvent.delete());
130+
break;
131+
}
132+
},
133+
),
134+
],
135+
),
136+
);
137+
}
138+
}
139+
140+
class CheckboxFilterConditionList extends StatelessWidget {
141+
final FilterInfo filterInfo;
142+
final PopoverMutex popoverMutex;
143+
final Function(CheckboxFilterCondition) onCondition;
144+
const CheckboxFilterConditionList({
145+
required this.filterInfo,
146+
required this.popoverMutex,
147+
required this.onCondition,
148+
Key? key,
149+
}) : super(key: key);
150+
151+
@override
152+
Widget build(BuildContext context) {
153+
final checkboxFilter = filterInfo.checkboxFilter()!;
154+
return PopoverActionList<ConditionWrapper>(
155+
asBarrier: true,
156+
mutex: popoverMutex,
157+
direction: PopoverDirection.bottomWithCenterAligned,
158+
actions: CheckboxFilterCondition.values
159+
.map(
160+
(action) => ConditionWrapper(
161+
action,
162+
checkboxFilter.condition == action,
163+
),
164+
)
165+
.toList(),
166+
buildChild: (controller) {
167+
return ConditionButton(
168+
conditionName: checkboxFilter.condition.filterName,
169+
onTap: () => controller.show(),
170+
);
171+
},
172+
onSelected: (action, controller) async {
173+
onCondition(action.inner);
174+
controller.close();
175+
},
176+
);
177+
}
178+
}
179+
180+
class ConditionWrapper extends ActionCell {
181+
final CheckboxFilterCondition inner;
182+
final bool isSelected;
183+
184+
ConditionWrapper(this.inner, this.isSelected);
185+
186+
@override
187+
Widget? rightIcon(Color iconColor) {
188+
if (isSelected) {
189+
return svgWidget("grid/checkmark");
190+
} else {
191+
return null;
192+
}
193+
}
194+
195+
@override
196+
String get name => inner.filterName;
197+
}
198+
199+
extension TextFilterConditionExtension on CheckboxFilterCondition {
200+
String get filterName {
201+
switch (this) {
202+
case CheckboxFilterCondition.IsChecked:
203+
return LocaleKeys.grid_checkboxFilter_isChecked.tr();
204+
case CheckboxFilterCondition.IsUnChecked:
205+
return LocaleKeys.grid_checkboxFilter_isUnchecked.tr();
206+
default:
207+
return "";
208+
}
14209
}
15210
}

frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_info.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
2+
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pb.dart';
23
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pb.dart';
34
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
45
import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
@@ -32,4 +33,11 @@ class FilterInfo {
3233
}
3334
return TextFilterPB.fromBuffer(filter.data);
3435
}
36+
37+
CheckboxFilterPB? checkboxFilter() {
38+
if (filter.fieldType != FieldType.Checkbox) {
39+
return null;
40+
}
41+
return CheckboxFilterPB.fromBuffer(filter.data);
42+
}
3543
}

frontend/app_flowy/test/bloc_test/grid_test/filter/filter_menu_test.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ void main() {
1717
viewId: context.gridView.id, fieldController: context.fieldController)
1818
..add(const GridFilterMenuEvent.initial());
1919
await gridResponseFuture();
20-
assert(menuBloc.state.creatableFields.length == 1);
20+
assert(menuBloc.state.creatableFields.length == 2);
2121

2222
final service = FilterFFIService(viewId: context.gridView.id);
2323
final textField = context.textFieldContext();
@@ -26,7 +26,7 @@ void main() {
2626
condition: TextFilterCondition.TextIsEmpty,
2727
content: "");
2828
await gridResponseFuture();
29-
assert(menuBloc.state.creatableFields.isEmpty);
29+
assert(menuBloc.state.creatableFields.length == 1);
3030
});
3131

3232
test('test filter menu after update existing text filter)', () async {

0 commit comments

Comments
 (0)