浏览代码

fix: filter UI bugs (#1489)

* chore: remove the add filter button if there is no filters can not be added

* fix: update field info after filter was changed

* chore: update filter choicechip ui

* chore: insert and delete one by one to keep the delete/insert index is right

* chore: show filter after creating the default filter

* chore: update textfield_tags version to calm the warnings

* chore: try to fix potential fails on backend test

Co-authored-by: nathan <[email protected]>
Nathan.fooo 2 年之前
父节点
当前提交
182bfae5ad
共有 23 个文件被更改,包括 251 次插入136 次删除
  1. 8 1
      frontend/app_flowy/assets/translations/en.json
  2. 5 3
      frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart
  3. 1 1
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_create_bloc.dart
  4. 31 4
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_menu_bloc.dart
  5. 17 17
      frontend/app_flowy/lib/plugins/grid/application/filter/text_filter_editor_bloc.dart
  6. 27 25
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  7. 10 5
      frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart
  8. 9 13
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  9. 26 5
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/choicechip.dart
  10. 81 25
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/text.dart
  11. 3 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart
  12. 5 8
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart
  13. 5 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart
  14. 1 2
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart
  15. 1 1
      frontend/app_flowy/pubspec.lock
  16. 1 1
      frontend/app_flowy/pubspec.yaml
  17. 3 1
      frontend/app_flowy/test/bloc_test/home_test/app_bloc_test.dart
  18. 2 5
      frontend/rust-lib/flowy-document/src/services/migration.rs
  19. 6 7
      frontend/rust-lib/flowy-grid/src/services/filter/controller.rs
  20. 1 1
      frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs
  21. 1 0
      frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs
  22. 2 2
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs
  23. 5 9
      frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs

+ 8 - 1
frontend/app_flowy/assets/translations/en.json

@@ -175,7 +175,14 @@
       "is": "Is",
       "isNot": "Is not",
       "isEmpty": "Is empty",
-      "isNotEmpty": "Is not empty"
+      "isNotEmpty": "Is not empty",
+      "choicechipPrefix": {
+        "isNot": "Not",
+        "startWith": "Starts with",
+        "endWith": "Ends with",
+        "isEmpty": "is empty",
+        "isNotEmpty": "is not empty"
+      }
     },
     "field": {
       "hide": "Hide",

+ 5 - 3
frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart

@@ -142,6 +142,9 @@ class GridFieldController {
             filters.retainWhere(
               (element) => !deleteFilterIds.contains(element.filter.id),
             );
+
+            _filterPBByFieldId.removeWhere(
+                (key, value) => deleteFilterIds.contains(value.id));
           }
 
           // Inserts the new filter if it's not exist
@@ -151,6 +154,7 @@ class GridFieldController {
             if (filterIndex == -1) {
               final fieldInfo = _findFieldInfoForFilter(fieldInfos, newFilter);
               if (fieldInfo != null) {
+                _filterPBByFieldId[fieldInfo.id] = newFilter;
                 filters.add(FilterInfo(gridId, newFilter, fieldInfo));
               }
             }
@@ -187,10 +191,9 @@ class GridFieldController {
                 }
                 _filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
               }
-
-              _updateFieldInfos();
             }
           }
+          _updateFieldInfos();
           _filterNotifier?.filters = filters;
         },
         (err) => Log.error(err),
@@ -345,7 +348,6 @@ class GridFieldController {
       }
 
       _filterCallbacks[onFilters] = callback;
-      callback();
       _filterNotifier?.addListener(callback);
     }
   }

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/application/filter/filter_create_bloc.dart

@@ -77,7 +77,7 @@ class GridCreateFilterBloc
 
   void _startListening() {
     _onFieldFn = (fields) {
-      fields.retainWhere((field) => field.hasFilter == false);
+      fields.retainWhere((field) => field.canCreateFilter);
       add(GridCreateFilterEvent.didReceiveFields(fields));
     };
     fieldController.addListener(onFields: _onFieldFn);

+ 31 - 4
frontend/app_flowy/lib/plugins/grid/application/filter/filter_menu_bloc.dart

@@ -11,6 +11,7 @@ class GridFilterMenuBloc
   final String viewId;
   final GridFieldController fieldController;
   void Function(List<FilterInfo>)? _onFilterFn;
+  void Function(List<FieldInfo>)? _onFieldFn;
 
   GridFilterMenuBloc({required this.viewId, required this.fieldController})
       : super(GridFilterMenuState.initial(
@@ -32,7 +33,12 @@ class GridFilterMenuBloc
             emit(state.copyWith(isVisible: isVisible));
           },
           didReceiveFields: (List<FieldInfo> fields) {
-            emit(state.copyWith(fields: fields));
+            emit(
+              state.copyWith(
+                fields: fields,
+                creatableFields: getCreatableFilter(fields),
+              ),
+            );
           },
         );
       },
