Skip to content

Commit

Permalink
chore: enable number filter (AppFlowy-IO#4653)
Browse files Browse the repository at this point in the history
* chore: enable filtering by number field type

* chore: code cleanup

* fix: integration test

* chore: remove unnecessary async from event handler
  • Loading branch information
richardshiue authored Feb 17, 2024
1 parent 1311e2d commit c159a5e
Show file tree
Hide file tree
Showing 8 changed files with 411 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import '../util/database_test_op.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();

group('database filter', () {
group('grid filter:', () {
testWidgets('add text filter', (tester) async {
await tester.openV020database();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,12 @@ class FieldInfo with _$FieldInfo {
}

bool get canCreateFilter {
if (hasFilter) return false;
if (hasFilter) {
return false;
}

switch (field.fieldType) {
case FieldType.Number:
case FieldType.Checkbox:
case FieldType.MultiSelect:
case FieldType.RichText:
Expand All @@ -62,7 +65,9 @@ class FieldInfo with _$FieldInfo {
}

bool get canCreateSort {
if (hasSort) return false;
if (hasSort) {
return false;
}

switch (field.fieldType) {
case FieldType.RichText:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import 'dart:async';

import 'package:appflowy/plugins/database/application/filter/filter_listener.dart';
import 'package:appflowy/plugins/database/application/filter/filter_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'number_filter_editor_bloc.freezed.dart';

class NumberFilterEditorBloc
extends Bloc<NumberFilterEditorEvent, NumberFilterEditorState> {
NumberFilterEditorBloc({required this.filterInfo})
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
_listener = FilterListener(
viewId: filterInfo.viewId,
filterId: filterInfo.filter.id,
),
super(NumberFilterEditorState.initial(filterInfo)) {
_dispatch();
_startListening();
}

final FilterInfo filterInfo;
final FilterBackendService _filterBackendSvc;
final FilterListener _listener;

void _dispatch() {
on<NumberFilterEditorEvent>(
(event, emit) async {
event.when(
didReceiveFilter: (filter) {
final filterInfo = state.filterInfo.copyWith(filter: filter);
emit(
state.copyWith(
filterInfo: filterInfo,
filter: filterInfo.numberFilter()!,
),
);
},
updateCondition: (NumberFilterConditionPB condition) {
_filterBackendSvc.insertNumberFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: condition,
content: state.filter.content,
);
},
updateContent: (content) {
_filterBackendSvc.insertNumberFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
condition: state.filter.condition,
content: content,
);
},
delete: () {
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
fieldType: filterInfo.fieldInfo.fieldType,
);
},
);
},
);
}

void _startListening() {
_listener.start(
onDeleted: () {
if (!isClosed) {
add(const NumberFilterEditorEvent.delete());
}
},
onUpdated: (filter) {
if (!isClosed) {
add(NumberFilterEditorEvent.didReceiveFilter(filter));
}
},
);
}

@override
Future<void> close() async {
await _listener.stop();
return super.close();
}
}

@freezed
class NumberFilterEditorEvent with _$NumberFilterEditorEvent {
const factory NumberFilterEditorEvent.didReceiveFilter(FilterPB filter) =
_DidReceiveFilter;
const factory NumberFilterEditorEvent.updateCondition(
NumberFilterConditionPB condition,
) = _UpdateCondition;
const factory NumberFilterEditorEvent.updateContent(String content) =
_UpdateContent;
const factory NumberFilterEditorEvent.delete() = _Delete;
}

@freezed
class NumberFilterEditorState with _$NumberFilterEditorState {
const factory NumberFilterEditorState({
required FilterInfo filterInfo,
required NumberFilterPB filter,
}) = _NumberFilterEditorState;

factory NumberFilterEditorState.initial(FilterInfo filterInfo) {
return NumberFilterEditorState(
filterInfo: filterInfo,
filter: filterInfo.numberFilter()!,
);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,227 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/application/filter/number_filter_editor_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';

class NumberFilterChoicechip extends StatelessWidget {
const NumberFilterChoicechip({required this.filterInfo, super.key});
class NumberFilterChoiceChip extends StatefulWidget {
const NumberFilterChoiceChip({
super.key,
required this.filterInfo,
});

final FilterInfo filterInfo;

@override
State<NumberFilterChoiceChip> createState() => _NumberFilterChoiceChipState();
}

class _NumberFilterChoiceChipState extends State<NumberFilterChoiceChip> {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => NumberFilterEditorBloc(
filterInfo: widget.filterInfo,
),
child: BlocBuilder<NumberFilterEditorBloc, NumberFilterEditorState>(
builder: (context, state) {
return AppFlowyPopover(
constraints: BoxConstraints.loose(const Size(200, 100)),
direction: PopoverDirection.bottomWithCenterAligned,
popupBuilder: (_) {
return BlocProvider.value(
value: context.read<NumberFilterEditorBloc>(),
child: const NumberFilterEditor(),
);
},
child: ChoiceChipButton(
filterInfo: state.filterInfo,
),
);
},
),
);
}
}

class NumberFilterEditor extends StatefulWidget {
const NumberFilterEditor({super.key});

@override
State<NumberFilterEditor> createState() => _NumberFilterEditorState();
}

class _NumberFilterEditorState extends State<NumberFilterEditor> {
final popoverMutex = PopoverMutex();

@override
Widget build(BuildContext context) {
return BlocBuilder<NumberFilterEditorBloc, NumberFilterEditorState>(
builder: (context, state) {
final List<Widget> children = [
_buildFilterPanel(context, state),
if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty &&
state.filter.condition !=
NumberFilterConditionPB.NumberIsNotEmpty) ...[
const VSpace(4),
_buildFilterNumberField(context, state),
],
];

return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
child: IntrinsicHeight(child: Column(children: children)),
);
},
);
}

Widget _buildFilterPanel(
BuildContext context,
NumberFilterEditorState state,
) {
return SizedBox(
height: 20,
child: Row(
children: [
Expanded(
child: FlowyText(
state.filterInfo.fieldInfo.name,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(4),
Expanded(
child: NumberFilterConditionPBList(
filterInfo: state.filterInfo,
popoverMutex: popoverMutex,
onCondition: (condition) {
context
.read<NumberFilterEditorBloc>()
.add(NumberFilterEditorEvent.updateCondition(condition));
},
),
),
const HSpace(4),
DisclosureButton(
popoverMutex: popoverMutex,
onAction: (action) {
switch (action) {
case FilterDisclosureAction.delete:
context
.read<NumberFilterEditorBloc>()
.add(const NumberFilterEditorEvent.delete());
break;
}
},
),
],
),
);
}

Widget _buildFilterNumberField(
BuildContext context,
NumberFilterEditorState state,
) {
return FlowyTextField(
text: state.filter.content,
hintText: LocaleKeys.grid_settings_typeAValue.tr(),
debounceDuration: const Duration(milliseconds: 300),
autoFocus: false,
onChanged: (text) {
context
.read<NumberFilterEditorBloc>()
.add(NumberFilterEditorEvent.updateContent(text));
},
);
}
}

class NumberFilterConditionPBList extends StatelessWidget {
const NumberFilterConditionPBList({
super.key,
required this.filterInfo,
required this.popoverMutex,
required this.onCondition,
});

final FilterInfo filterInfo;
final PopoverMutex popoverMutex;
final Function(NumberFilterConditionPB) onCondition;

@override
Widget build(BuildContext context) {
return ChoiceChipButton(filterInfo: filterInfo);
final numberFilter = filterInfo.numberFilter()!;
return PopoverActionList<ConditionWrapper>(
asBarrier: true,
mutex: popoverMutex,
direction: PopoverDirection.bottomWithCenterAligned,
actions: NumberFilterConditionPB.values
.map(
(action) => ConditionWrapper(
action,
numberFilter.condition == action,
),
)
.toList(),
buildChild: (controller) {
return ConditionButton(
conditionName: numberFilter.condition.filterName,
onTap: () => controller.show(),
);
},
onSelected: (action, controller) {
onCondition(action.inner);
controller.close();
},
);
}
}

class ConditionWrapper extends ActionCell {
ConditionWrapper(this.inner, this.isSelected);

final NumberFilterConditionPB inner;
final bool isSelected;

@override
Widget? rightIcon(Color iconColor) =>
isSelected ? const FlowySvg(FlowySvgs.check_s) : null;

@override
String get name => inner.filterName;
}

extension NumberFilterConditionPBExtension on NumberFilterConditionPB {
String get filterName {
return switch (this) {
NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(),
NumberFilterConditionPB.NotEqual =>
LocaleKeys.grid_numberFilter_notEqual.tr(),
NumberFilterConditionPB.LessThan =>
LocaleKeys.grid_numberFilter_lessThan.tr(),
NumberFilterConditionPB.LessThanOrEqualTo =>
LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(),
NumberFilterConditionPB.GreaterThan =>
LocaleKeys.grid_numberFilter_greaterThan.tr(),
NumberFilterConditionPB.GreaterThanOrEqualTo =>
LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(),
NumberFilterConditionPB.NumberIsEmpty =>
LocaleKeys.grid_numberFilter_isEmpty.tr(),
NumberFilterConditionPB.NumberIsNotEmpty =>
LocaleKeys.grid_numberFilter_isNotEmpty.tr(),
_ => "",
};
}
}
Loading

0 comments on commit c159a5e

Please sign in to comment.