@@ -44,9 +50,18 @@ class GridFilterMenuBloc
       add(GridFilterMenuEvent.didReceiveFilters(filters));
     };
 
-    fieldController.addListener(onFilters: (filters) {
-      _onFilterFn?.call(filters);
-    });
+    _onFieldFn = (fields) {
+      add(GridFilterMenuEvent.didReceiveFields(fields));
+    };
+
+    fieldController.addListener(
+      onFilters: (filters) {
+        _onFilterFn?.call(filters);
+      },
+      onFields: (fields) {
+        _onFieldFn?.call(fields);
+      },
+    );
   }
 
   @override
@@ -55,6 +70,10 @@ class GridFilterMenuBloc
       fieldController.removeListener(onFiltersListener: _onFilterFn!);
       _onFilterFn = null;
     }
+    if (_onFieldFn != null) {
+      fieldController.removeListener(onFieldsListener: _onFieldFn!);
+      _onFieldFn = null;
+    }
     return super.close();
   }
 }
@@ -75,6 +94,7 @@ class GridFilterMenuState with _$GridFilterMenuState {
     required String viewId,
     required List<FilterInfo> filters,
     required List<FieldInfo> fields,
+    required List<FieldInfo> creatableFields,
     required bool isVisible,
   }) = _GridFilterMenuState;
 
@@ -87,6 +107,13 @@ class GridFilterMenuState with _$GridFilterMenuState {
         viewId: viewId,
         filters: filterInfos,
         fields: fields,
+        creatableFields: getCreatableFilter(fields),
         isVisible: false,
       );
 }
+
+List<FieldInfo> getCreatableFilter(List<FieldInfo> fieldInfos) {
+  final List<FieldInfo> creatableFields = List.from(fieldInfos);
+  creatableFields.retainWhere((element) => element.canCreateFilter);
+  return creatableFields;
+}

+ 17 - 17
frontend/app_flowy/lib/plugins/grid/application/filter/text_filter_editor_bloc.dart

@@ -1,5 +1,4 @@
 import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
-import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
@@ -30,26 +29,20 @@ class TextFilterEditorBloc
             _startListening();
           },
           updateCondition: (TextFilterCondition condition) {
-            final textFilter = filterInfo.textFilter()!;
             _ffiService.insertTextFilter(
               filterId: filterInfo.filter.id,
               fieldId: filterInfo.field.id,
               condition: condition,
-              content: textFilter.content,
+              content: state.filter.content,
             );
           },
           updateContent: (content) {
-            final textFilter = filterInfo.textFilter();
-            if (textFilter != null) {
-              _ffiService.insertTextFilter(
-                filterId: filterInfo.filter.id,
-                fieldId: filterInfo.field.id,
-                condition: textFilter.condition,
-                content: content,
-              );
-            } else {
-              Log.error("Invalid text filter");
-            }
+            _ffiService.insertTextFilter(
+              filterId: filterInfo.filter.id,
+              fieldId: filterInfo.field.id,
+              condition: state.filter.condition,
+              content: content,
+            );
           },
           delete: () {
             _ffiService.deleteFilter(
@@ -60,7 +53,11 @@ class TextFilterEditorBloc
           },
           didReceiveFilter: (FilterPB filter) {
             final filterInfo = state.filterInfo.copyWith(filter: filter);
-            emit(state.copyWith(filterInfo: filterInfo));
+            final textFilter = filterInfo.textFilter()!;
+            emit(state.copyWith(
+              filterInfo: filterInfo,
+              filter: textFilter,
+            ));
           },
         );
       },
@@ -99,12 +96,15 @@ class TextFilterEditorEvent with _$TextFilterEditorEvent {
 
 @freezed
 class TextFilterEditorState with _$TextFilterEditorState {
-  const factory TextFilterEditorState({required FilterInfo filterInfo}) =
-      _GridFilterState;
+  const factory TextFilterEditorState({
+    required FilterInfo filterInfo,
+    required TextFilterPB filter,
+  }) = _GridFilterState;
 
   factory TextFilterEditorState.initial(FilterInfo filterInfo) {
     return TextFilterEditorState(
       filterInfo: filterInfo,
+      filter: filterInfo.textFilter()!,
     );
   }
 }

+ 27 - 25
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -78,22 +78,23 @@ class GridRowCache {
     _showRows(changeset.visibleRows);
   }
 
-  void _deleteRows(List<String> deletedRows) {
-    if (deletedRows.isEmpty) return;
-
-    final deletedIndex = _rowList.removeRows(deletedRows);
-    if (deletedIndex.isNotEmpty) {
-      _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
+  void _deleteRows(List<String> deletedRowIds) {
+    for (final rowId in deletedRowIds) {
+      final deletedRow = _rowList.remove(rowId);
+      if (deletedRow != null) {
+        _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
+      }
     }
   }
 
   void _insertRows(List<InsertedRowPB> insertRows) {
-    if (insertRows.isEmpty) return;
-
-    InsertedIndexs insertIndexs =
-        _rowList.insertRows(insertRows, (rowPB) => buildGridRow(rowPB));
-    if (insertIndexs.isNotEmpty) {
-      _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
+    for (final insertedRow in insertRows) {
+      final insertedIndex =
+          _rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
+      if (insertedIndex != null) {
+        _rowChangeReasonNotifier
+            .receive(RowsChangedReason.insert(insertedIndex));
+      }
     }
   }
 
@@ -108,21 +109,22 @@ class GridRowCache {
   }
 
   void _hideRows(List<String> invisibleRows) {
-    if (invisibleRows.isEmpty) return;
-
-    final List<DeletedIndex> deletedRows = _rowList.removeRows(invisibleRows);
-    if (deletedRows.isNotEmpty) {
-      _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRows));
+    for (final rowId in invisibleRows) {
+      final deletedRow = _rowList.remove(rowId);
+      if (deletedRow != null) {
+        _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRow));
+      }
     }
   }
 
   void _showRows(List<InsertedRowPB> visibleRows) {
-    if (visibleRows.isEmpty) return;
-
-    final List<InsertedIndex> insertedRows =
-        _rowList.insertRows(visibleRows, (rowPB) => buildGridRow(rowPB));
-    if (insertedRows.isNotEmpty) {
-      _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertedRows));
+    for (final insertedRow in visibleRows) {
+      final insertedIndex =
+          _rowList.insert(insertedRow.index, buildGridRow(insertedRow.row));
+      if (insertedIndex != null) {
+        _rowChangeReasonNotifier
+            .receive(RowsChangedReason.insert(insertedIndex));
+      }
     }
   }
 
@@ -274,8 +276,8 @@ typedef UpdatedIndexMap = LinkedHashMap<String, UpdatedIndex>;
 
 @freezed
 class RowsChangedReason with _$RowsChangedReason {
-  const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert;
-  const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete;
+  const factory RowsChangedReason.insert(InsertedIndex item) = _Insert;
+  const factory RowsChangedReason.delete(DeletedIndex item) = _Delete;
   const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update;
   const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
   const factory RowsChangedReason.initial() = InitialListState;

+ 10 - 5
frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart

@@ -39,10 +39,10 @@ class RowList {
     _rowInfoByRowId[rowId] = rowInfo;
   }
 
-  void insert(int index, RowInfo rowInfo) {
+  InsertedIndex? insert(int index, RowInfo rowInfo) {
     final rowId = rowInfo.rowPB.id;
     var insertedIndex = index;
-    if (_rowInfos.length < insertedIndex) {
+    if (_rowInfos.length <= insertedIndex) {
       insertedIndex = _rowInfos.length;
     }
 
@@ -50,13 +50,16 @@ class RowList {
     if (oldRowInfo != null) {
       _rowInfos.insert(insertedIndex, rowInfo);
       _rowInfos.remove(oldRowInfo);
+      _rowInfoByRowId[rowId] = rowInfo;
+      return null;
     } else {
       _rowInfos.insert(insertedIndex, rowInfo);
+      _rowInfoByRowId[rowId] = rowInfo;
+      return InsertedIndex(index: insertedIndex, rowId: rowId);
     }
-    _rowInfoByRowId[rowId] = rowInfo;
   }
 
-  RowInfo? remove(String rowId) {
+  DeletedIndex? remove(String rowId) {
     final rowInfo = _rowInfoByRowId[rowId];
     if (rowInfo != null) {
       final index = _rowInfos.indexOf(rowInfo);
@@ -64,8 +67,10 @@ class RowList {
         _rowInfoByRowId.remove(rowInfo.rowPB.id);
         _rowInfos.remove(rowInfo);
       }
+      return DeletedIndex(index: index, rowInfo: rowInfo);
+    } else {
+      return null;
     }
-    return rowInfo;
   }
 
   InsertedIndexs insertRows(

+ 9 - 13
frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart

@@ -207,20 +207,16 @@ class _GridRowsState extends State<_GridRows> {
     return BlocConsumer<GridBloc, GridState>(
       listenWhen: (previous, current) => previous.reason != current.reason,
       listener: (context, state) {
-        state.reason.mapOrNull(
-          insert: (value) {
-            for (final item in value.items) {
-              _key.currentState?.insertItem(item.index);
-            }
+        state.reason.whenOrNull(
+          insert: (item) {
+            _key.currentState?.insertItem(item.index);
           },
-          delete: (value) {
-            for (final item in value.items) {
-              _key.currentState?.removeItem(
-                item.index,
-                (context, animation) =>
-                    _renderRow(context, item.rowInfo, animation),
-              );
-            }
+          delete: (item) {
+            _key.currentState?.removeItem(
+              item.index,
+              (context, animation) =>
+                  _renderRow(context, item.rowInfo, animation),
+            );
           },
         );
       },

+ 26 - 5
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/choicechip.dart

@@ -10,19 +10,17 @@ import 'dart:math' as math;
 class ChoiceChipButton extends StatelessWidget {
   final FilterInfo filterInfo;
   final VoidCallback? onTap;
+  final String filterDesc;
 
   const ChoiceChipButton({
     Key? key,
     required this.filterInfo,
+    this.filterDesc = '',
     this.onTap,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    final arrow = Transform.rotate(
-      angle: -math.pi / 2,
-      child: svgWidget("home/arrow_left"),
-    );
     final borderSide = BorderSide(
       color: AFThemeExtension.of(context).toggleOffFill,
       width: 1.0,
@@ -46,10 +44,33 @@ class ChoiceChipButton extends StatelessWidget {
           filterInfo.field.fieldType.iconName(),
           color: Theme.of(context).colorScheme.onSurface,
         ),
-        rightIcon: arrow,
+        rightIcon: _ChoicechipFilterDesc(filterDesc: filterDesc),
         hoverColor: AFThemeExtension.of(context).lightGreyHover,
         onTap: onTap,
       ),
     );
   }
 }
+
+class _ChoicechipFilterDesc extends StatelessWidget {
+  final String filterDesc;
+  const _ChoicechipFilterDesc({this.filterDesc = '', Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final arrow = Transform.rotate(
+      angle: -math.pi / 2,
+      child: svgWidget("home/arrow_left"),
+    );
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 2),
+      child: Row(
+        children: [
+          if (filterDesc.isNotEmpty) FlowyText(': $filterDesc'),
+          arrow,
+        ],
+      ),
+    );
+  }
+}

+ 81 - 25
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/text.dart

@@ -26,27 +26,62 @@ class TextFilterChoicechip extends StatefulWidget {
 }
 
 class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
+  late TextFilterEditorBloc bloc;
+
+  @override
+  void initState() {
+    bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo)
+      ..add(const TextFilterEditorEvent.initial());
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    bloc.close();
+    super.dispose();
+  }
+
   @override
   Widget build(BuildContext context) {
-    return AppFlowyPopover(
-      controller: PopoverController(),
-      constraints: BoxConstraints.loose(const Size(200, 76)),
-      direction: PopoverDirection.bottomWithCenterAligned,
-      popupBuilder: (BuildContext context) {
-        return TextFilterEditor(filterInfo: widget.filterInfo);
-      },
-      child: ChoiceChipButton(
-        filterInfo: widget.filterInfo,
-        onTap: () {},
+    return BlocProvider.value(
+      value: bloc,
+      child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
+        builder: (blocContext, state) {
+          return AppFlowyPopover(
+            controller: PopoverController(),
+            constraints: BoxConstraints.loose(const Size(200, 76)),
+            direction: PopoverDirection.bottomWithCenterAligned,
+            popupBuilder: (BuildContext context) {
+              return TextFilterEditor(bloc: bloc);
+            },
+            child: ChoiceChipButton(
+              filterInfo: widget.filterInfo,
+              filterDesc: _makeFilterDesc(state),
+            ),
+          );
+        },
       ),
     );
   }
+
+  String _makeFilterDesc(TextFilterEditorState state) {
+    String filterDesc = state.filter.condition.choicechipPrefix;
+    if (state.filter.condition == TextFilterCondition.TextIsEmpty ||
+        state.filter.condition == TextFilterCondition.TextIsNotEmpty) {
+      return filterDesc;
+    }
+
+    if (state.filter.content.isNotEmpty) {
+      filterDesc += " ${state.filter.content}";
+    }
+
+    return filterDesc;
+  }
 }
 
 class TextFilterEditor extends StatefulWidget {
-  final FilterInfo filterInfo;
-  const TextFilterEditor({required this.filterInfo, Key? key})
-      : super(key: key);
+  final TextFilterEditorBloc bloc;
+  const TextFilterEditor({required this.bloc, Key? key}) : super(key: key);
 
   @override
   State<TextFilterEditor> createState() => _TextFilterEditorState();
@@ -57,20 +92,23 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
 
   @override
   Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => TextFilterEditorBloc(filterInfo: widget.filterInfo)
-        ..add(const TextFilterEditorEvent.initial()),
+    return BlocProvider.value(
+      value: widget.bloc,
       child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
         builder: (context, state) {
+          final List<Widget> children = [
+            _buildFilterPannel(context, state),
+          ];
+
+          if (state.filter.condition != TextFilterCondition.TextIsEmpty &&
+              state.filter.condition != TextFilterCondition.TextIsNotEmpty) {
+            children.add(const VSpace(4));
+            children.add(_buildFilterTextField(context, state));
+          }
+
           return Padding(
             padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
-            child: Column(
-              children: [
-                _buildFilterPannel(context, state),
-                const VSpace(4),
-                _buildFilterTextField(context, state),
-              ],
-            ),
+            child: IntrinsicHeight(child: Column(children: children)),
           );
         },
       ),
@@ -113,9 +151,8 @@ class _TextFilterEditorState extends State<TextFilterEditor> {
 
   Widget _buildFilterTextField(
       BuildContext context, TextFilterEditorState state) {
-    final textFilter = state.filterInfo.textFilter()!;
     return FilterTextField(
-      text: textFilter.content,
+      text: state.filter.content,
       hintText: LocaleKeys.grid_settings_typeAValue.tr(),
       autoFucous: false,
       onSubmitted: (text) {
@@ -209,4 +246,23 @@ extension TextFilterConditionExtension on TextFilterCondition {
         return "";
     }
   }
+
+  String get choicechipPrefix {
+    switch (this) {
+      case TextFilterCondition.DoesNotContain:
+        return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
+      case TextFilterCondition.EndsWith:
+        return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr();
+      case TextFilterCondition.IsNot:
+        return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
+      case TextFilterCondition.StartsWith:
+        return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr();
+      case TextFilterCondition.TextIsEmpty:
+        return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr();
+      case TextFilterCondition.TextIsNotEmpty:
+        return LocaleKeys.grid_textFilter_choicechipPrefix_isNotEmpty.tr();
+      default:
+        return "";
+    }
+  }
 }

+ 3 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart

@@ -17,11 +17,13 @@ class GridCreateFilterList extends StatefulWidget {
   final String viewId;
   final GridFieldController fieldController;
   final VoidCallback onClosed;
+  final VoidCallback? onCreateFilter;
 
   const GridCreateFilterList({
     required this.viewId,
     required this.fieldController,
     required this.onClosed,
+    this.onCreateFilter,
     Key? key,
   }) : super(key: key);
 
@@ -102,6 +104,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
 
   void createFilter(FieldInfo field) {
     editBloc.add(GridCreateFilterEvent.createDefaultFilter(field));
+    widget.onCreateFilter?.call();
   }
 }
 

+ 5 - 8
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart

@@ -13,7 +13,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'create_filter_list.dart';
-import 'filter_info.dart';
 import 'menu_item.dart';
 
 class GridFilterMenu extends StatelessWidget {
@@ -28,7 +27,7 @@ class GridFilterMenu extends StatelessWidget {
             children: [
               buildDivider(context),
               const VSpace(6),
-              buildFilterItems(state.viewId, state.filters),
+              buildFilterItems(state.viewId, state),
             ],
           ));
         } else {
@@ -55,8 +54,8 @@ class GridFilterMenu extends StatelessWidget {
     );
   }
 
-  Widget buildFilterItems(String viewId, List<FilterInfo> filters) {
-    final List<Widget> children = filters
+  Widget buildFilterItems(String viewId, GridFilterMenuState state) {
+    final List<Widget> children = state.filters
         .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
         .toList();
     return Row(
@@ -70,7 +69,7 @@ class GridFilterMenu extends StatelessWidget {
           ),
         ),
         const HSpace(4),
-        AddFilterButton(viewId: viewId),
+        if (state.creatableFields.isNotEmpty) AddFilterButton(viewId: viewId),
       ],
     );
   }
@@ -110,9 +109,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
             "home/add",
             color: Theme.of(context).colorScheme.onSurface,
           ),
-          onTap: () {
-            popoverController.show();
-          },
+          onTap: () => popoverController.show(),
         ),
       ),
     );

+ 5 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart

@@ -69,6 +69,11 @@ class _FilterButtonState extends State<FilterButton> {
           viewId: bloc.viewId,
           fieldController: bloc.fieldController,
           onClosed: () => _popoverController.close(),
+          onCreateFilter: () {
+            if (!bloc.state.isVisible) {
+              bloc.add(const GridFilterMenuEvent.toggleMenu());
+            }
+          },
         );
       },
     );

+ 1 - 2
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -63,8 +63,7 @@ class FlowyButton extends StatelessWidget {
     children.add(Expanded(child: text));
 
     if (rightIcon != null) {
-      children.add(
-          SizedBox.fromSize(size: const Size.square(16), child: rightIcon!));
+      children.add(rightIcon!);
     }
 
     Widget child = Row(

+ 1 - 1
frontend/app_flowy/pubspec.lock

@@ -1209,7 +1209,7 @@ packages:
       name: textfield_tags
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0+1"
+    version: "2.0.2"
   textstyle_extensions:
     dependency: "direct main"
     description:

+ 1 - 1
frontend/app_flowy/pubspec.yaml

@@ -70,7 +70,7 @@ dependencies:
   connectivity_plus: ^2.3.6+1
   connectivity_plus_platform_interface: ^1.2.2
   easy_localization: ^3.0.0
-  textfield_tags: ^2.0.0
+  textfield_tags: ^2.0.2
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.2

+ 3 - 1
frontend/app_flowy/test/bloc_test/home_test/app_bloc_test.dart

@@ -24,7 +24,7 @@ void main() {
     assert(bloc.state.app.name == 'Hello world');
   });
 
-  test('delete ap test', () async {
+  test('delete app test', () async {
     final app = await testContext.createTestApp();
     final bloc = AppBloc(app: app)..add(const AppEvent.initial());
     await blocResponseFuture();
@@ -64,9 +64,11 @@ void main() {
     await blocResponseFuture();
     bloc.add(AppEvent.createView("3", DocumentPluginBuilder()));
     await blocResponseFuture();
+    assert(bloc.state.views.length == 3);
 
     final appViewData = AppViewDataContext(appId: app.id);
     appViewData.views = bloc.state.views;
+
     final viewSectionBloc = ViewSectionBloc(
       appViewData: appViewData,
     )..add(const ViewSectionEvent.initial());

+ 2 - 5
frontend/rust-lib/flowy-document/src/services/migration.rs

@@ -39,8 +39,8 @@ impl DocumentMigration {
             }
 
             let document_id = revisions.first().unwrap().object_id.clone();
-            match make_operations_from_revisions(revisions) {
-                Ok(delta) => match DeltaRevisionMigration::run(delta) {
+            if let Ok(delta) = make_operations_from_revisions(revisions) {
+                match DeltaRevisionMigration::run(delta) {
                     Ok(transaction) => {
                         let bytes = Bytes::from(transaction.to_bytes()?);
                         let md5 = format!("{:x}", md5::compute(&bytes));
@@ -59,9 +59,6 @@ impl DocumentMigration {
                             err
                         );
                     }
-                },
-                Err(e) => {
-                    tracing::error!("[Document migration]: Make delta from revisions failed: {:?}", e);
                 }
             }
         }

+ 6 - 7
frontend/rust-lib/flowy-grid/src/services/filter/controller.rs

@@ -212,19 +212,18 @@ impl FilterController {
                     filter_id = new_filter.as_ref().map(|filter| filter.id.clone());
                 }
 
-                // Update the cached filter
+                // Update the corresponding filter in the cache
                 if let Some(filter_rev) = self.delegate.get_filter_rev(updated_filter_type.new.clone()).await {
                     let _ = self.cache_filters(vec![filter_rev]).await;
                 }
 
                 if let Some(filter_id) = filter_id {
-                    let updated_filter = UpdatedFilter {
-                        filter_id,
-                        filter: new_filter,
-                    };
                     notification = Some(FilterChangesetNotificationPB::from_update(
                         &self.view_id,
-                        vec![updated_filter],
+                        vec![UpdatedFilter {
+                            filter_id,
+                            filter: new_filter,
+                        }],
                     ));
                 }
             }
@@ -331,7 +330,7 @@ fn filter_row(
             };
         }
     }
-    None
+    Some((row_rev.id.clone(), true))
 }
 
 // Returns None if there is no change in this cell after applying the filter

+ 1 - 1
frontend/rust-lib/flowy-grid/src/services/group/controller_impls/select_option_controller/util.rs

@@ -99,7 +99,7 @@ pub fn move_group_row(group: &mut Group, context: &mut MoveGroupRowContext) -> O
                     inserted_row.index = Some(to_index as i32);
                     group.insert_row(to_index, row_pb);
                 } else {
-                    tracing::warn!("Mote to index: {} is out of bounds", to_index);
+                    tracing::warn!("Move to index: {} is out of bounds", to_index);
                     tracing::debug!("Group:{} append row:{}", group.id, row_rev.id);
                     group.add_row(row_pb);
                 }

+ 1 - 0
frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs

@@ -359,6 +359,7 @@ impl GridViewRevisionEditor {
             .await
             .did_receive_filter_changed(FilterChangeset::from_delete(filter_type.clone()))
             .await;
+
         let _ = self
             .modify(|pad| {
                 let changeset = pad.delete_filter(&params.filter_id, &filter_type.field_id, &field_type_rev)?;

+ 2 - 2
frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs

@@ -206,8 +206,8 @@ impl GridFilterTest {
                 let mut receiver = self.editor.subscribe_view_changed(&self.grid_id).await.unwrap();
                 match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await {
                     Ok(changed) =>  match changed.unwrap() { GridViewChanged::DidReceiveFilterResult(changed) => {
-                        assert_eq!(changed.visible_rows.len(), visible_row_len);
-                        assert_eq!(changed.invisible_rows.len(), hide_row_len);
+                        assert_eq!(changed.visible_rows.len(), visible_row_len, "visible rows not match");
+                        assert_eq!(changed.invisible_rows.len(), hide_row_len, "invisible rows not match");
                     } },
                     Err(e) => {
                         panic!("Process task timeout: {:?}", e);

+ 5 - 9
frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs

@@ -98,20 +98,16 @@ async fn grid_filter_single_select_is_test2() {
             row_index: 1,
             option_id: option.id.clone(),
         },
-        AssertFilterChanged {
-            visible_row_len: 1,
-            hide_row_len: 0,
-        },
         AssertNumberOfVisibleRows { expected: 3 },
         UpdateSingleSelectCell {
             row_index: 1,
             option_id: "".to_string(),
         },
-        // AssertFilterChanged {
-        //     visible_row_len: 0,
-        //     hide_row_len: 1,
-        // },
-        // AssertNumberOfVisibleRows { expected: 2 },
+        AssertFilterChanged {
+            visible_row_len: 0,
+            hide_row_len: 1,
+        },
+        AssertNumberOfVisibleRows { expected: 2 },
     ];
     test.run_scripts(scripts).await;
 }