瀏覽代碼

Implement Grid's filter UI (#1474)

* fix: border of field cell

* chore: add filter button

* chore: refactor setting button event

* chore: notify row did changed with filter configuration

* chore: add filter bloc test

* chore: config add filter button

* chore: create filter

* chore: load filters and update corresponding field property

* chore: add filter choice chip

* chore: config choice chip ui

* chore: send notification when filter updated

* chore: update filter after update field type option data

* fix: remove/add filter when update field's type option

* chore: create home setting bloc to save the setting of the home screen

* chore: add filter test

* chore: edit text filter ui

* fix: filter cell bugs

* fix: insert row out of bound

* chore: update setting icon in grid

* chore: shrink wrap the filter list

* refactor: extract row container from row cache

* chore: disable split-debuginfo

Co-authored-by: nathan <[email protected]>
Nathan.fooo 2 年之前
父節點
當前提交
69a7ae5201
共有 100 個文件被更改,包括 3006 次插入1203 次删除
  1. 15 1
      frontend/app_flowy/assets/translations/en.json
  2. 25 25
      frontend/app_flowy/lib/plugins/board/application/board_bloc.dart
  3. 1 1
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  4. 2 2
      frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart
  5. 5 5
      frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart
  6. 2 2
      frontend/app_flowy/lib/plugins/board/presentation/board_page.dart
  7. 16 11
      frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart
  8. 1 1
      frontend/app_flowy/lib/plugins/document/document.dart
  9. 19 16
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart
  10. 2 2
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart
  11. 4 4
      frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart
  12. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart
  13. 2 2
      frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart
  14. 1 1
      frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart
  15. 304 85
      frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart
  16. 0 5
      frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart
  17. 5 5
      frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart
  18. 0 206
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_bloc.dart
  19. 179 0
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_create_bloc.dart
  20. 76 2
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart
  21. 92 0
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_menu_bloc.dart
  22. 37 17
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_service.dart
  23. 110 0
      frontend/app_flowy/lib/plugins/grid/application/filter/text_filter_editor_bloc.dart
  24. 13 15
      frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart
  25. 25 20
      frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart
  26. 6 6
      frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart
  27. 11 7
      frontend/app_flowy/lib/plugins/grid/application/grid_service.dart
  28. 4 4
      frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart
  29. 56 106
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  30. 151 0
      frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart
  31. 6 6
      frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart
  32. 5 6
      frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart
  33. 4 3
      frontend/app_flowy/lib/plugins/grid/application/setting/setting_bloc.dart
  34. 32 32
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  35. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart
  36. 1 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  37. 12 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart
  38. 15 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/checkbox.dart
  39. 55 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/choicechip.dart
  40. 15 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/date.dart
  41. 15 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/number.dart
  42. 15 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option.dart
  43. 212 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/text.dart
  44. 14 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/url.dart
  45. 37 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/condition_button.dart
  46. 165 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart
  47. 73 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/disclosure_button.dart
  48. 35 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_info.dart
  49. 138 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart
  50. 41 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu_item.dart
  51. 76 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/text_field.dart
  52. 2 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart
  53. 12 12
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart
  54. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart
  55. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart
  56. 5 6
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart
  57. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart
  58. 4 4
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart
  59. 76 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart
  60. 11 11
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart
  61. 10 9
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart
  62. 11 43
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart
  63. 6 72
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart
  64. 100 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart
  65. 0 5
      frontend/app_flowy/lib/startup/deps_resolver.dart
  66. 0 49
      frontend/app_flowy/lib/workspace/application/home/home_bloc.dart
  67. 124 0
      frontend/app_flowy/lib/workspace/application/home/home_setting_bloc.dart
  68. 7 8
      frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart
  69. 33 31
      frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart
  70. 4 2
      frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart
  71. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart
  72. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart
  73. 1 1
      frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart
  74. 4 4
      frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart
  75. 4 22
      frontend/app_flowy/lib/workspace/presentation/home/navigation.dart
  76. 0 2
      frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart
  77. 1 1
      frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart
  78. 22 4
      frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart
  79. 22 21
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart
  80. 4 4
      frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart
  81. 2 2
      frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart
  82. 15 15
      frontend/app_flowy/test/bloc_test/board_test/util.dart
  83. 146 0
      frontend/app_flowy/test/bloc_test/grid_test/create_filter_test.dart
  84. 56 0
      frontend/app_flowy/test/bloc_test/grid_test/edit_field_change_filter_test.dart
  85. 8 8
      frontend/app_flowy/test/bloc_test/grid_test/edit_field_edit_test.dart
  86. 0 144
      frontend/app_flowy/test/bloc_test/grid_test/filter_bloc_test.dart
  87. 9 4
      frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart
  88. 45 33
      frontend/app_flowy/test/bloc_test/grid_test/util.dart
  89. 1 1
      frontend/rust-lib/Cargo.toml
  90. 9 1
      frontend/rust-lib/flowy-grid/src/entities/block_entities.rs
  91. 21 15
      frontend/rust-lib/flowy-grid/src/entities/field_entities.rs
  92. 23 0
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs
  93. 26 12
      frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs
  94. 12 12
      frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs
  95. 6 6
      frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs
  96. 15 14
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  97. 1 1
      frontend/rust-lib/flowy-grid/src/event_map.rs
  98. 2 3
      frontend/rust-lib/flowy-grid/src/services/block_editor.rs
  99. 5 11
      frontend/rust-lib/flowy-grid/src/services/block_manager.rs
  100. 3 1
      frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs

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

@@ -161,7 +161,21 @@
       "filter": "Filter",
       "sortBy": "Sort by",
       "Properties": "Properties",
-      "group": "Group"
+      "group": "Group",
+      "addFilter": "Add Filter",
+      "deleteFilter": "Delete filter",
+      "filterBy": "Filter by...",
+      "typeAValue": "Type a value..."
+    },
+    "textFilter": {
+      "contains": "Contains",
+      "doesNotContain": "Does not contain",
+      "endsWith": "Ends with",
+      "startWith": "Starts with",
+      "is": "Is",
+      "isNot": "Is not",
+      "isEmpty": "Is empty",
+      "isNotEmpty": "Is not empty"
     },
     "field": {
       "hide": "Hide",

+ 25 - 25
frontend/app_flowy/lib/plugins/board/application/board_bloc.dart

@@ -136,9 +136,9 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
   }
 
   void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) {
-    final fieldContext = fieldController.getField(group.fieldId);
-    if (fieldContext == null) {
-      Log.warn("FieldContext should not be null");
+    final fieldInfo = fieldController.getField(group.fieldId);
+    if (fieldInfo == null) {
+      Log.warn("fieldInfo should not be null");
       return;
     }
 
@@ -147,7 +147,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
     //   group.groupId,
     //   GroupItem(
     //     row: row,
-    //     fieldContext: fieldContext,
+    //     fieldInfo: fieldInfo,
     //     isDraggable: !isEdit,
     //   ),
     // );
@@ -204,7 +204,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
         items: _buildGroupItems(group),
         customData: GroupData(
           group: group,
-          fieldContext: fieldController.getField(group.fieldId)!,
+          fieldInfo: fieldController.getField(group.fieldId)!,
         ),
       );
     }).toList();
@@ -275,10 +275,10 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
 
   List<AppFlowyGroupItem> _buildGroupItems(GroupPB group) {
     final items = group.rows.map((row) {
-      final fieldContext = fieldController.getField(group.fieldId);
+      final fieldInfo = fieldController.getField(group.fieldId);
       return GroupItem(
         row: row,
-        fieldContext: fieldContext!,
+        fieldInfo: fieldInfo!,
       );
     }).toList();
 
@@ -374,11 +374,11 @@ class GridFieldEquatable extends Equatable {
 
 class GroupItem extends AppFlowyGroupItem {
   final RowPB row;
-  final GridFieldContext fieldContext;
+  final FieldInfo fieldInfo;
 
   GroupItem({
     required this.row,
-    required this.fieldContext,
+    required this.fieldInfo,
     bool draggable = true,
   }) {
     super.draggable = draggable;
@@ -401,22 +401,22 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
 
   @override
   void insertRow(GroupPB group, RowPB row, int? index) {
-    final fieldContext = fieldController.getField(group.fieldId);
-    if (fieldContext == null) {
-      Log.warn("FieldContext should not be null");
+    final fieldInfo = fieldController.getField(group.fieldId);
+    if (fieldInfo == null) {
+      Log.warn("fieldInfo should not be null");
       return;
     }
 
     if (index != null) {
       final item = GroupItem(
         row: row,
-        fieldContext: fieldContext,
+        fieldInfo: fieldInfo,
       );
       controller.insertGroupItem(group.groupId, index, item);
     } else {
       final item = GroupItem(
         row: row,
-        fieldContext: fieldContext,
+        fieldInfo: fieldInfo,
       );
       controller.addGroupItem(group.groupId, item);
     }
@@ -429,30 +429,30 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate {
 
   @override
   void updateRow(GroupPB group, RowPB row) {
-    final fieldContext = fieldController.getField(group.fieldId);
-    if (fieldContext == null) {
-      Log.warn("FieldContext should not be null");
+    final fieldInfo = fieldController.getField(group.fieldId);
+    if (fieldInfo == null) {
+      Log.warn("fieldInfo should not be null");
       return;
     }
     controller.updateGroupItem(
       group.groupId,
       GroupItem(
         row: row,
-        fieldContext: fieldContext,
+        fieldInfo: fieldInfo,
       ),
     );
   }
 
   @override
   void addNewRow(GroupPB group, RowPB row, int? index) {
-    final fieldContext = fieldController.getField(group.fieldId);
-    if (fieldContext == null) {
-      Log.warn("FieldContext should not be null");
+    final fieldInfo = fieldController.getField(group.fieldId);
+    if (fieldInfo == null) {
+      Log.warn("fieldInfo should not be null");
       return;
     }
     final item = GroupItem(
       row: row,
-      fieldContext: fieldContext,
+      fieldInfo: fieldInfo,
       draggable: false,
     );
 
@@ -479,10 +479,10 @@ class BoardEditingRow {
 
 class GroupData {
   final GroupPB group;
-  final GridFieldContext fieldContext;
+  final FieldInfo fieldInfo;
   GroupData({
     required this.group,
-    required this.fieldContext,
+    required this.fieldInfo,
   });
 
   CheckboxGroup? asCheckboxGroup() {
@@ -490,7 +490,7 @@ class GroupData {
     return CheckboxGroup(group);
   }
 
-  FieldType get fieldType => fieldContext.fieldType;
+  FieldType get fieldType => fieldInfo.fieldType;
 }
 
 class CheckboxGroup {

+ 1 - 1
frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart

@@ -12,7 +12,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
 
 import 'board_listener.dart';
 
-typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
+typedef OnFieldsChanged = void Function(UnmodifiableListView<FieldInfo>);
 typedef OnGridChanged = void Function(GridPB);
 typedef DidLoadGroups = void Function(List<GroupPB>);
 typedef OnUpdatedGroup = void Function(List<GroupPB>);

+ 2 - 2
frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart

@@ -58,14 +58,14 @@ class BoardDateCellState with _$BoardDateCellState {
   const factory BoardDateCellState({
     required DateCellDataPB? data,
     required String dateStr,
-    required GridFieldContext fieldContext,
+    required FieldInfo fieldInfo,
   }) = _BoardDateCellState;
 
   factory BoardDateCellState.initial(GridDateCellController context) {
     final cellData = context.getCellData();
 
     return BoardDateCellState(
-      fieldContext: context.fieldContext,
+      fieldInfo: context.fieldInfo,
       data: cellData,
       dateStr: _dateStrFromCellData(cellData),
     );

+ 5 - 5
frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart

@@ -63,7 +63,7 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
     return RowInfo(
       gridId: _rowService.gridId,
       fields: UnmodifiableListView(
-        state.cells.map((cell) => cell.identifier.fieldContext).toList(),
+        state.cells.map((cell) => cell.identifier.fieldInfo).toList(),
       ),
       rowPB: state.rowPB,
       visible: true,
@@ -133,10 +133,10 @@ class BoardCellEquatable extends Equatable {
   @override
   List<Object?> get props {
     return [
-      identifier.fieldContext.id,
-      identifier.fieldContext.fieldType,
-      identifier.fieldContext.visibility,
-      identifier.fieldContext.width,
+      identifier.fieldInfo.id,
+      identifier.fieldInfo.fieldType,
+      identifier.fieldInfo.visibility,
+      identifier.fieldInfo.width,
     ];
   }
 }

+ 2 - 2
frontend/app_flowy/lib/plugins/board/presentation/board_page.dart

@@ -236,7 +236,7 @@ class _BoardContentState extends State<BoardContent> {
       child: BoardCard(
         gridId: gridId,
         groupId: groupData.group.groupId,
-        fieldId: groupItem.fieldContext.id,
+        fieldId: groupItem.fieldInfo.id,
         isEditing: isEditing,
         cellBuilder: cellBuilder,
         dataController: cardController,
@@ -285,7 +285,7 @@ class _BoardContentState extends State<BoardContent> {
   ) {
     final rowInfo = RowInfo(
       gridId: gridId,
-      fields: UnmodifiableListView(fieldController.fieldContexts),
+      fields: UnmodifiableListView(fieldController.fieldInfos),
       rowPB: rowPB,
       visible: true,
     );

+ 16 - 11
frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart

@@ -1,10 +1,12 @@
 import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:flowy_infra/image.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/color_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flowy_infra_ui/style_widget/icon_button.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
 import 'package:flutter/material.dart';
 
+import '../../../../generated/locale_keys.g.dart';
 import 'board_setting.dart';
 
 class BoardToolbarContext {
@@ -30,6 +32,7 @@ class BoardToolbar extends StatelessWidget {
       height: 40,
       child: Row(
         children: [
+          const Spacer(),
           _SettingButton(
             settingContext: BoardSettingContext.from(toolbarContext),
           ),
@@ -61,16 +64,18 @@ class _SettingButtonState extends State<_SettingButton> {
   Widget build(BuildContext context) {
     return AppFlowyPopover(
       controller: popoverController,
+      direction: PopoverDirection.leftWithTopAligned,
+      triggerActions: PopoverTriggerFlags.none,
       constraints: BoxConstraints.loose(const Size(260, 400)),
-      child: FlowyIconButton(
-        width: 22,
-        icon: Padding(
-          padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0),
-          child: svgWidget(
-            "grid/setting/setting",
-            color: Theme.of(context).colorScheme.onSurface,
-          ),
-        ),
+      child: FlowyTextButton(
+        LocaleKeys.settings_title.tr(),
+        fontSize: 14,
+        fillColor: Colors.transparent,
+        hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
+        onPressed: () {
+          popoverController.show();
+        },
       ),
       popupBuilder: (BuildContext popoverContext) {
         return BoardSettingListPopover(

+ 1 - 1
frontend/app_flowy/lib/plugins/document/document.dart

@@ -213,7 +213,7 @@ class ShareActionWrapper extends ActionCell {
   ShareActionWrapper(this.inner);
 
   @override
-  Widget? icon(Color iconColor) => null;
+  Widget? leftIcon(Color iconColor) => null;
 
   @override
   String get name => inner.name;

+ 19 - 16
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart

@@ -150,22 +150,22 @@ class IGridCellController<T, D> extends Equatable {
         _fieldNotifier = fieldNotifier,
         _fieldService = FieldService(
           gridId: cellId.gridId,
-          fieldId: cellId.fieldContext.id,
+          fieldId: cellId.fieldInfo.id,
         ),
         _cacheKey = GridCellCacheKey(
           rowId: cellId.rowId,
-          fieldId: cellId.fieldContext.id,
+          fieldId: cellId.fieldInfo.id,
         );
 
   String get gridId => cellId.gridId;
 
   String get rowId => cellId.rowId;
 
-  String get fieldId => cellId.fieldContext.id;
+  String get fieldId => cellId.fieldInfo.id;
 
-  GridFieldContext get fieldContext => cellId.fieldContext;
+  FieldInfo get fieldInfo => cellId.fieldInfo;
 
-  FieldType get fieldType => cellId.fieldContext.fieldType;
+  FieldType get fieldType => cellId.fieldInfo.fieldType;
 
   VoidCallback? startListening({
     required void Function(T?) onCellChanged,
@@ -179,7 +179,7 @@ class IGridCellController<T, D> extends Equatable {
 
     _cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey));
     _cellListener =
-        CellListener(rowId: cellId.rowId, fieldId: cellId.fieldContext.id);
+        CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id);
 
     /// 1.Listen on user edit event and load the new cell data if needed.
     /// For example:
@@ -310,30 +310,33 @@ class IGridCellController<T, D> extends Equatable {
 
   @override
   List<Object> get props =>
-      [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldContext.id];
+      [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id];
 }
 
 class GridCellFieldNotifierImpl extends IGridCellFieldNotifier {
-  final GridFieldController _cache;
-  OnChangeset? _onChangesetFn;
+  final GridFieldController _fieldController;
+  OnReceiveUpdateFields? _onChangesetFn;
 
-  GridCellFieldNotifierImpl(GridFieldController cache) : _cache = cache;
+  GridCellFieldNotifierImpl(GridFieldController cache)
+      : _fieldController = cache;
 
   @override
   void onCellDispose() {
     if (_onChangesetFn != null) {
-      _cache.removeListener(onChangesetListener: _onChangesetFn!);
+      _fieldController.removeListener(onChangesetListener: _onChangesetFn!);
       _onChangesetFn = null;
     }
   }
 
   @override
-  void onCellFieldChanged(void Function(FieldPB p1) callback) {
-    _onChangesetFn = (GridFieldChangesetPB changeset) {
-      for (final updatedField in changeset.updatedFields) {
-        callback(updatedField);
+  void onCellFieldChanged(void Function(FieldInfo) callback) {
+    _onChangesetFn = (List<FieldInfo> filedInfos) {
+      for (final field in filedInfos) {
+        callback(field);
       }
     };
-    _cache.addListener(onChangeset: _onChangesetFn);
+    _fieldController.addListener(
+      onFieldsUpdated: _onChangesetFn,
+    );
   }
 }

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart

@@ -1,10 +1,10 @@
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:flutter/foundation.dart';
 
 import 'cell_service.dart';
 
 abstract class IGridCellFieldNotifier {
-  void onCellFieldChanged(void Function(FieldPB) callback);
+  void onCellFieldChanged(void Function(FieldInfo) callback);
   void onCellDispose();
 }
 

+ 4 - 4
frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart

@@ -60,17 +60,17 @@ class GridCellIdentifier with _$GridCellIdentifier {
   const factory GridCellIdentifier({
     required String gridId,
     required String rowId,
-    required GridFieldContext fieldContext,
+    required FieldInfo fieldInfo,
   }) = _GridCellIdentifier;
 
   // ignore: unused_element
   const GridCellIdentifier._();
 
-  String get fieldId => fieldContext.id;
+  String get fieldId => fieldInfo.id;
 
-  FieldType get fieldType => fieldContext.fieldType;
+  FieldType get fieldType => fieldInfo.fieldType;
 
   ValueKey key() {
-    return ValueKey("$rowId$fieldId${fieldContext.fieldType}");
+    return ValueKey("$rowId$fieldId${fieldInfo.fieldType}");
   }
 }

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart

@@ -176,7 +176,7 @@ class DateCalBloc extends Bloc<DateCalEvent, DateCalState> {
 
     final result = await FieldService.updateFieldTypeOption(
       gridId: cellController.gridId,
-      fieldId: cellController.fieldContext.id,
+      fieldId: cellController.fieldInfo.id,
       typeOptionData: newDateTypeOption.writeToBuffer(),
     );
 

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart

@@ -58,14 +58,14 @@ class DateCellState with _$DateCellState {
   const factory DateCellState({
     required DateCellDataPB? data,
     required String dateStr,
-    required GridFieldContext fieldContext,
+    required FieldInfo fieldInfo,
   }) = _DateCellState;
 
   factory DateCellState.initial(GridDateCellController context) {
     final cellData = context.getCellData();
 
     return DateCellState(
-      fieldContext: context.fieldContext,
+      fieldInfo: context.fieldInfo,
       data: cellData,
       dateStr: _dateStrFromCellData(cellData),
     );

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart

@@ -11,7 +11,7 @@ class SelectOptionService {
   SelectOptionService({required this.cellId});
 
   String get gridId => cellId.gridId;
-  String get fieldId => cellId.fieldContext.id;
+  String get fieldId => cellId.fieldInfo.id;
   String get rowId => cellId.rowId;
 
   Future<Either<Unit, FlowyError>> create({required String name}) {

+ 304 - 85
frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart

@@ -1,22 +1,26 @@
 import 'dart:collection';
 import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
 import 'package:app_flowy/plugins/grid/application/grid_service.dart';
 import 'package:app_flowy/plugins/grid/application/setting/setting_listener.dart';
 import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
 import 'package:flutter/foundation.dart';
 import '../row/row_cache.dart';
 
 class _GridFieldNotifier extends ChangeNotifier {
-  List<GridFieldContext> _fieldContexts = [];
+  List<FieldInfo> _fieldInfos = [];
 
-  set fieldContexts(List<GridFieldContext> fieldContexts) {
-    _fieldContexts = fieldContexts;
+  set fieldInfos(List<FieldInfo> fieldInfos) {
+    _fieldInfos = fieldInfos;
     notifyListeners();
   }
 
@@ -24,91 +28,222 @@ class _GridFieldNotifier extends ChangeNotifier {
     notifyListeners();
   }
 
-  List<GridFieldContext> get fieldContexts => _fieldContexts;
+  List<FieldInfo> get fieldInfos => _fieldInfos;
 }
 
-typedef OnChangeset = void Function(GridFieldChangesetPB);
-typedef OnReceiveFields = void Function(List<GridFieldContext>);
+class _GridFilterNotifier extends ChangeNotifier {
+  List<FilterInfo> _filters = [];
+
+  set filters(List<FilterInfo> filters) {
+    _filters = filters;
+    notifyListeners();
+  }
+
+  void notify() {
+    notifyListeners();
+  }
+
+  List<FilterInfo> get filters => _filters;
+}
+
+typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
+typedef OnReceiveFields = void Function(List<FieldInfo>);
+typedef OnReceiveFilters = void Function(List<FilterInfo>);
 
 class GridFieldController {
   final String gridId;
+  // Listeners
   final GridFieldsListener _fieldListener;
   final SettingListener _settingListener;
-  final Map<OnReceiveFields, VoidCallback> _fieldCallbackMap = {};
-  final Map<OnChangeset, OnChangeset> _changesetCallbackMap = {};
+  final FiltersListener _filterListener;
+
+  // FFI services
   final GridFFIService _gridFFIService;
   final SettingFFIService _settingFFIService;
+  final FilterFFIService _filterFFIService;
 
+  // Field callbacks
+  final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
   _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier();
-  final Map<String, GridGroupConfigurationPB> _configurationByFieldId = {};
 
-  List<GridFieldContext> get fieldContexts =>
-      [..._fieldNotifier?.fieldContexts ?? []];
+  // Field updated callbacks
+  final Map<OnReceiveUpdateFields, void Function(List<FieldInfo>)>
+      _updatedFieldCallbacks = {};
+
+  // Group callbacks
+  final Map<String, GroupConfigurationPB> _groupConfigurationByFieldId = {};
+
+  // Filter callbacks
+  final Map<OnReceiveFilters, VoidCallback> _filterCallbacks = {};
+  _GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
+  final Map<String, FilterPB> _filterPBByFieldId = {};
+
+  // Getters
+  List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
+  List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
+  FieldInfo? getField(String fieldId) {
+    final fields = _fieldNotifier?.fieldInfos
+            .where((element) => element.id == fieldId)
+            .toList() ??
+        [];
+    if (fields.isEmpty) {
+      return null;
+    }
+    assert(fields.length == 1);
+    return fields.first;
+  }
+
+  FilterInfo? getFilter(String filterId) {
+    final filters = _filterNotifier?.filters
+            .where((element) => element.filter.id == filterId)
+            .toList() ??
+        [];
+    if (filters.isEmpty) {
+      return null;
+    }
+    assert(filters.length == 1);
+    return filters.first;
+  }
 
   GridFieldController({required this.gridId})
       : _fieldListener = GridFieldsListener(gridId: gridId),
+        _settingListener = SettingListener(gridId: gridId),
+        _filterListener = FiltersListener(viewId: gridId),
         _gridFFIService = GridFFIService(gridId: gridId),
-        _settingFFIService = SettingFFIService(viewId: gridId),
-        _settingListener = SettingListener(gridId: gridId) {
+        _filterFFIService = FilterFFIService(viewId: gridId),
+        _settingFFIService = SettingFFIService(viewId: gridId) {
     //Listen on field's changes
-    _fieldListener.start(onFieldsChanged: (result) {
+    _listenOnFieldChanges();
+
+    //Listen on setting changes
+    _listenOnSettingChanges();
+
+    //Listen on the fitler changes
+    _listenOnFilterChanges();
+
+    _settingFFIService.getSetting().then((result) {
+      result.fold(
+        (setting) => _updateSettingConfiguration(setting),
+        (err) => Log.error(err),
+      );
+    });
+  }
+
+  void _listenOnFilterChanges() {
+    //Listen on the fitler changes
+    _filterListener.start(onFilterChanged: (result) {
       result.fold(
         (changeset) {
-          _deleteFields(changeset.deletedFields);
-          _insertFields(changeset.insertedFields);
-          _updateFields(changeset.updatedFields);
-          for (final listener in _changesetCallbackMap.values) {
-            listener(changeset);
+          final List<FilterInfo> filters = filterInfos;
+          // Deletes the filters
+          final deleteFilterIds =
+              changeset.deleteFilters.map((e) => e.id).toList();
+          if (deleteFilterIds.isNotEmpty) {
+            filters.retainWhere(
+              (element) => !deleteFilterIds.contains(element.filter.id),
+            );
+          }
+
+          // Inserts the new filter if it's not exist
+          for (final newFilter in changeset.insertFilters) {
+            final filterIndex = filters
+                .indexWhere((element) => element.filter.id == newFilter.id);
+            if (filterIndex == -1) {
+              final fieldInfo = _findFieldInfoForFilter(fieldInfos, newFilter);
+              if (fieldInfo != null) {
+                filters.add(FilterInfo(gridId, newFilter, fieldInfo));
+              }
+            }
           }
+
+          for (final updatedFilter in changeset.updateFilters) {
+            final filterIndex = filters.indexWhere(
+              (element) => element.filter.id == updatedFilter.filterId,
+            );
+            // Remove the old filter
+            if (filterIndex != -1) {
+              filters.removeAt(filterIndex);
+              _filterPBByFieldId.removeWhere(
+                  (key, value) => value.id == updatedFilter.filterId);
+            }
+
+            // Insert the filter if there is a fitler and its field info is
+            // not null
+            if (updatedFilter.hasFilter()) {
+              final fieldInfo = _findFieldInfoForFilter(
+                fieldInfos,
+                updatedFilter.filter,
+              );
+
+              if (fieldInfo != null) {
+                // Insert the filter with the position: filterIndex, otherwise,
+                // append it to the end of the list.
+                final filterInfo =
+                    FilterInfo(gridId, updatedFilter.filter, fieldInfo);
+                if (filterIndex != -1) {
+                  filters.insert(filterIndex, filterInfo);
+                } else {
+                  filters.add(filterInfo);
+                }
+                _filterPBByFieldId[fieldInfo.id] = updatedFilter.filter;
+              }
+
+              _updateFieldInfos();
+            }
+          }
+          _filterNotifier?.filters = filters;
         },
         (err) => Log.error(err),
       );
     });
+  }
 
+  void _listenOnSettingChanges() {
     //Listen on setting changes
     _settingListener.start(onSettingUpdated: (result) {
       result.fold(
-        (setting) => _updateGroupConfiguration(setting),
+        (setting) => _updateSettingConfiguration(setting),
         (r) => Log.error(r),
       );
     });
+  }
 
-    _settingFFIService.getSetting().then((result) {
+  void _listenOnFieldChanges() {
+    //Listen on field's changes
+    _fieldListener.start(onFieldsChanged: (result) {
       result.fold(
-        (setting) => _updateGroupConfiguration(setting),
+        (changeset) {
+          _deleteFields(changeset.deletedFields);
+          _insertFields(changeset.insertedFields);
+
+          final updateFields = _updateFields(changeset.updatedFields);
+          for (final listener in _updatedFieldCallbacks.values) {
+            listener(updateFields);
+          }
+        },
         (err) => Log.error(err),
       );
     });
   }
 
-  GridFieldContext? getField(String fieldId) {
-    final fields = _fieldNotifier?.fieldContexts
-        .where(
-          (element) => element.id == fieldId,
-        )
-        .toList();
-    if (fields?.isEmpty ?? true) {
-      return null;
+  void _updateSettingConfiguration(GridSettingPB setting) {
+    _groupConfigurationByFieldId.clear();
+    for (final configuration in setting.groupConfigurations.items) {
+      _groupConfigurationByFieldId[configuration.fieldId] = configuration;
     }
-    return fields!.first;
-  }
 
-  void _updateGroupConfiguration(GridSettingPB setting) {
-    _configurationByFieldId.clear();
-    for (final configuration in setting.groupConfigurations.items) {
-      _configurationByFieldId[configuration.fieldId] = configuration;
+    for (final configuration in setting.filters.items) {
+      _filterPBByFieldId[configuration.fieldId] = configuration;
     }
-    _updateFieldContexts();
+
+    _updateFieldInfos();
   }
 
-  void _updateFieldContexts() {
+  void _updateFieldInfos() {
     if (_fieldNotifier != null) {
-      for (var field in _fieldNotifier!.fieldContexts) {
-        if (_configurationByFieldId[field.id] != null) {
-          field._isGroupField = true;
-        } else {
-          field._isGroupField = false;
-        }
+      for (var field in _fieldNotifier!.fieldInfos) {
+        field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
+        field._hasFilter = _filterPBByFieldId[field.id] != null;
       }
       _fieldNotifier?.notify();
     }
@@ -116,20 +251,33 @@ class GridFieldController {
 
   Future<void> dispose() async {
     await _fieldListener.stop();
+    await _filterListener.stop();
+    await _settingListener.stop();
+
+    for (final callback in _fieldCallbacks.values) {
+      _fieldNotifier?.removeListener(callback);
+    }
     _fieldNotifier?.dispose();
     _fieldNotifier = null;
+
+    for (final callback in _filterCallbacks.values) {
+      _filterNotifier?.removeListener(callback);
+    }
+    _filterNotifier?.dispose();
+    _filterNotifier = null;
   }
 
-  Future<Either<Unit, FlowyError>> loadFields(
-      {required List<FieldIdPB> fieldIds}) async {
+  Future<Either<Unit, FlowyError>> loadFields({
+    required List<FieldIdPB> fieldIds,
+  }) async {
     final result = await _gridFFIService.getFields(fieldIds: fieldIds);
     return Future(
       () => result.fold(
         (newFields) {
-          _fieldNotifier?.fieldContexts = newFields.items
-              .map((field) => GridFieldContext(field: field))
-              .toList();
-          _updateFieldContexts();
+          _fieldNotifier?.fieldInfos =
+              newFields.map((field) => FieldInfo(field: field)).toList();
+          _loadFilters();
+          _updateFieldInfos();
           return left(unit);
         },
         (err) => right(err),
@@ -137,20 +285,43 @@ class GridFieldController {
     );
   }
 
+  Future<Either<Unit, FlowyError>> _loadFilters() async {
+    return _filterFFIService.getAllFilters().then((result) {
+      return result.fold(
+        (filterPBs) {
+          final List<FilterInfo> filters = [];
+          for (final filterPB in filterPBs) {
+            final fieldInfo = _findFieldInfoForFilter(fieldInfos, filterPB);
+            if (fieldInfo != null) {
+              final filterInfo = FilterInfo(gridId, filterPB, fieldInfo);
+              filters.add(filterInfo);
+            }
+          }
+
+          _updateFieldInfos();
+          _filterNotifier?.filters = filters;
+          return left(unit);
+        },
+        (err) => right(err),
+      );
+    });
+  }
+
   void addListener({
     OnReceiveFields? onFields,
-    OnChangeset? onChangeset,
+    OnReceiveUpdateFields? onFieldsUpdated,
+    OnReceiveFilters? onFilters,
     bool Function()? listenWhen,
   }) {
-    if (onChangeset != null) {
-      callback(c) {
+    if (onFieldsUpdated != null) {
+      callback(List<FieldInfo> updateFields) {
         if (listenWhen != null && listenWhen() == false) {
           return;
         }
-        onChangeset(c);
+        onFieldsUpdated(updateFields);
       }
 
-      _changesetCallbackMap[onChangeset] = callback;
+      _updatedFieldCallbacks[onFieldsUpdated] = callback;
     }
 
     if (onFields != null) {
@@ -158,27 +329,43 @@ class GridFieldController {
         if (listenWhen != null && listenWhen() == false) {
           return;
         }
-        onFields(fieldContexts);
+        onFields(fieldInfos);
       }
 
-      _fieldCallbackMap[onFields] = callback;
+      _fieldCallbacks[onFields] = callback;
       _fieldNotifier?.addListener(callback);
     }
+
+    if (onFilters != null) {
+      callback() {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onFilters(filterInfos);
+      }
+
+      _filterCallbacks[onFilters] = callback;
+      callback();
+      _filterNotifier?.addListener(callback);
+    }
   }
 
   void removeListener({
     OnReceiveFields? onFieldsListener,
-    OnChangeset? onChangesetListener,
+    OnReceiveFilters? onFiltersListener,
+    OnReceiveUpdateFields? onChangesetListener,
   }) {
     if (onFieldsListener != null) {
-      final callback = _fieldCallbackMap.remove(onFieldsListener);
+      final callback = _fieldCallbacks.remove(onFieldsListener);
       if (callback != null) {
         _fieldNotifier?.removeListener(callback);
       }
     }
-
-    if (onChangesetListener != null) {
-      _changesetCallbackMap.remove(onChangesetListener);
+    if (onFiltersListener != null) {
+      final callback = _filterCallbacks.remove(onFiltersListener);
+      if (callback != null) {
+        _filterNotifier?.removeListener(callback);
+      }
     }
   }
 
@@ -186,58 +373,65 @@ class GridFieldController {
     if (deletedFields.isEmpty) {
       return;
     }
-    final List<GridFieldContext> newFields = fieldContexts;
+    final List<FieldInfo> newFields = fieldInfos;
     final Map<String, FieldIdPB> deletedFieldMap = {
       for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder
     };
 
     newFields.retainWhere((field) => (deletedFieldMap[field.id] == null));
-    _fieldNotifier?.fieldContexts = newFields;
+    _fieldNotifier?.fieldInfos = newFields;
   }
 
   void _insertFields(List<IndexFieldPB> insertedFields) {
     if (insertedFields.isEmpty) {
       return;
     }
-    final List<GridFieldContext> newFields = fieldContexts;
+    final List<FieldInfo> newFields = fieldInfos;
     for (final indexField in insertedFields) {
-      final gridField = GridFieldContext(field: indexField.field_1);
+      final gridField = FieldInfo(field: indexField.field_1);
       if (newFields.length > indexField.index) {
         newFields.insert(indexField.index, gridField);
       } else {
         newFields.add(gridField);
       }
     }
-    _fieldNotifier?.fieldContexts = newFields;
+    _fieldNotifier?.fieldInfos = newFields;
   }
 
-  void _updateFields(List<FieldPB> updatedFields) {
-    if (updatedFields.isEmpty) {
-      return;
+  List<FieldInfo> _updateFields(List<FieldPB> updatedFieldPBs) {
+    if (updatedFieldPBs.isEmpty) {
+      return [];
     }
-    final List<GridFieldContext> newFields = fieldContexts;
-    for (final updatedField in updatedFields) {
+
+    final List<FieldInfo> newFields = fieldInfos;
+    final List<FieldInfo> updatedFields = [];
+    for (final updatedFieldPB in updatedFieldPBs) {
       final index =
-          newFields.indexWhere((field) => field.id == updatedField.id);
+          newFields.indexWhere((field) => field.id == updatedFieldPB.id);
       if (index != -1) {
         newFields.removeAt(index);
-        final gridField = GridFieldContext(field: updatedField);
-        newFields.insert(index, gridField);
+        final fieldInfo = FieldInfo(field: updatedFieldPB);
+        newFields.insert(index, fieldInfo);
+        updatedFields.add(fieldInfo);
       }
     }
-    _fieldNotifier?.fieldContexts = newFields;
+
+    if (updatedFields.isNotEmpty) {
+      _fieldNotifier?.fieldInfos = newFields;
+    }
+    return updatedFields;
   }
 }
 
 class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
   final GridFieldController _cache;
-  OnChangeset? _onChangesetFn;
+  OnReceiveUpdateFields? _onChangesetFn;
   OnReceiveFields? _onFieldFn;
   GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache;
 
   @override
-  UnmodifiableListView<GridFieldContext> get fields =>
-      UnmodifiableListView(_cache.fieldContexts);
+  UnmodifiableListView<FieldInfo> get fields =>
+      UnmodifiableListView(_cache.fieldInfos);
 
   @override
   void onRowFieldsChanged(VoidCallback callback) {
@@ -246,14 +440,14 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
   }
 
   @override
-  void onRowFieldChanged(void Function(FieldPB) callback) {
-    _onChangesetFn = (GridFieldChangesetPB changeset) {
-      for (final updatedField in changeset.updatedFields) {
+  void onRowFieldChanged(void Function(FieldInfo) callback) {
+    _onChangesetFn = (List<FieldInfo> fieldInfos) {
+      for (final updatedField in fieldInfos) {
         callback(updatedField);
       }
     };
 
-    _cache.addListener(onChangeset: _onChangesetFn);
+    _cache.addListener(onFieldsUpdated: _onChangesetFn);
   }
 
   @override
@@ -270,10 +464,25 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
   }
 }
 
-class GridFieldContext {
+FieldInfo? _findFieldInfoForFilter(
+    List<FieldInfo> fieldInfos, FilterPB filter) {
+  final fieldIndex = fieldInfos.indexWhere((element) {
+    return element.id == filter.fieldId &&
+        element.fieldType == filter.fieldType;
+  });
+  if (fieldIndex != -1) {
+    return fieldInfos[fieldIndex];
+  } else {
+    return null;
+  }
+}
+
+class FieldInfo {
   final FieldPB _field;
   bool _isGroupField = false;
 
+  bool _hasFilter = false;
+
   String get id => _field.id;
 
   FieldType get fieldType => _field.fieldType;
@@ -290,6 +499,8 @@ class GridFieldContext {
 
   bool get isGroupField => _isGroupField;
 
+  bool get hasFilter => _hasFilter;
+
   bool get canGroup {
     switch (_field.fieldType) {
       case FieldType.Checkbox:
@@ -311,5 +522,13 @@ class GridFieldContext {
     return false;
   }
 
-  GridFieldContext({required FieldPB field}) : _field = field;
+  bool get canCreateFilter {
+    if (hasFilter) return false;
+
+    if (_field.fieldType != FieldType.RichText) return false;
+
+    return true;
+  }
+
+  FieldInfo({required FieldPB field}) : _field = field;
 }

+ 0 - 5
frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart

@@ -34,7 +34,6 @@ class FieldService {
     bool? frozen,
     bool? visibility,
     double? width,
-    List<int>? typeOptionData,
   }) {
     var payload = FieldChangesetPB.create()
       ..gridId = gridId
@@ -60,10 +59,6 @@ class FieldService {
       payload.width = width.toInt();
     }
 
-    if (typeOptionData != null) {
-      payload.typeOptionData = typeOptionData;
-    }
-
     return GridEventUpdateField(payload).send();
   }
 

+ 5 - 5
frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart

@@ -4,7 +4,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_service.dart';
 import 'package:dartz/dartz.dart';
-import 'package:protobuf/protobuf.dart';
+import 'package:protobuf/protobuf.dart' hide FieldInfo;
 import 'package:flowy_sdk/log.dart';
 
 import 'type_option_context.dart';
@@ -18,18 +18,18 @@ class TypeOptionDataController {
   /// Returns a [TypeOptionDataController] used to modify the specified
   /// [FieldPB]'s data
   ///
-  /// Should call [loadTypeOptionData] if the passed-in [GridFieldContext]
+  /// Should call [loadTypeOptionData] if the passed-in [FieldInfo]
   /// is null
   ///
   TypeOptionDataController({
     required this.gridId,
     required this.loader,
-    GridFieldContext? fieldContext,
+    FieldInfo? fieldInfo,
   }) {
-    if (fieldContext != null) {
+    if (fieldInfo != null) {
       _data = TypeOptionPB.create()
         ..gridId = gridId
-        ..field_2 = fieldContext.field;
+        ..field_2 = fieldInfo.field;
     }
   }
 

+ 0 - 206
frontend/app_flowy/lib/plugins/grid/application/filter/filter_bloc.dart

@@ -1,206 +0,0 @@
-import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart';
-import 'package:flowy_sdk/log.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/number_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:flutter_bloc/flutter_bloc.dart';
-import 'package:freezed_annotation/freezed_annotation.dart';
-import 'dart:async';
-import 'filter_service.dart';
-
-part 'filter_bloc.freezed.dart';
-
-class GridFilterBloc extends Bloc<GridFilterEvent, GridFilterState> {
-  final String viewId;
-  final FilterFFIService _ffiService;
-  final FilterListener _listener;
-  GridFilterBloc({required this.viewId})
-      : _ffiService = FilterFFIService(viewId: viewId),
-        _listener = FilterListener(viewId: viewId),
-        super(GridFilterState.initial()) {
-    on<GridFilterEvent>(
-      (event, emit) async {
-        event.when(
-          initial: () async {
-            _startListening();
-            await _loadFilters();
-          },
-          deleteFilter: (
-            String fieldId,
-            String filterId,
-            FieldType fieldType,
-          ) {
-            _ffiService.deleteFilter(
-              fieldId: fieldId,
-              filterId: filterId,
-              fieldType: fieldType,
-            );
-          },
-          didReceiveFilters: (filters) {
-            emit(state.copyWith(filters: filters));
-          },
-          createCheckboxFilter: (
-            String fieldId,
-            CheckboxFilterCondition condition,
-          ) {
-            _ffiService.createCheckboxFilter(
-              fieldId: fieldId,
-              condition: condition,
-            );
-          },
-          createNumberFilter: (
-            String fieldId,
-            NumberFilterCondition condition,
-            String content,
-          ) {
-            _ffiService.createNumberFilter(
-              fieldId: fieldId,
-              condition: condition,
-              content: content,
-            );
-          },
-          createTextFilter: (
-            String fieldId,
-            TextFilterCondition condition,
-            String content,
-          ) {
-            _ffiService.createTextFilter(
-              fieldId: fieldId,
-              condition: condition,
-            );
-          },
-          createDateFilter: (
-            String fieldId,
-            DateFilterCondition condition,
-            int timestamp,
-          ) {
-            _ffiService.createDateFilter(
-              fieldId: fieldId,
-              condition: condition,
-              timestamp: timestamp,
-            );
-          },
-          createDateFilterInRange: (
-            String fieldId,
-            DateFilterCondition condition,
-            int start,
-            int end,
-          ) {
-            _ffiService.createDateFilter(
-              fieldId: fieldId,
-              condition: condition,
-              start: start,
-              end: end,
-            );
-          },
-        );
-      },
-    );
-  }
-
-  void _startListening() {
-    _listener.start(onFilterChanged: (result) {
-      result.fold(
-        (changeset) {
-          final List<FilterPB> filters = List.from(state.filters);
-
-          // Deletes the filters
-          final deleteFilterIds =
-              changeset.deleteFilters.map((e) => e.id).toList();
-          filters.retainWhere(
-            (element) => !deleteFilterIds.contains(element.id),
-          );
-
-          // Inserts the new filter if it's not exist
-          for (final newFilter in changeset.insertFilters) {
-            final index =
-                filters.indexWhere((element) => element.id == newFilter.id);
-            if (index == -1) {
-              filters.add(newFilter);
-            }
-          }
-
-          if (!isClosed) {
-            add(GridFilterEvent.didReceiveFilters(filters));
-          }
-        },
-        (err) => Log.error(err),
-      );
-    });
-  }
-
-  Future<void> _loadFilters() async {
-    final result = await _ffiService.getAllFilters();
-    result.fold(
-      (filters) {
-        if (!isClosed) {
-          add(GridFilterEvent.didReceiveFilters(filters));
-        }
-      },
-      (err) => Log.error(err),
-    );
-  }
-
-  @override
-  Future<void> close() async {
-    await _listener.stop();
-    return super.close();
-  }
-}
-
-@freezed
-class GridFilterEvent with _$GridFilterEvent {
-  const factory GridFilterEvent.initial() = _Initial;
-  const factory GridFilterEvent.didReceiveFilters(List<FilterPB> filters) =
-      _DidReceiveFilters;
-
-  const factory GridFilterEvent.deleteFilter({
-    required String fieldId,
-    required String filterId,
-    required FieldType fieldType,
-  }) = _DeleteFilter;
-
-  const factory GridFilterEvent.createTextFilter({
-    required String fieldId,
-    required TextFilterCondition condition,
-    required String content,
-  }) = _CreateTextFilter;
-
-  const factory GridFilterEvent.createCheckboxFilter({
-    required String fieldId,
-    required CheckboxFilterCondition condition,
-  }) = _CreateCheckboxFilter;
-
-  const factory GridFilterEvent.createNumberFilter({
-    required String fieldId,
-    required NumberFilterCondition condition,
-    required String content,
-  }) = _CreateCheckboxFitler;
-
-  const factory GridFilterEvent.createDateFilter({
-    required String fieldId,
-    required DateFilterCondition condition,
-    required int start,
-  }) = _CreateDateFitler;
-
-  const factory GridFilterEvent.createDateFilterInRange({
-    required String fieldId,
-    required DateFilterCondition condition,
-    required int start,
-    required int end,
-  }) = _CreateDateFitlerInRange;
-}
-
-@freezed
-class GridFilterState with _$GridFilterState {
-  const factory GridFilterState({
-    required List<FilterPB> filters,
-  }) = _GridFilterState;
-
-  factory GridFilterState.initial() => const GridFilterState(
-        filters: [],
-      );
-}

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

@@ -0,0 +1,179 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbenum.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'filter_service.dart';
+
+part 'filter_create_bloc.freezed.dart';
+
+class GridCreateFilterBloc
+    extends Bloc<GridCreateFilterEvent, GridCreateFilterState> {
+  final String viewId;
+  final FilterFFIService _ffiService;
+  final GridFieldController fieldController;
+  void Function(List<FieldInfo>)? _onFieldFn;
+  GridCreateFilterBloc({required this.viewId, required this.fieldController})
+      : _ffiService = FilterFFIService(viewId: viewId),
+        super(GridCreateFilterState.initial(fieldController.fieldInfos)) {
+    on<GridCreateFilterEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () async {
+            _startListening();
+          },
+          didReceiveFields: (List<FieldInfo> fields) {
+            emit(
+              state.copyWith(
+                allFields: fields,
+                creatableFields: _filterFields(fields, state.filterText),
+              ),
+            );
+          },
+          didReceiveFilterText: (String text) {
+            emit(
+              state.copyWith(
+                filterText: text,
+                creatableFields: _filterFields(state.allFields, text),
+              ),
+            );
+          },
+          createDefaultFilter: (FieldInfo field) {
+            emit(state.copyWith(didCreateFilter: true));
+            _createDefaultFilter(field);
+          },
+        );
+      },
+    );
+  }
+
+  List<FieldInfo> _filterFields(
+    List<FieldInfo> fields,
+    String filterText,
+  ) {
+    final List<FieldInfo> allFields = List.from(fields);
+    final keyword = filterText.toLowerCase();
+    allFields.retainWhere((field) {
+      if (field.canCreateFilter) {
+        return false;
+      }
+
+      if (filterText.isNotEmpty) {
+        return field.name.toLowerCase().contains(keyword);
+      }
+
+      return true;
+    });
+
+    return allFields;
+  }
+
+  void _startListening() {
+    _onFieldFn = (fields) {
+      fields.retainWhere((field) => field.hasFilter == false);
+      add(GridCreateFilterEvent.didReceiveFields(fields));
+    };
+    fieldController.addListener(onFields: _onFieldFn);
+  }
+
+  Future<Either<Unit, FlowyError>> _createDefaultFilter(FieldInfo field) async {
+    final fieldId = field.id;
+    switch (field.fieldType) {
+      case FieldType.Checkbox:
+        return _ffiService.insertCheckboxFilter(
+          fieldId: fieldId,
+          condition: CheckboxFilterCondition.IsChecked,
+        );
+      case FieldType.DateTime:
+        final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
+        return _ffiService.insertDateFilter(
+          fieldId: fieldId,
+          condition: DateFilterCondition.DateIs,
+          timestamp: timestamp,
+        );
+      case FieldType.MultiSelect:
+        return _ffiService.insertSingleSelectFilter(
+          fieldId: fieldId,
+          condition: SelectOptionCondition.OptionIs,
+        );
+      case FieldType.Number:
+        return _ffiService.insertNumberFilter(
+          fieldId: fieldId,
+          condition: NumberFilterCondition.Equal,
+          content: "",
+        );
+      case FieldType.RichText:
+        return _ffiService.insertTextFilter(
+          fieldId: fieldId,
+          condition: TextFilterCondition.Contains,
+          content: '',
+        );
+      case FieldType.SingleSelect:
+        return _ffiService.insertSingleSelectFilter(
+          fieldId: fieldId,
+          condition: SelectOptionCondition.OptionIs,
+        );
+      case FieldType.URL:
+        return _ffiService.insertURLFilter(
+          fieldId: fieldId,
+          condition: TextFilterCondition.Contains,
+        );
+    }
+
+    return left(unit);
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onFieldFn != null) {
+      fieldController.removeListener(onFieldsListener: _onFieldFn);
+      _onFieldFn = null;
+    }
+    return super.close();
+  }
+}
+
+@freezed
+class GridCreateFilterEvent with _$GridCreateFilterEvent {
+  const factory GridCreateFilterEvent.initial() = _Initial;
+  const factory GridCreateFilterEvent.didReceiveFields(List<FieldInfo> fields) =
+      _DidReceiveFields;
+
+  const factory GridCreateFilterEvent.createDefaultFilter(FieldInfo field) =
+      _CreateDefaultFilter;
+
+  const factory GridCreateFilterEvent.didReceiveFilterText(String text) =
+      _DidReceiveFilterText;
+}
+
+@freezed
+class GridCreateFilterState with _$GridCreateFilterState {
+  const factory GridCreateFilterState({
+    required String filterText,
+    required List<FieldInfo> creatableFields,
+    required List<FieldInfo> allFields,
+    required bool didCreateFilter,
+  }) = _GridFilterState;
+
+  factory GridCreateFilterState.initial(List<FieldInfo> fields) {
+    return GridCreateFilterState(
+      filterText: "",
+      creatableFields: getCreatableFilter(fields),
+      allFields: fields,
+      didCreateFilter: false,
+    );
+  }
+}
+
+List<FieldInfo> getCreatableFilter(List<FieldInfo> fieldInfos) {
+  final List<FieldInfo> creatableFields = List.from(fieldInfos);
+  creatableFields.retainWhere((element) => element.canCreateFilter);
+  return creatableFields;
+}

+ 76 - 2
frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart

@@ -6,17 +6,18 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/filter_changeset.pb.dart';
 import 'package:dartz/dartz.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
 
 typedef UpdateFilterNotifiedValue
     = Either<FilterChangesetNotificationPB, FlowyError>;
 
-class FilterListener {
+class FiltersListener {
   final String viewId;
 
   PublishNotifier<UpdateFilterNotifiedValue>? _filterNotifier =
       PublishNotifier();
   GridNotificationListener? _listener;
-  FilterListener({required this.viewId});
+  FiltersListener({required this.viewId});
 
   void start({
     required void Function(UpdateFilterNotifiedValue) onFilterChanged,
@@ -51,3 +52,76 @@ class FilterListener {
     _filterNotifier = null;
   }
 }
+
+class FilterListener {
+  final String viewId;
+  final String filterId;
+
+  PublishNotifier<FilterPB>? _onDeleteNotifier = PublishNotifier();
+  PublishNotifier<FilterPB>? _onUpdateNotifier = PublishNotifier();
+
+  GridNotificationListener? _listener;
+  FilterListener({required this.viewId, required this.filterId});
+
+  void start({
+    void Function()? onDeleted,
+    void Function(FilterPB)? onUpdated,
+  }) {
+    _onDeleteNotifier?.addPublishListener((_) {
+      onDeleted?.call();
+    });
+
+    _onUpdateNotifier?.addPublishListener((filter) {
+      onUpdated?.call(filter);
+    });
+
+    _listener = GridNotificationListener(
+      objectId: viewId,
+      handler: _handler,
+    );
+  }
+
+  void handleChangeset(FilterChangesetNotificationPB changeset) {
+    // check the delete filter
+    final deletedIndex = changeset.deleteFilters.indexWhere(
+      (element) => element.id == filterId,
+    );
+    if (deletedIndex != -1) {
+      _onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex];
+    }
+
+    // check the updated filter
+    final updatedIndex = changeset.updateFilters.indexWhere(
+      (element) => element.filter.id == filterId,
+    );
+    if (updatedIndex != -1) {
+      _onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter;
+    }
+  }
+
+  void _handler(
+    GridDartNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case GridDartNotification.DidUpdateFilter:
+        result.fold(
+          (payload) => handleChangeset(
+              FilterChangesetNotificationPB.fromBuffer(payload)),
+          (error) {},
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _onDeleteNotifier?.dispose();
+    _onDeleteNotifier = null;
+
+    _onUpdateNotifier?.dispose();
+    _onUpdateNotifier = null;
+  }
+}

+ 92 - 0
frontend/app_flowy/lib/plugins/grid/application/filter/filter_menu_bloc.dart

@@ -0,0 +1,92 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+part 'filter_menu_bloc.freezed.dart';
+
+class GridFilterMenuBloc
+    extends Bloc<GridFilterMenuEvent, GridFilterMenuState> {
+  final String viewId;
+  final GridFieldController fieldController;
+  void Function(List<FilterInfo>)? _onFilterFn;
+
+  GridFilterMenuBloc({required this.viewId, required this.fieldController})
+      : super(GridFilterMenuState.initial(
+          viewId,
+          fieldController.filterInfos,
+          fieldController.fieldInfos,
+        )) {
+    on<GridFilterMenuEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {
+            _startListening();
+          },
+          didReceiveFilters: (filters) {
+            emit(state.copyWith(filters: filters));
+          },
+          toggleMenu: () {
+            final isVisible = !state.isVisible;
+            emit(state.copyWith(isVisible: isVisible));
+          },
+          didReceiveFields: (List<FieldInfo> fields) {
+            emit(state.copyWith(fields: fields));
+          },
+        );
+      },
+    );
+  }
+
+  void _startListening() {
+    _onFilterFn = (filters) {
+      add(GridFilterMenuEvent.didReceiveFilters(filters));
+    };
+
+    fieldController.addListener(onFilters: (filters) {
+      _onFilterFn?.call(filters);
+    });
+  }
+
+  @override
+  Future<void> close() {
+    if (_onFilterFn != null) {
+      fieldController.removeListener(onFiltersListener: _onFilterFn!);
+      _onFilterFn = null;
+    }
+    return super.close();
+  }
+}
+
+@freezed
+class GridFilterMenuEvent with _$GridFilterMenuEvent {
+  const factory GridFilterMenuEvent.initial() = _Initial;
+  const factory GridFilterMenuEvent.didReceiveFilters(
+      List<FilterInfo> filters) = _DidReceiveFilters;
+  const factory GridFilterMenuEvent.didReceiveFields(List<FieldInfo> fields) =
+      _DidReceiveFields;
+  const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
+}
+
+@freezed
+class GridFilterMenuState with _$GridFilterMenuState {
+  const factory GridFilterMenuState({
+    required String viewId,
+    required List<FilterInfo> filters,
+    required List<FieldInfo> fields,
+    required bool isVisible,
+  }) = _GridFilterMenuState;
+
+  factory GridFilterMenuState.initial(
+    String viewId,
+    List<FilterInfo> filterInfos,
+    List<FieldInfo> fields,
+  ) =>
+      GridFilterMenuState(
+        viewId: viewId,
+        filters: filterInfos,
+        fields: fields,
+        isVisible: false,
+      );
+}

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

@@ -28,37 +28,42 @@ class FilterFFIService {
     });
   }
 
-  Future<Either<Unit, FlowyError>> createTextFilter({
+  Future<Either<Unit, FlowyError>> insertTextFilter({
     required String fieldId,
+    String? filterId,
     required TextFilterCondition condition,
-    String content = "",
+    required String content,
   }) {
     final filter = TextFilterPB()
       ..condition = condition
       ..content = content;
 
-    return createFilter(
+    return insertFilter(
       fieldId: fieldId,
+      filterId: filterId,
       fieldType: FieldType.RichText,
       data: filter.writeToBuffer(),
     );
   }
 
-  Future<Either<Unit, FlowyError>> createCheckboxFilter({
+  Future<Either<Unit, FlowyError>> insertCheckboxFilter({
     required String fieldId,
+    String? filterId,
     required CheckboxFilterCondition condition,
   }) {
     final filter = CheckboxFilterPB()..condition = condition;
 
-    return createFilter(
+    return insertFilter(
       fieldId: fieldId,
+      filterId: filterId,
       fieldType: FieldType.Checkbox,
       data: filter.writeToBuffer(),
     );
   }
 
-  Future<Either<Unit, FlowyError>> createNumberFilter({
+  Future<Either<Unit, FlowyError>> insertNumberFilter({
     required String fieldId,
+    String? filterId,
     required NumberFilterCondition condition,
     String content = "",
   }) {
@@ -66,15 +71,17 @@ class FilterFFIService {
       ..condition = condition
       ..content = content;
 
-    return createFilter(
+    return insertFilter(
       fieldId: fieldId,
+      filterId: filterId,
       fieldType: FieldType.Number,
       data: filter.writeToBuffer(),
     );
   }
 
-  Future<Either<Unit, FlowyError>> createDateFilter({
+  Future<Either<Unit, FlowyError>> insertDateFilter({
     required String fieldId,
+    String? filterId,
     required DateFilterCondition condition,
     int? start,
     int? end,
@@ -93,15 +100,17 @@ class FilterFFIService {
       }
     }
 
-    return createFilter(
+    return insertFilter(
       fieldId: fieldId,
+      filterId: filterId,
       fieldType: FieldType.DateTime,
       data: filter.writeToBuffer(),
     );
   }
 
-  Future<Either<Unit, FlowyError>> createURLFilter({
+  Future<Either<Unit, FlowyError>> insertURLFilter({
     required String fieldId,
+    String? filterId,
     required TextFilterCondition condition,
     String content = "",
   }) {
@@ -109,15 +118,17 @@ class FilterFFIService {
       ..condition = condition
       ..content = content;
 
-    return createFilter(
+    return insertFilter(
       fieldId: fieldId,
+      filterId: filterId,
       fieldType: FieldType.URL,
       data: filter.writeToBuffer(),
     );
   }
 
-  Future<Either<Unit, FlowyError>> createSingleSelectFilter({
+  Future<Either<Unit, FlowyError>> insertSingleSelectFilter({
     required String fieldId,
+    String? filterId,
     required SelectOptionCondition condition,
     List<String> optionIds = const [],
   }) {
@@ -125,15 +136,17 @@ class FilterFFIService {
       ..condition = condition
       ..optionIds.addAll(optionIds);
 
-    return createFilter(
+    return insertFilter(
       fieldId: fieldId,
+      filterId: filterId,
       fieldType: FieldType.SingleSelect,
       data: filter.writeToBuffer(),
     );
   }
 
-  Future<Either<Unit, FlowyError>> createMultiSelectFilter({
+  Future<Either<Unit, FlowyError>> insertMultiSelectFilter({
     required String fieldId,
+    String? filterId,
     required SelectOptionCondition condition,
     List<String> optionIds = const [],
   }) {
@@ -141,25 +154,31 @@ class FilterFFIService {
       ..condition = condition
       ..optionIds.addAll(optionIds);
 
-    return createFilter(
+    return insertFilter(
       fieldId: fieldId,
+      filterId: filterId,
       fieldType: FieldType.MultiSelect,
       data: filter.writeToBuffer(),
     );
   }
 
-  Future<Either<Unit, FlowyError>> createFilter({
+  Future<Either<Unit, FlowyError>> insertFilter({
     required String fieldId,
+    String? filterId,
     required FieldType fieldType,
     required List<int> data,
   }) {
     TextFilterCondition.DoesNotContain.value;
 
-    final insertFilterPayload = CreateFilterPayloadPB.create()
+    var insertFilterPayload = AlterFilterPayloadPB.create()
       ..fieldId = fieldId
       ..fieldType = fieldType
       ..data = data;
 
+    if (filterId != null) {
+      insertFilterPayload.filterId = filterId;
+    }
+
     final payload = GridSettingChangesetPB.create()
       ..gridId = viewId
       ..insertFilter = insertFilterPayload;
@@ -189,6 +208,7 @@ class FilterFFIService {
     final payload = GridSettingChangesetPB.create()
       ..gridId = viewId
       ..deleteFilter = deleteFilterPayload;
+
     return GridEventUpdateGridSetting(payload).send().then((result) {
       return result.fold(
         (l) => left(l),

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

@@ -0,0 +1,110 @@
+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';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'filter_listener.dart';
+import 'filter_service.dart';
+
+part 'text_filter_editor_bloc.freezed.dart';
+
+class TextFilterEditorBloc
+    extends Bloc<TextFilterEditorEvent, TextFilterEditorState> {
+  final FilterInfo filterInfo;
+  final FilterFFIService _ffiService;
+  final FilterListener _listener;
+
+  TextFilterEditorBloc({required this.filterInfo})
+      : _ffiService = FilterFFIService(viewId: filterInfo.viewId),
+        _listener = FilterListener(
+          viewId: filterInfo.viewId,
+          filterId: filterInfo.filter.id,
+        ),
+        super(TextFilterEditorState.initial(filterInfo)) {
+    on<TextFilterEditorEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () async {
+            _startListening();
+          },
+          updateCondition: (TextFilterCondition condition) {
+            final textFilter = filterInfo.textFilter()!;
+            _ffiService.insertTextFilter(
+              filterId: filterInfo.filter.id,
+              fieldId: filterInfo.field.id,
+              condition: condition,
+              content: textFilter.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");
+            }
+          },
+          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);
+            emit(state.copyWith(filterInfo: filterInfo));
+          },
+        );
+      },
+    );
+  }
+
+  void _startListening() {
+    _listener.start(
+      onDeleted: () {
+        if (!isClosed) add(const TextFilterEditorEvent.delete());
+      },
+      onUpdated: (filter) {
+        if (!isClosed) add(TextFilterEditorEvent.didReceiveFilter(filter));
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    await _listener.stop();
+    return super.close();
+  }
+}
+
+@freezed
+class TextFilterEditorEvent with _$TextFilterEditorEvent {
+  const factory TextFilterEditorEvent.initial() = _Initial;
+  const factory TextFilterEditorEvent.didReceiveFilter(FilterPB filter) =
+      _DidReceiveFilter;
+  const factory TextFilterEditorEvent.updateCondition(
+      TextFilterCondition condition) = _UpdateCondition;
+  const factory TextFilterEditorEvent.updateContent(String content) =
+      _UpdateContent;
+  const factory TextFilterEditorEvent.delete() = _Delete;
+}
+
+@freezed
+class TextFilterEditorState with _$TextFilterEditorState {
+  const factory TextFilterEditorState({required FilterInfo filterInfo}) =
+      _GridFilterState;
+
+  factory TextFilterEditorState.initial(FilterInfo filterInfo) {
+    return TextFilterEditorState(
+      filterInfo: filterInfo,
+    );
+  }
+}

+ 13 - 15
frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart

@@ -16,12 +16,11 @@ import 'row/row_service.dart';
 part 'grid_bloc.freezed.dart';
 
 class GridBloc extends Bloc<GridEvent, GridState> {
-  final GridDataController dataController;
+  final GridController gridController;
   void Function()? _createRowOperation;
 
-  GridBloc({required ViewPB view})
-      : dataController = GridDataController(view: view),
-        super(GridState.initial(view.id)) {
+  GridBloc({required ViewPB view, required this.gridController})
+      : super(GridState.initial(view.id)) {
     on<GridEvent>(
       (event, emit) async {
         await event.when(
@@ -32,9 +31,9 @@ class GridBloc extends Bloc<GridEvent, GridState> {
           createRow: () {
             state.loadingState.when(
               loading: () {
-                _createRowOperation = () => dataController.createRow();
+                _createRowOperation = () => gridController.createRow();
               },
-              finish: (_) => dataController.createRow(),
+              finish: (_) => gridController.createRow(),
             );
           },
           deleteRow: (rowInfo) async {
@@ -66,17 +65,17 @@ class GridBloc extends Bloc<GridEvent, GridState> {
 
   @override
   Future<void> close() async {
-    await dataController.dispose();
+    await gridController.dispose();
     return super.close();
   }
 
   GridRowCache? getRowCache(String blockId, String rowId) {
-    final GridBlockCache? blockCache = dataController.blocks[blockId];
+    final GridBlockCache? blockCache = gridController.blocks[blockId];
     return blockCache?.rowCache;
   }
 
   void _startListening() {
-    dataController.addListener(
+    gridController.addListener(
       onGridChanged: (grid) {
         if (!isClosed) {
           add(GridEvent.didReceiveGridUpdate(grid));
@@ -96,7 +95,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
   }
 
   Future<void> _openGrid(Emitter<GridState> emit) async {
-    final result = await dataController.openGrid();
+    final result = await gridController.openGrid();
     result.fold(
       (grid) {
         if (_createRowOperation != null) {
@@ -124,7 +123,7 @@ class GridEvent with _$GridEvent {
     RowsChangedReason listState,
   ) = _DidReceiveRowUpdate;
   const factory GridEvent.didReceiveFieldUpdate(
-    UnmodifiableListView<GridFieldContext> fields,
+    List<FieldInfo> fields,
   ) = _DidReceiveFieldUpdate;
 
   const factory GridEvent.didReceiveGridUpdate(
@@ -163,9 +162,9 @@ class GridLoadingState with _$GridLoadingState {
 }
 
 class GridFieldEquatable extends Equatable {
-  final UnmodifiableListView<GridFieldContext> _fields;
+  final List<FieldInfo> _fields;
   const GridFieldEquatable(
-    UnmodifiableListView<GridFieldContext> fields,
+    List<FieldInfo> fields,
   ) : _fields = fields;
 
   @override
@@ -182,6 +181,5 @@ class GridFieldEquatable extends Equatable {
     ];
   }
 
-  UnmodifiableListView<GridFieldContext> get value =>
-      UnmodifiableListView(_fields);
+  UnmodifiableListView<FieldInfo> get value => UnmodifiableListView(_fields);
 }

+ 25 - 20
frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart

@@ -1,5 +1,6 @@
 import 'dart:collection';
 
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
@@ -12,7 +13,8 @@ import 'field/field_controller.dart';
 import 'prelude.dart';
 import 'row/row_cache.dart';
 
-typedef OnFieldsChanged = void Function(UnmodifiableListView<GridFieldContext>);
+typedef OnFieldsChanged = void Function(List<FieldInfo>);
+typedef OnFiltersChanged = void Function(List<FilterInfo>);
 typedef OnGridChanged = void Function(GridPB);
 
 typedef OnRowsChanged = void Function(
@@ -21,20 +23,19 @@ typedef OnRowsChanged = void Function(
 );
 typedef ListenOnRowChangedCondition = bool Function();
 
-class GridDataController {
+class GridController {
   final String gridId;
   final GridFFIService _gridFFIService;
   final GridFieldController fieldController;
+  OnRowsChanged? _onRowChanged;
+  OnGridChanged? _onGridChanged;
 
+  // Getters
   // key: the block id
   final LinkedHashMap<String, GridBlockCache> _blocks;
   UnmodifiableMapView<String, GridBlockCache> get blocks =>
       UnmodifiableMapView(_blocks);
 
-  OnRowsChanged? _onRowChanged;
-  OnFieldsChanged? _onFieldsChanged;
-  OnGridChanged? _onGridChanged;
-
   List<RowInfo> get rowInfos {
     final List<RowInfo> rows = [];
     for (var block in _blocks.values) {
@@ -43,7 +44,7 @@ class GridDataController {
     return rows;
   }
 
-  GridDataController({required ViewPB view})
+  GridController({required ViewPB view})
       : gridId = view.id,
         // ignore: prefer_collection_literals
         _blocks = LinkedHashMap(),
@@ -51,32 +52,36 @@ class GridDataController {
         fieldController = GridFieldController(gridId: view.id);
 
   void addListener({
-    required OnGridChanged onGridChanged,
-    required OnRowsChanged onRowsChanged,
-    required OnFieldsChanged onFieldsChanged,
+    OnGridChanged? onGridChanged,
+    OnRowsChanged? onRowsChanged,
+    OnFieldsChanged? onFieldsChanged,
+    OnFiltersChanged? onFiltersChanged,
   }) {
     _onGridChanged = onGridChanged;
     _onRowChanged = onRowsChanged;
-    _onFieldsChanged = onFieldsChanged;
 
-    fieldController.addListener(onFields: (fields) {
-      _onFieldsChanged?.call(UnmodifiableListView(fields));
-    });
+    fieldController.addListener(
+      onFields: onFieldsChanged,
+      onFilters: onFiltersChanged,
+    );
   }
 
   // Loads the rows from each block
   Future<Either<Unit, FlowyError>> openGrid() async {
-    final result = await _gridFFIService.openGrid();
-    return Future(
-      () => result.fold(
+    return _gridFFIService.openGrid().then((result) {
+      return result.fold(
         (grid) async {
           _initialBlocks(grid.blocks);
           _onGridChanged?.call(grid);
-          return await fieldController.loadFields(fieldIds: grid.fields);
+
+          final result = await fieldController.loadFields(
+            fieldIds: grid.fields,
+          );
+          return result;
         },
         (err) => right(err),
-      ),
-    );
+      );
+    });
   }
 
   Future<void> createRow() async {

+ 6 - 6
frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart

@@ -15,7 +15,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
   GridHeaderBloc({
     required this.gridId,
     required this.fieldController,
-  }) : super(GridHeaderState.initial(fieldController.fieldContexts)) {
+  }) : super(GridHeaderState.initial(fieldController.fieldInfos)) {
     on<GridHeaderEvent>(
       (event, emit) async {
         await event.map(
@@ -41,7 +41,7 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 
   Future<void> _moveField(
       _MoveField value, Emitter<GridHeaderState> emit) async {
-    final fields = List<GridFieldContext>.from(state.fields);
+    final fields = List<FieldInfo>.from(state.fields);
     fields.insert(value.toIndex, fields.removeAt(value.fromIndex));
     emit(state.copyWith(fields: fields));
 
@@ -69,18 +69,18 @@ class GridHeaderBloc extends Bloc<GridHeaderEvent, GridHeaderState> {
 @freezed
 class GridHeaderEvent with _$GridHeaderEvent {
   const factory GridHeaderEvent.initial() = _InitialHeader;
-  const factory GridHeaderEvent.didReceiveFieldUpdate(
-      List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
+  const factory GridHeaderEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =
+      _DidReceiveFieldUpdate;
   const factory GridHeaderEvent.moveField(
       FieldPB field, int fromIndex, int toIndex) = _MoveField;
 }
 
 @freezed
 class GridHeaderState with _$GridHeaderState {
-  const factory GridHeaderState({required List<GridFieldContext> fields}) =
+  const factory GridHeaderState({required List<FieldInfo> fields}) =
       _GridHeaderState;
 
-  factory GridHeaderState.initial(List<GridFieldContext> fields) {
+  factory GridHeaderState.initial(List<FieldInfo> fields) {
     // final List<FieldPB> newFields = List.from(fields);
     // newFields.retainWhere((field) => field.visibility);
     return GridHeaderState(fields: fields);

+ 11 - 7
frontend/app_flowy/lib/plugins/grid/application/grid_service.dart

@@ -42,12 +42,16 @@ class GridFFIService {
     return GridEventCreateBoardCard(payload).send();
   }
 
-  Future<Either<RepeatedFieldPB, FlowyError>> getFields(
-      {required List<FieldIdPB> fieldIds}) {
-    final payload = GetFieldPayloadPB.create()
-      ..gridId = gridId
-      ..fieldIds = RepeatedFieldIdPB(items: fieldIds);
-    return GridEventGetFields(payload).send();
+  Future<Either<List<FieldPB>, FlowyError>> getFields(
+      {List<FieldIdPB>? fieldIds}) {
+    var payload = GetFieldPayloadPB.create()..gridId = gridId;
+
+    if (fieldIds != null) {
+      payload.fieldIds = RepeatedFieldIdPB(items: fieldIds);
+    }
+    return GridEventGetFields(payload).send().then((result) {
+      return result.fold((l) => left(l.items), (r) => right(r));
+    });
   }
 
   Future<Either<Unit, FlowyError>> closeGrid() {
@@ -55,7 +59,7 @@ class GridFFIService {
     return FolderEventCloseView(request).send();
   }
 
-  Future<Either<RepeatedGridGroupPB, FlowyError>> loadGroups() {
+  Future<Either<RepeatedGroupPB, FlowyError>> loadGroups() {
     final payload = GridIdPB(value: gridId);
     return GridEventGetGroup(payload).send();
   }

+ 4 - 4
frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart

@@ -35,7 +35,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
           },
           didReceiveCells: (_DidReceiveCells value) async {
             final cells = value.gridCellMap.values
-                .map((e) => GridCellEquatable(e.fieldContext))
+                .map((e) => GridCellEquatable(e.fieldInfo))
                 .toList();
             emit(state.copyWith(
               gridCellMap: value.gridCellMap,
@@ -88,16 +88,16 @@ class RowState with _$RowState {
         gridCellMap: cellDataMap,
         cells: UnmodifiableListView(
           cellDataMap.values
-              .map((e) => GridCellEquatable(e.fieldContext))
+              .map((e) => GridCellEquatable(e.fieldInfo))
               .toList(),
         ),
       );
 }
 
 class GridCellEquatable extends Equatable {
-  final GridFieldContext _fieldContext;
+  final FieldInfo _fieldContext;
 
-  const GridCellEquatable(GridFieldContext field) : _fieldContext = field;
+  const GridCellEquatable(FieldInfo field) : _fieldContext = field;
 
   @override
   List<Object?> get props => [

+ 56 - 106
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -4,18 +4,19 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'package:flowy_sdk/dispatch/dispatch.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
 import 'package:flutter/foundation.dart';
 import 'package:freezed_annotation/freezed_annotation.dart';
+
+import 'row_list.dart';
 part 'row_cache.freezed.dart';
 
 typedef RowUpdateCallback = void Function();
 
 abstract class IGridRowFieldNotifier {
-  UnmodifiableListView<GridFieldContext> get fields;
+  UnmodifiableListView<FieldInfo> get fields;
   void onRowFieldsChanged(VoidCallback callback);
-  void onRowFieldChanged(void Function(FieldPB) callback);
+  void onRowFieldChanged(void Function(FieldInfo) callback);
   void onRowDispose();
 }
 
@@ -30,17 +31,14 @@ class GridRowCache {
 
   /// _rows containers the current block's rows
   /// Use List to reverse the order of the GridRow.
-  List<RowInfo> _rowInfos = [];
-
-  /// Use Map for faster access the raw row data.
-  final HashMap<String, RowInfo> _rowInfoByRowId;
+  final RowList _rowList = RowList();
 
   final GridCellCache _cellCache;
   final IGridRowFieldNotifier _fieldNotifier;
   final _RowChangesetNotifier _rowChangeReasonNotifier;
 
   UnmodifiableListView<RowInfo> get visibleRows {
-    var visibleRows = [..._rowInfos];
+    var visibleRows = [..._rowList.rows];
     visibleRows.retainWhere((element) => element.visible);
     return UnmodifiableListView(visibleRows);
   }
@@ -52,7 +50,6 @@ class GridRowCache {
     required this.block,
     required IGridRowFieldNotifier notifier,
   })  : _cellCache = GridCellCache(gridId: gridId),
-        _rowInfoByRowId = HashMap(),
         _rowChangeReasonNotifier = _RowChangesetNotifier(),
         _fieldNotifier = notifier {
     //
@@ -63,8 +60,7 @@ class GridRowCache {
 
     for (final row in block.rows) {
       final rowInfo = buildGridRow(row);
-      _rowInfos.add(rowInfo);
-      _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
+      _rowList.add(rowInfo);
     }
   }
 
@@ -83,90 +79,50 @@ class GridRowCache {
   }
 
   void _deleteRows(List<String> deletedRows) {
-    if (deletedRows.isEmpty) {
-      return;
-    }
+    if (deletedRows.isEmpty) return;
 
-    final List<RowInfo> newRows = [];
-    final DeletedIndexs deletedIndex = [];
-    final Map<String, String> deletedRowByRowId = {
-      for (var rowId in deletedRows) rowId: rowId
-    };
-
-    _rowInfos.asMap().forEach((index, RowInfo rowInfo) {
-      if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
-        newRows.add(rowInfo);
-      } else {
-        _rowInfoByRowId.remove(rowInfo.rowPB.id);
-        deletedIndex.add(DeletedIndex(index: index, row: rowInfo));
-      }
-    });
-    _rowInfos = newRows;
-    _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
+    final deletedIndex = _rowList.removeRows(deletedRows);
+    if (deletedIndex.isNotEmpty) {
+      _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex));
+    }
   }
 
   void _insertRows(List<InsertedRowPB> insertRows) {
-    if (insertRows.isEmpty) {
-      return;
-    }
+    if (insertRows.isEmpty) return;
 
-    InsertedIndexs insertIndexs = [];
-    for (final InsertedRowPB insertRow in insertRows) {
-      final insertIndex = InsertedIndex(
-        index: insertRow.index,
-        rowId: insertRow.row.id,
-      );
-      insertIndexs.add(insertIndex);
-      final rowInfo = buildGridRow(insertRow.row);
-      _rowInfos.insert(insertRow.index, rowInfo);
-      _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
+    InsertedIndexs insertIndexs =
+        _rowList.insertRows(insertRows, (rowPB) => buildGridRow(rowPB));
+    if (insertIndexs.isNotEmpty) {
+      _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
     }
-
-    _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs));
   }
 
   void _updateRows(List<RowPB> updatedRows) {
-    if (updatedRows.isEmpty) {
-      return;
-    }
-
-    final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-    for (final RowPB updatedRow in updatedRows) {
-      final rowId = updatedRow.id;
-      final index = _rowInfos.indexWhere(
-        (rowInfo) => rowInfo.rowPB.id == rowId,
-      );
-      if (index != -1) {
-        final rowInfo = buildGridRow(updatedRow);
-        _rowInfoByRowId[rowId] = rowInfo;
+    if (updatedRows.isEmpty) return;
 
-        _rowInfos.removeAt(index);
-        _rowInfos.insert(index, rowInfo);
-        updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
-      }
+    final updatedIndexs =
+        _rowList.updateRows(updatedRows, (rowPB) => buildGridRow(rowPB));
+    if (updatedIndexs.isNotEmpty) {
+      _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
     }
-
-    _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
   }
 
   void _hideRows(List<String> invisibleRows) {
-    for (final rowId in invisibleRows) {
-      _rowInfoByRowId[rowId]?.visible = false;
-    }
+    if (invisibleRows.isEmpty) return;
 
-    if (invisibleRows.isNotEmpty) {
-      _rowChangeReasonNotifier
-          .receive(const RowsChangedReason.filterDidChange());
+    final List<DeletedIndex> deletedRows = _rowList.removeRows(invisibleRows);
+    if (deletedRows.isNotEmpty) {
+      _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRows));
     }
   }
 
-  void _showRows(List<String> visibleRows) {
-    for (final rowId in visibleRows) {
-      _rowInfoByRowId[rowId]?.visible = true;
-    }
-    if (visibleRows.isNotEmpty) {
-      _rowChangeReasonNotifier
-          .receive(const RowsChangedReason.filterDidChange());
+  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));
     }
   }
 
@@ -188,7 +144,7 @@ class GridRowCache {
 
       notifyUpdate() {
         if (onCellUpdated != null) {
-          final rowInfo = _rowInfoByRowId[rowId];
+          final rowInfo = _rowList.get(rowId);
           if (rowInfo != null) {
             final GridCellMap cellDataMap =
                 _makeGridCells(rowId, rowInfo.rowPB);
@@ -214,7 +170,7 @@ class GridRowCache {
   }
 
   GridCellMap loadGridCells(String rowId) {
-    final RowPB? data = _rowInfoByRowId[rowId]?.rowPB;
+    final RowPB? data = _rowList.get(rowId)?.rowPB;
     if (data == null) {
       _loadRow(rowId);
     }
@@ -242,7 +198,7 @@ class GridRowCache {
         cellDataMap[field.id] = GridCellIdentifier(
           rowId: rowId,
           gridId: gridId,
-          fieldContext: field,
+          fieldInfo: field,
         );
       }
     }
@@ -256,26 +212,20 @@ class GridRowCache {
     final updatedRow = optionRow.row;
     updatedRow.freeze();
 
-    final index =
-        _rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id);
-    if (index != -1) {
-      // update the corresponding row in _rows if they are not the same
-      if (_rowInfos[index].rowPB != updatedRow) {
-        final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow);
-        _rowInfos.insert(index, rowInfo);
-        _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo;
-
-        // Calculate the update index
-        final UpdatedIndexs updatedIndexs = UpdatedIndexs();
-        updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
-          index: index,
-          rowId: rowInfo.rowPB.id,
-        );
+    final rowInfo = _rowList.get(updatedRow.id);
+    final rowIndex = _rowList.indexOfRow(updatedRow.id);
+    if (rowInfo != null && rowIndex != null) {
+      final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow);
+      _rowList.remove(updatedRow.id);
+      _rowList.insert(rowIndex, updatedRowInfo);
+
+      final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
+      updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex(
+        index: rowIndex,
+        rowId: updatedRowInfo.rowPB.id,
+      );
 
-        //
-        _rowChangeReasonNotifier
-            .receive(RowsChangedReason.update(updatedIndexs));
-      }
+      _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
     }
   }
 
@@ -302,7 +252,6 @@ class _RowChangesetNotifier extends ChangeNotifier {
       update: (_) => notifyListeners(),
       fieldDidChange: (_) => notifyListeners(),
       initial: (_) {},
-      filterDidChange: (_FilterDidChange value) => notifyListeners(),
     );
   }
 }
@@ -311,7 +260,7 @@ class _RowChangesetNotifier extends ChangeNotifier {
 class RowInfo with _$RowInfo {
   factory RowInfo({
     required String gridId,
-    required UnmodifiableListView<GridFieldContext> fields,
+    required UnmodifiableListView<FieldInfo> fields,
     required RowPB rowPB,
     required bool visible,
   }) = _RowInfo;
@@ -319,15 +268,16 @@ class RowInfo with _$RowInfo {
 
 typedef InsertedIndexs = List<InsertedIndex>;
 typedef DeletedIndexs = List<DeletedIndex>;
-typedef UpdatedIndexs = LinkedHashMap<String, UpdatedIndex>;
+// key: id of the row
+// value: UpdatedIndex
+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.update(UpdatedIndexs indexs) = _Update;
+  const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update;
   const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
-  const factory RowsChangedReason.filterDidChange() = _FilterDidChange;
   const factory RowsChangedReason.initial() = InitialListState;
 }
 
@@ -342,10 +292,10 @@ class InsertedIndex {
 
 class DeletedIndex {
   final int index;
-  final RowInfo row;
+  final RowInfo rowInfo;
   DeletedIndex({
     required this.index,
-    required this.row,
+    required this.rowInfo,
   });
 }
 

+ 151 - 0
frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart

@@ -0,0 +1,151 @@
+import 'dart:collection';
+
+import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
+
+import 'row_cache.dart';
+
+class RowList {
+  /// _rows containers the current block's rows
+  /// Use List to reverse the order of the GridRow.
+  List<RowInfo> _rowInfos = [];
+
+  List<RowInfo> get rows => List.from(_rowInfos);
+
+  /// Use Map for faster access the raw row data.
+  final HashMap<String, RowInfo> _rowInfoByRowId = HashMap();
+
+  RowInfo? get(String rowId) {
+    return _rowInfoByRowId[rowId];
+  }
+
+  int? indexOfRow(String rowId) {
+    final rowInfo = _rowInfoByRowId[rowId];
+    if (rowInfo != null) {
+      return _rowInfos.indexOf(rowInfo);
+    }
+    return null;
+  }
+
+  void add(RowInfo rowInfo) {
+    final rowId = rowInfo.rowPB.id;
+    if (contains(rowId)) {
+      final index =
+          _rowInfos.indexWhere((element) => element.rowPB.id == rowId);
+      _rowInfos.removeAt(index);
+      _rowInfos.insert(index, rowInfo);
+    } else {
+      _rowInfos.add(rowInfo);
+    }
+    _rowInfoByRowId[rowId] = rowInfo;
+  }
+
+  void insert(int index, RowInfo rowInfo) {
+    final rowId = rowInfo.rowPB.id;
+    var insertedIndex = index;
+    if (_rowInfos.length < insertedIndex) {
+      insertedIndex = _rowInfos.length;
+    }
+
+    final oldRowInfo = get(rowId);
+    if (oldRowInfo != null) {
+      _rowInfos.insert(insertedIndex, rowInfo);
+      _rowInfos.remove(oldRowInfo);
+    } else {
+      _rowInfos.insert(insertedIndex, rowInfo);
+    }
+    _rowInfoByRowId[rowId] = rowInfo;
+  }
+
+  RowInfo? remove(String rowId) {
+    final rowInfo = _rowInfoByRowId[rowId];
+    if (rowInfo != null) {
+      final index = _rowInfos.indexOf(rowInfo);
+      if (index != -1) {
+        _rowInfoByRowId.remove(rowInfo.rowPB.id);
+        _rowInfos.remove(rowInfo);
+      }
+    }
+    return rowInfo;
+  }
+
+  InsertedIndexs insertRows(
+    List<InsertedRowPB> insertedRows,
+    RowInfo Function(RowPB) builder,
+  ) {
+    InsertedIndexs insertIndexs = [];
+    for (final insertRow in insertedRows) {
+      final isContains = contains(insertRow.row.id);
+
+      var index = insertRow.index;
+      if (_rowInfos.length < index) {
+        index = _rowInfos.length;
+      }
+      insert(index, builder(insertRow.row));
+
+      if (!isContains) {
+        insertIndexs.add(InsertedIndex(
+          index: index,
+          rowId: insertRow.row.id,
+        ));
+      }
+    }
+    return insertIndexs;
+  }
+
+  DeletedIndexs removeRows(List<String> rowIds) {
+    final List<RowInfo> newRows = [];
+    final DeletedIndexs deletedIndex = [];
+    final Map<String, String> deletedRowByRowId = {
+      for (var rowId in rowIds) rowId: rowId
+    };
+
+    _rowInfos.asMap().forEach((index, RowInfo rowInfo) {
+      if (deletedRowByRowId[rowInfo.rowPB.id] == null) {
+        newRows.add(rowInfo);
+      } else {
+        _rowInfoByRowId.remove(rowInfo.rowPB.id);
+        deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo));
+      }
+    });
+    _rowInfos = newRows;
+    return deletedIndex;
+  }
+
+  UpdatedIndexMap updateRows(
+    List<RowPB> updatedRows,
+    RowInfo Function(RowPB) builder,
+  ) {
+    final UpdatedIndexMap updatedIndexs = UpdatedIndexMap();
+    for (final RowPB updatedRow in updatedRows) {
+      final rowId = updatedRow.id;
+      final index = _rowInfos.indexWhere(
+        (rowInfo) => rowInfo.rowPB.id == rowId,
+      );
+      if (index != -1) {
+        final rowInfo = builder(updatedRow);
+        insert(index, rowInfo);
+        updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId);
+      }
+    }
+    return updatedIndexs;
+  }
+
+  List<DeletedIndex> markRowsAsInvisible(List<String> rowIds) {
+    final List<DeletedIndex> deletedRows = [];
+
+    for (final rowId in rowIds) {
+      final rowInfo = _rowInfoByRowId[rowId];
+      if (rowInfo != null) {
+        final index = _rowInfos.indexOf(rowInfo);
+        if (index != -1) {
+          deletedRows.add(DeletedIndex(index: index, rowInfo: rowInfo));
+        }
+      }
+    }
+    return deletedRows;
+  }
+
+  bool contains(String rowId) {
+    return _rowInfoByRowId[rowId] != null;
+  }
+}

+ 6 - 6
frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart

@@ -12,14 +12,14 @@ part 'group_bloc.freezed.dart';
 class GridGroupBloc extends Bloc<GridGroupEvent, GridGroupState> {
   final GridFieldController _fieldController;
   final SettingFFIService _settingFFIService;
-  Function(List<GridFieldContext>)? _onFieldsFn;
+  Function(List<FieldInfo>)? _onFieldsFn;
 
   GridGroupBloc({
     required String viewId,
     required GridFieldController fieldController,
   })  : _fieldController = fieldController,
         _settingFFIService = SettingFFIService(viewId: viewId),
-        super(GridGroupState.initial(viewId, fieldController.fieldContexts)) {
+        super(GridGroupState.initial(viewId, fieldController.fieldInfos)) {
     on<GridGroupEvent>(
       (event, emit) async {
         event.when(
@@ -67,19 +67,19 @@ class GridGroupEvent with _$GridGroupEvent {
     String fieldId,
     FieldType fieldType,
   ) = _GroupByField;
-  const factory GridGroupEvent.didReceiveFieldUpdate(
-      List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
+  const factory GridGroupEvent.didReceiveFieldUpdate(List<FieldInfo> fields) =
+      _DidReceiveFieldUpdate;
 }
 
 @freezed
 class GridGroupState with _$GridGroupState {
   const factory GridGroupState({
     required String gridId,
-    required List<GridFieldContext> fieldContexts,
+    required List<FieldInfo> fieldContexts,
   }) = _GridGroupState;
 
   factory GridGroupState.initial(
-          String gridId, List<GridFieldContext> fieldContexts) =>
+          String gridId, List<FieldInfo> fieldContexts) =>
       GridGroupState(
         gridId: gridId,
         fieldContexts: fieldContexts,

+ 5 - 6
frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart

@@ -10,13 +10,12 @@ part 'property_bloc.freezed.dart';
 
 class GridPropertyBloc extends Bloc<GridPropertyEvent, GridPropertyState> {
   final GridFieldController _fieldController;
-  Function(List<GridFieldContext>)? _onFieldsFn;
+  Function(List<FieldInfo>)? _onFieldsFn;
 
   GridPropertyBloc(
       {required String gridId, required GridFieldController fieldController})
       : _fieldController = fieldController,
-        super(
-            GridPropertyState.initial(gridId, fieldController.fieldContexts)) {
+        super(GridPropertyState.initial(gridId, fieldController.fieldInfos)) {
     on<GridPropertyEvent>(
       (event, emit) async {
         await event.map(
@@ -69,7 +68,7 @@ class GridPropertyEvent with _$GridPropertyEvent {
   const factory GridPropertyEvent.setFieldVisibility(
       String fieldId, bool visibility) = _SetFieldVisibility;
   const factory GridPropertyEvent.didReceiveFieldUpdate(
-      List<GridFieldContext> fields) = _DidReceiveFieldUpdate;
+      List<FieldInfo> fields) = _DidReceiveFieldUpdate;
   const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) =
       _MoveField;
 }
@@ -78,12 +77,12 @@ class GridPropertyEvent with _$GridPropertyEvent {
 class GridPropertyState with _$GridPropertyState {
   const factory GridPropertyState({
     required String gridId,
-    required List<GridFieldContext> fieldContexts,
+    required List<FieldInfo> fieldContexts,
   }) = _GridPropertyState;
 
   factory GridPropertyState.initial(
     String gridId,
-    List<GridFieldContext> fieldContexts,
+    List<FieldInfo> fieldContexts,
   ) =>
       GridPropertyState(
         gridId: gridId,

+ 4 - 3
frontend/app_flowy/lib/plugins/grid/application/setting/setting_bloc.dart

@@ -25,7 +25,8 @@ class GridSettingBloc extends Bloc<GridSettingEvent, GridSettingState> {
 
 @freezed
 class GridSettingEvent with _$GridSettingEvent {
-  const factory GridSettingEvent.performAction(GridSettingAction action) = _PerformAction;
+  const factory GridSettingEvent.performAction(GridSettingAction action) =
+      _PerformAction;
 }
 
 @freezed
@@ -40,7 +41,7 @@ class GridSettingState with _$GridSettingState {
 }
 
 enum GridSettingAction {
-  filter,
+  showFilters,
   sortBy,
-  properties,
+  showProperties,
 }

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

@@ -1,7 +1,8 @@
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
 import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart';
-import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
@@ -15,6 +16,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:flutter/material.dart';
 import 'package:linked_scroll_controller/linked_scroll_controller.dart';
 import '../application/row/row_cache.dart';
+import '../application/setting/setting_bloc.dart';
 import 'controller/grid_scroll.dart';
 import 'layout/layout.dart';
 import 'layout/sizes.dart';
@@ -24,17 +26,20 @@ import 'widgets/footer/grid_footer.dart';
 import 'widgets/header/grid_header.dart';
 import 'widgets/row/row_detail.dart';
 import 'widgets/shortcuts.dart';
+import 'widgets/filter/menu.dart';
 import 'widgets/toolbar/grid_toolbar.dart';
 
 class GridPage extends StatefulWidget {
   final ViewPB view;
+  final GridController gridController;
   final VoidCallback? onDeleted;
 
   GridPage({
     required this.view,
     this.onDeleted,
     Key? key,
-  }) : super(key: ValueKey(view.id));
+  })  : gridController = GridController(view: view),
+        super(key: key);
 
   @override
   State<GridPage> createState() => _GridPageState();
@@ -46,8 +51,19 @@ class _GridPageState extends State<GridPage> {
     return MultiBlocProvider(
       providers: [
         BlocProvider<GridBloc>(
-          create: (context) => getIt<GridBloc>(param1: widget.view)
-            ..add(const GridEvent.initial()),
+          create: (context) => GridBloc(
+            view: widget.view,
+            gridController: widget.gridController,
+          )..add(const GridEvent.initial()),
+        ),
+        BlocProvider<GridFilterMenuBloc>(
+          create: (context) => GridFilterMenuBloc(
+            viewId: widget.view.id,
+            fieldController: widget.gridController.fieldController,
+          )..add(const GridFilterMenuEvent.initial()),
+        ),
+        BlocProvider<GridSettingBloc>(
+          create: (context) => GridSettingBloc(gridId: widget.view.id),
         ),
       ],
       child: BlocBuilder<GridBloc, GridState>(
@@ -122,7 +138,8 @@ class _FlowyGridState extends State<FlowyGrid> {
         return Column(
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
-            const _GridToolbarAdaptor(),
+            const GridToolbar(),
+            const GridFilterMenu(),
             _gridHeader(context, state.gridId),
             Flexible(child: child),
             const RowCountBadge(),
@@ -166,7 +183,7 @@ class _FlowyGridState extends State<FlowyGrid> {
 
   Widget _gridHeader(BuildContext context, String gridId) {
     final fieldController =
-        context.read<GridBloc>().dataController.fieldController;
+        context.read<GridBloc>().gridController.fieldController;
     return GridHeaderSliverAdaptor(
       gridId: gridId,
       fieldController: fieldController,
@@ -175,27 +192,6 @@ class _FlowyGridState extends State<FlowyGrid> {
   }
 }
 
-class _GridToolbarAdaptor extends StatelessWidget {
-  const _GridToolbarAdaptor({Key? key}) : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocSelector<GridBloc, GridState, GridToolbarContext>(
-      selector: (state) {
-        final fieldController =
-            context.read<GridBloc>().dataController.fieldController;
-        return GridToolbarContext(
-          gridId: state.gridId,
-          fieldController: fieldController,
-        );
-      },
-      builder: (context, toolbarContext) {
-        return GridToolbar(toolbarContext: toolbarContext);
-      },
-    );
-  }
-}
-
 class _GridRows extends StatefulWidget {
   const _GridRows({Key? key}) : super(key: key);
 
@@ -222,7 +218,7 @@ class _GridRowsState extends State<_GridRows> {
               _key.currentState?.removeItem(
                 item.index,
                 (context, animation) =>
-                    _renderRow(context, item.row, animation),
+                    _renderRow(context, item.rowInfo, animation),
               );
             }
           },
@@ -235,9 +231,13 @@ class _GridRowsState extends State<_GridRows> {
           initialItemCount: context.read<GridBloc>().state.rowInfos.length,
           itemBuilder:
               (BuildContext context, int index, Animation<double> animation) {
-            final RowInfo rowInfo =
-                context.read<GridBloc>().state.rowInfos[index];
-            return _renderRow(context, rowInfo, animation);
+            final rowInfos = context.read<GridBloc>().state.rowInfos;
+            if (index >= rowInfos.length) {
+              return const SizedBox();
+            } else {
+              final RowInfo rowInfo = rowInfos[index];
+              return _renderRow(context, rowInfo, animation);
+            }
           },
         );
       },
@@ -258,7 +258,7 @@ class _GridRowsState extends State<_GridRows> {
     if (rowCache == null) return const SizedBox();
 
     final fieldController =
-        context.read<GridBloc>().dataController.fieldController;
+        context.read<GridBloc>().gridController.fieldController;
     final dataController = GridRowDataController(
       rowInfo: rowInfo,
       fieldController: fieldController,

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart

@@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
 import 'sizes.dart';
 
 class GridLayout {
-  static double headerWidth(List<GridFieldContext> fields) {
+  static double headerWidth(List<FieldInfo> fields) {
     if (fields.isEmpty) return 0;
 
     final fieldsWidth = fields

+ 1 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart

@@ -61,7 +61,6 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
                 SliverToBoxAdapter(
                   child: _TextField(popoverMutex: popoverMutex),
                 ),
-                const SliverToBoxAdapter(child: VSpace(6)),
                 const SliverToBoxAdapter(child: TypeOptionSeparator()),
                 const SliverToBoxAdapter(child: VSpace(6)),
                 const SliverToBoxAdapter(child: _Title()),
@@ -145,7 +144,7 @@ class _TextField extends StatelessWidget {
             value: (option) => option);
 
         return SizedBox(
-          height: 62,
+          height: 52,
           child: SelectOptionTextField(
             options: state.options,
             selectedOptionMap: optionMap,

+ 12 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart

@@ -49,6 +49,7 @@ class SelectOptionTextField extends StatefulWidget {
 class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
   late FocusNode focusNode;
   late TextEditingController controller;
+  var textLength = 0;
 
   @override
   void initState() {
@@ -61,6 +62,14 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
     super.initState();
   }
 
+  String? _suffixText() {
+    if (widget.maxLength != null) {
+      return '${textLength.toString()}/${widget.maxLength.toString()}';
+    } else {
+      return null;
+    }
+  }
+
   @override
   Widget build(BuildContext context) {
     return TextFieldTags(
@@ -83,6 +92,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
             focusNode: focusNode,
             onTap: widget.onClick,
             onChanged: (text) {
+              textLength = text.length;
               if (onChanged != null) {
                 onChanged(text);
               }
@@ -114,6 +124,8 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
               isDense: true,
               prefixIcon: _renderTags(context, sc),
               hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
+              suffixText: _suffixText(),
+              counterText: "",
               prefixIconConstraints:
                   BoxConstraints(maxWidth: widget.distanceToText),
               focusedBorder: OutlineInputBorder(

+ 15 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/checkbox.dart

@@ -0,0 +1,15 @@
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
+import 'package:flutter/material.dart';
+
+import 'choicechip.dart';
+
+class CheckboxFilterChoicechip extends StatelessWidget {
+  final FilterInfo filterInfo;
+  const CheckboxFilterChoicechip({required this.filterInfo, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ChoiceChipButton(filterInfo: filterInfo);
+  }
+}

+ 55 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/choicechip.dart

@@ -0,0 +1,55 @@
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
+import 'package:flowy_infra/color_extension.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+import 'dart:math' as math;
+
+class ChoiceChipButton extends StatelessWidget {
+  final FilterInfo filterInfo;
+  final VoidCallback? onTap;
+
+  const ChoiceChipButton({
+    Key? key,
+    required this.filterInfo,
+    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,
+    );
+
+    final decoration = BoxDecoration(
+      color: Colors.transparent,
+      border: Border.fromBorderSide(borderSide),
+      borderRadius: const BorderRadius.all(Radius.circular(14)),
+    );
+
+    return SizedBox(
+      height: 28,
+      child: FlowyButton(
+        decoration: decoration,
+        useIntrinsicWidth: true,
+        text: FlowyText(filterInfo.field.name, fontSize: 12),
+        margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+        radius: const BorderRadius.all(Radius.circular(14)),
+        leftIcon: svgWidget(
+          filterInfo.field.fieldType.iconName(),
+          color: Theme.of(context).colorScheme.onSurface,
+        ),
+        rightIcon: arrow,
+        hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        onTap: onTap,
+      ),
+    );
+  }
+}

+ 15 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/date.dart

@@ -0,0 +1,15 @@
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
+import 'package:flutter/material.dart';
+
+import 'choicechip.dart';
+
+class DateFilterChoicechip extends StatelessWidget {
+  final FilterInfo filterInfo;
+  const DateFilterChoicechip({required this.filterInfo, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ChoiceChipButton(filterInfo: filterInfo);
+  }
+}

+ 15 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/number.dart

@@ -0,0 +1,15 @@
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
+import 'package:flutter/material.dart';
+
+import 'choicechip.dart';
+
+class NumberFilterChoicechip extends StatelessWidget {
+  final FilterInfo filterInfo;
+  const NumberFilterChoicechip({required this.filterInfo, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ChoiceChipButton(filterInfo: filterInfo);
+  }
+}

+ 15 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option.dart

@@ -0,0 +1,15 @@
+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);
+  }
+}

+ 212 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/text.dart

@@ -0,0 +1,212 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/grid/application/filter/text_filter_editor_bloc.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/condition_button.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:app_flowy/plugins/grid/presentation/widgets/filter/text_field.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_infra_ui/flowy_infra_ui.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/text_filter.pb.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'choicechip.dart';
+
+class TextFilterChoicechip extends StatefulWidget {
+  final FilterInfo filterInfo;
+  const TextFilterChoicechip({required this.filterInfo, Key? key})
+      : super(key: key);
+
+  @override
+  State<TextFilterChoicechip> createState() => _TextFilterChoicechipState();
+}
+
+class _TextFilterChoicechipState extends State<TextFilterChoicechip> {
+  @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: () {},
+      ),
+    );
+  }
+}
+
+class TextFilterEditor extends StatefulWidget {
+  final FilterInfo filterInfo;
+  const TextFilterEditor({required this.filterInfo, Key? key})
+      : super(key: key);
+
+  @override
+  State<TextFilterEditor> createState() => _TextFilterEditorState();
+}
+
+class _TextFilterEditorState extends State<TextFilterEditor> {
+  final popoverMutex = PopoverMutex();
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => TextFilterEditorBloc(filterInfo: widget.filterInfo)
+        ..add(const TextFilterEditorEvent.initial()),
+      child: BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
+        builder: (context, state) {
+          return Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
+            child: Column(
+              children: [
+                _buildFilterPannel(context, state),
+                const VSpace(4),
+                _buildFilterTextField(context, state),
+              ],
+            ),
+          );
+        },
+      ),
+    );
+  }
+
+  Widget _buildFilterPannel(BuildContext context, TextFilterEditorState state) {
+    return SizedBox(
+      height: 20,
+      child: Row(
+        children: [
+          FlowyText(state.filterInfo.field.name),
+          const HSpace(4),
+          TextFilterConditionList(
+            filterInfo: state.filterInfo,
+            popoverMutex: popoverMutex,
+            onCondition: (condition) {
+              context
+                  .read<TextFilterEditorBloc>()
+                  .add(TextFilterEditorEvent.updateCondition(condition));
+            },
+          ),
+          const Spacer(),
+          DisclosureButton(
+            popoverMutex: popoverMutex,
+            onAction: (action) {
+              switch (action) {
+                case FilterDisclosureAction.delete:
+                  context
+                      .read<TextFilterEditorBloc>()
+                      .add(const TextFilterEditorEvent.delete());
+                  break;
+              }
+            },
+          ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildFilterTextField(
+      BuildContext context, TextFilterEditorState state) {
+    final textFilter = state.filterInfo.textFilter()!;
+    return FilterTextField(
+      text: textFilter.content,
+      hintText: LocaleKeys.grid_settings_typeAValue.tr(),
+      autoFucous: false,
+      onSubmitted: (text) {
+        context
+            .read<TextFilterEditorBloc>()
+            .add(TextFilterEditorEvent.updateContent(text));
+      },
+    );
+  }
+}
+
+class TextFilterConditionList extends StatelessWidget {
+  final FilterInfo filterInfo;
+  final PopoverMutex popoverMutex;
+  final Function(TextFilterCondition) onCondition;
+  const TextFilterConditionList({
+    required this.filterInfo,
+    required this.popoverMutex,
+    required this.onCondition,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final textFilter = filterInfo.textFilter()!;
+    return PopoverActionList<ConditionWrapper>(
+      asBarrier: true,
+      mutex: popoverMutex,
+      direction: PopoverDirection.bottomWithCenterAligned,
+      actions: TextFilterCondition.values
+          .map(
+            (action) => ConditionWrapper(
+              action,
+              textFilter.condition == action,
+            ),
+          )
+          .toList(),
+      buildChild: (controller) {
+        return ConditionButton(
+          conditionName: textFilter.condition.filterName,
+          onTap: () => controller.show(),
+        );
+      },
+      onSelected: (action, controller) async {
+        onCondition(action.inner);
+        controller.close();
+      },
+    );
+  }
+}
+
+class ConditionWrapper extends ActionCell {
+  final TextFilterCondition inner;
+  final bool isSelected;
+
+  ConditionWrapper(this.inner, this.isSelected);
+
+  @override
+  Widget? rightIcon(Color iconColor) {
+    if (isSelected) {
+      return svgWidget("grid/checkmark");
+    } else {
+      return null;
+    }
+  }
+
+  @override
+  String get name => inner.filterName;
+}
+
+extension TextFilterConditionExtension on TextFilterCondition {
+  String get filterName {
+    switch (this) {
+      case TextFilterCondition.Contains:
+        return LocaleKeys.grid_textFilter_contains.tr();
+      case TextFilterCondition.DoesNotContain:
+        return LocaleKeys.grid_textFilter_doesNotContain.tr();
+      case TextFilterCondition.EndsWith:
+        return LocaleKeys.grid_textFilter_endsWith.tr();
+      case TextFilterCondition.Is:
+        return LocaleKeys.grid_textFilter_is.tr();
+      case TextFilterCondition.IsNot:
+        return LocaleKeys.grid_textFilter_isNot.tr();
+      case TextFilterCondition.StartsWith:
+        return LocaleKeys.grid_textFilter_startWith.tr();
+      case TextFilterCondition.TextIsEmpty:
+        return LocaleKeys.grid_textFilter_isEmpty.tr();
+      case TextFilterCondition.TextIsNotEmpty:
+        return LocaleKeys.grid_textFilter_isNotEmpty.tr();
+      default:
+        return "";
+    }
+  }
+}

+ 14 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/url.dart

@@ -0,0 +1,14 @@
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
+import 'package:flutter/material.dart';
+import 'choicechip.dart';
+
+class URLFilterChoicechip extends StatelessWidget {
+  final FilterInfo filterInfo;
+  const URLFilterChoicechip({required this.filterInfo, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return ChoiceChipButton(filterInfo: filterInfo);
+  }
+}

+ 37 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/condition_button.dart

@@ -0,0 +1,37 @@
+import 'dart:math' as math;
+import 'package:flowy_infra/color_extension.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flutter/material.dart';
+
+class ConditionButton extends StatelessWidget {
+  final String conditionName;
+  final VoidCallback onTap;
+  const ConditionButton({
+    required this.conditionName,
+    required this.onTap,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final arrow = Transform.rotate(
+      angle: -math.pi / 2,
+      child: svgWidget("home/arrow_left"),
+    );
+
+    return SizedBox(
+      height: 20,
+      child: FlowyButton(
+        useIntrinsicWidth: true,
+        text: FlowyText(conditionName, fontSize: 10),
+        margin: const EdgeInsets.symmetric(horizontal: 4),
+        radius: const BorderRadius.all(Radius.circular(2)),
+        rightIcon: arrow,
+        hoverColor: AFThemeExtension.of(context).lightGreyHover,
+        onTap: onTap,
+      ),
+    );
+  }
+}

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

@@ -0,0 +1,165 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_create_bloc.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/filter/text_field.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/style_widget/button.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:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class GridCreateFilterList extends StatefulWidget {
+  final String viewId;
+  final GridFieldController fieldController;
+  final VoidCallback onClosed;
+
+  const GridCreateFilterList({
+    required this.viewId,
+    required this.fieldController,
+    required this.onClosed,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _GridCreateFilterListState();
+}
+
+class _GridCreateFilterListState extends State<GridCreateFilterList> {
+  late GridCreateFilterBloc editBloc;
+
+  @override
+  void initState() {
+    editBloc = GridCreateFilterBloc(
+      viewId: widget.viewId,
+      fieldController: widget.fieldController,
+    )..add(const GridCreateFilterEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: editBloc,
+      child: BlocListener<GridCreateFilterBloc, GridCreateFilterState>(
+        listener: (context, state) {
+          if (state.didCreateFilter) {
+            widget.onClosed();
+          }
+        },
+        child: BlocBuilder<GridCreateFilterBloc, GridCreateFilterState>(
+          builder: (context, state) {
+            final cells = state.creatableFields.map((fieldInfo) {
+              return SizedBox(
+                height: GridSize.typeOptionItemHeight,
+                child: _FilterPropertyCell(
+                  fieldInfo: fieldInfo,
+                  onTap: (fieldInfo) => createFilter(fieldInfo),
+                ),
+              );
+            }).toList();
+
+            List<Widget> slivers = [
+              SliverPersistentHeader(
+                pinned: true,
+                delegate: _FilterTextFieldDelegate(),
+              ),
+              SliverToBoxAdapter(
+                child: ListView.separated(
+                  controller: ScrollController(),
+                  shrinkWrap: true,
+                  itemCount: cells.length,
+                  itemBuilder: (BuildContext context, int index) {
+                    return cells[index];
+                  },
+                  separatorBuilder: (BuildContext context, int index) {
+                    return VSpace(GridSize.typeOptionSeparatorHeight);
+                  },
+                ),
+              ),
+            ];
+            return CustomScrollView(
+              shrinkWrap: true,
+              slivers: slivers,
+              controller: ScrollController(),
+              physics: StyledScrollPhysics(),
+            );
+          },
+        ),
+      ),
+    );
+  }
+
+  @override
+  Future<void> dispose() async {
+    editBloc.close();
+    super.dispose();
+  }
+
+  void createFilter(FieldInfo field) {
+    editBloc.add(GridCreateFilterEvent.createDefaultFilter(field));
+  }
+}
+
+class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate {
+  _FilterTextFieldDelegate();
+
+  double fixHeight = 46;
+
+  @override
+  Widget build(
+      BuildContext context, double shrinkOffset, bool overlapsContent) {
+    return Padding(
+      padding: const EdgeInsets.only(top: 4),
+      child: Container(
+        color: Theme.of(context).colorScheme.background,
+        height: fixHeight,
+        child: FilterTextField(
+          hintText: LocaleKeys.grid_settings_filterBy.tr(),
+          onChanged: (text) {
+            context
+                .read<GridCreateFilterBloc>()
+                .add(GridCreateFilterEvent.didReceiveFilterText(text));
+          },
+        ),
+      ),
+    );
+  }
+
+  @override
+  double get maxExtent => fixHeight;
+
+  @override
+  double get minExtent => fixHeight;
+
+  @override
+  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
+    return false;
+  }
+}
+
+class _FilterPropertyCell extends StatelessWidget {
+  final FieldInfo fieldInfo;
+  final Function(FieldInfo) onTap;
+  const _FilterPropertyCell({
+    required this.fieldInfo,
+    required this.onTap,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return FlowyButton(
+      text: FlowyText.medium(fieldInfo.name, fontSize: 12),
+      onTap: () => onTap(fieldInfo),
+      leftIcon: svgWidget(
+        fieldInfo.fieldType.iconName(),
+        color: Theme.of(context).colorScheme.onSurface,
+      ),
+    );
+  }
+}

+ 73 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/disclosure_button.dart

@@ -0,0 +1,73 @@
+import 'package:app_flowy/generated/locale_keys.g.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_infra_ui/style_widget/icon_button.dart';
+import 'package:flutter/material.dart';
+
+class DisclosureButton extends StatefulWidget {
+  final PopoverMutex popoverMutex;
+  final Function(FilterDisclosureAction) onAction;
+  const DisclosureButton({
+    required this.popoverMutex,
+    required this.onAction,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<DisclosureButton> createState() => _DisclosureButtonState();
+}
+
+class _DisclosureButtonState extends State<DisclosureButton> {
+  @override
+  Widget build(BuildContext context) {
+    return PopoverActionList<FilterDisclosureActionWrapper>(
+      asBarrier: true,
+      mutex: widget.popoverMutex,
+      direction: PopoverDirection.rightWithTopAligned,
+      actions: FilterDisclosureAction.values
+          .map((action) => FilterDisclosureActionWrapper(action))
+          .toList(),
+      buildChild: (controller) {
+        return FlowyIconButton(
+          width: 20,
+          icon: svgWidget(
+            "editor/details",
+            color: Theme.of(context).colorScheme.onSurface,
+          ),
+          onPressed: () => controller.show(),
+        );
+      },
+      onSelected: (action, controller) async {
+        widget.onAction(action.inner);
+        controller.close();
+      },
+    );
+  }
+}
+
+enum FilterDisclosureAction {
+  delete,
+}
+
+class FilterDisclosureActionWrapper extends ActionCell {
+  final FilterDisclosureAction inner;
+
+  FilterDisclosureActionWrapper(this.inner);
+
+  @override
+  Widget? leftIcon(Color iconColor) => null;
+
+  @override
+  String get name => inner.name;
+}
+
+extension FilterDisclosureActionExtension on FilterDisclosureAction {
+  String get name {
+    switch (this) {
+      case FilterDisclosureAction.delete:
+        return LocaleKeys.grid_settings_deleteFilter.tr();
+    }
+  }
+}

+ 35 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_info.dart

@@ -0,0 +1,35 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.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/text_filter.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
+
+class FilterInfo {
+  final String viewId;
+  final FilterPB filter;
+  final FieldInfo field;
+
+  FilterInfo(this.viewId, this.filter, this.field);
+
+  FilterInfo copyWith({FilterPB? filter, FieldInfo? field}) {
+    return FilterInfo(
+      viewId,
+      filter ?? this.filter,
+      field ?? this.field,
+    );
+  }
+
+  DateFilterPB? dateFilter() {
+    if (filter.fieldType != FieldType.DateTime) {
+      return null;
+    }
+    return DateFilterPB.fromBuffer(filter.data);
+  }
+
+  TextFilterPB? textFilter() {
+    if (filter.fieldType != FieldType.RichText) {
+      return null;
+    }
+    return TextFilterPB.fromBuffer(filter.data);
+  }
+}

+ 138 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart

@@ -0,0 +1,138 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/color_extension.dart';
+import 'package:flowy_infra/image.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+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 {
+  const GridFilterMenu({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
+      builder: (context, state) {
+        if (state.isVisible) {
+          return _wrapPadding(Column(
+            children: [
+              buildDivider(context),
+              const VSpace(6),
+              buildFilterItems(state.viewId, state.filters),
+            ],
+          ));
+        } else {
+          return const SizedBox();
+        }
+      },
+    );
+  }
+
+  Widget _wrapPadding(Widget child) {
+    return Padding(
+      padding: EdgeInsets.symmetric(
+        horizontal: GridSize.leadingHeaderPadding,
+        vertical: 6,
+      ),
+      child: child,
+    );
+  }
+
+  Widget buildDivider(BuildContext context) {
+    return Divider(
+      height: 1.0,
+      color: AFThemeExtension.of(context).toggleOffFill,
+    );
+  }
+
+  Widget buildFilterItems(String viewId, List<FilterInfo> filters) {
+    final List<Widget> children = filters
+        .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
+        .toList();
+    return Row(
+      children: [
+        SingleChildScrollView(
+          controller: ScrollController(),
+          scrollDirection: Axis.horizontal,
+          child: Wrap(
+            spacing: 4,
+            children: children,
+          ),
+        ),
+        const HSpace(4),
+        AddFilterButton(viewId: viewId),
+      ],
+    );
+  }
+}
+
+class AddFilterButton extends StatefulWidget {
+  final String viewId;
+  const AddFilterButton({required this.viewId, Key? key}) : super(key: key);
+
+  @override
+  State<AddFilterButton> createState() => _AddFilterButtonState();
+}
+
+class _AddFilterButtonState extends State<AddFilterButton> {
+  late PopoverController popoverController;
+
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return wrapPopover(
+      context,
+      SizedBox(
+        height: 28,
+        child: FlowyButton(
+          text: FlowyText(
+            LocaleKeys.grid_settings_addFilter.tr(),
+            fontSize: 12,
+          ),
+          useIntrinsicWidth: true,
+          hoverColor: AFThemeExtension.of(context).lightGreyHover,
+          leftIcon: svgWidget(
+            "home/add",
+            color: Theme.of(context).colorScheme.onSurface,
+          ),
+          onTap: () {
+            popoverController.show();
+          },
+        ),
+      ),
+    );
+  }
+
+  Widget wrapPopover(BuildContext buildContext, Widget child) {
+    return AppFlowyPopover(
+      controller: popoverController,
+      constraints: BoxConstraints.loose(const Size(200, 300)),
+      margin: const EdgeInsets.all(6),
+      triggerActions: PopoverTriggerFlags.none,
+      child: child,
+      popupBuilder: (BuildContext context) {
+        final bloc = buildContext.read<GridFilterMenuBloc>();
+        return GridCreateFilterList(
+          viewId: widget.viewId,
+          fieldController: bloc.fieldController,
+          onClosed: () => popoverController.close(),
+        );
+      },
+    );
+  }
+}

+ 41 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu_item.dart

@@ -0,0 +1,41 @@
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pbenum.dart';
+import 'package:flutter/material.dart';
+
+import 'choicechip/checkbox.dart';
+import 'choicechip/date.dart';
+import 'choicechip/number.dart';
+import 'choicechip/select_option.dart';
+import 'choicechip/text.dart';
+import 'choicechip/url.dart';
+import 'filter_info.dart';
+
+class FilterMenuItem extends StatelessWidget {
+  final FilterInfo filterInfo;
+  const FilterMenuItem({required this.filterInfo, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return buildFilterChoicechip(filterInfo);
+  }
+}
+
+Widget buildFilterChoicechip(FilterInfo filterInfo) {
+  switch (filterInfo.field.fieldType) {
+    case FieldType.Checkbox:
+      return CheckboxFilterChoicechip(filterInfo: filterInfo);
+    case FieldType.DateTime:
+      return DateFilterChoicechip(filterInfo: filterInfo);
+    case FieldType.MultiSelect:
+      return SelectOptionFilterChoicechip(filterInfo: filterInfo);
+    case FieldType.Number:
+      return NumberFilterChoicechip(filterInfo: filterInfo);
+    case FieldType.RichText:
+      return TextFilterChoicechip(filterInfo: filterInfo);
+    case FieldType.SingleSelect:
+      return SelectOptionFilterChoicechip(filterInfo: filterInfo);
+    case FieldType.URL:
+      return URLFilterChoicechip(filterInfo: filterInfo);
+    default:
+      return const SizedBox();
+  }
+}

+ 76 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/text_field.dart

@@ -0,0 +1,76 @@
+import 'package:flowy_infra/size.dart';
+import 'package:flowy_infra/text_style.dart';
+import 'package:flutter/material.dart';
+import 'package:textstyle_extensions/textstyle_extensions.dart';
+
+class FilterTextField extends StatefulWidget {
+  final String hintText;
+  final String text;
+  final void Function(String)? onChanged;
+  final void Function(String)? onSubmitted;
+  final bool autoFucous;
+  const FilterTextField({
+    this.hintText = "",
+    this.text = "",
+    this.onChanged,
+    this.onSubmitted,
+    this.autoFucous = true,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<FilterTextField> createState() => FilterTextFieldState();
+}
+
+class FilterTextFieldState extends State<FilterTextField> {
+  late FocusNode focusNode;
+  late TextEditingController controller;
+
+  @override
+  void initState() {
+    focusNode = FocusNode();
+    controller = TextEditingController();
+    controller.text = widget.text;
+    if (widget.autoFucous) {
+      WidgetsBinding.instance.addPostFrameCallback((_) {
+        focusNode.requestFocus();
+      });
+    }
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return TextField(
+      controller: controller,
+      focusNode: focusNode,
+      onChanged: (text) {
+        widget.onChanged?.call(text);
+      },
+      onSubmitted: (text) {
+        widget.onSubmitted?.call(text);
+      },
+      maxLines: 1,
+      style: TextStyles.body1.size(FontSizes.s12),
+      decoration: InputDecoration(
+        contentPadding: const EdgeInsets.all(10),
+        enabledBorder: OutlineInputBorder(
+          borderSide: BorderSide(
+            color: Theme.of(context).colorScheme.primary,
+            width: 1.0,
+          ),
+          borderRadius: Corners.s10Border,
+        ),
+        isDense: true,
+        hintText: widget.hintText,
+        focusedBorder: OutlineInputBorder(
+          borderSide: BorderSide(
+            color: Theme.of(context).colorScheme.primary,
+            width: 1.0,
+          ),
+          borderRadius: Corners.s8Border,
+        ),
+      ),
+    );
+  }
+}

+ 2 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart

@@ -45,7 +45,7 @@ class _GridFieldCellState extends State<GridFieldCell> {
         builder: (context, state) {
           final button = AppFlowyPopover(
             triggerActions: PopoverTriggerFlags.none,
-            constraints: BoxConstraints.loose(const Size(240, 840)),
+            constraints: BoxConstraints.loose(const Size(240, 440)),
             direction: PopoverDirection.bottomWithLeftAligned,
             controller: popoverController,
             popupBuilder: (BuildContext context) {
@@ -172,6 +172,7 @@ class FieldCellButton extends StatelessWidget {
         field.fieldType.iconName(),
         color: Theme.of(context).colorScheme.onSurface,
       ),
+      radius: BorderRadius.zero,
       text: FlowyText.medium(
         text,
         maxLines: maxLines,

+ 12 - 12
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart

@@ -88,9 +88,9 @@ class _EditFieldButton extends StatelessWidget {
 }
 
 class _FieldOperationList extends StatelessWidget {
-  final GridFieldCellContext fieldContext;
+  final GridFieldCellContext fieldInfo;
   final VoidCallback onDismissed;
-  const _FieldOperationList(this.fieldContext, this.onDismissed, {Key? key})
+  const _FieldOperationList(this.fieldInfo, this.onDismissed, {Key? key})
       : super(key: key);
 
   @override
@@ -113,14 +113,14 @@ class _FieldOperationList extends StatelessWidget {
         bool enable = true;
         switch (action) {
           case FieldAction.delete:
-            enable = !fieldContext.field.isPrimary;
+            enable = !fieldInfo.field.isPrimary;
             break;
           default:
             break;
         }
 
         return FieldActionCell(
-          fieldContext: fieldContext,
+          fieldInfo: fieldInfo,
           action: action,
           onTap: onDismissed,
           enable: enable,
@@ -131,13 +131,13 @@ class _FieldOperationList extends StatelessWidget {
 }
 
 class FieldActionCell extends StatelessWidget {
-  final GridFieldCellContext fieldContext;
+  final GridFieldCellContext fieldInfo;
   final VoidCallback onTap;
   final FieldAction action;
   final bool enable;
 
   const FieldActionCell({
-    required this.fieldContext,
+    required this.fieldInfo,
     required this.action,
     required this.onTap,
     required this.enable,
@@ -153,7 +153,7 @@ class FieldActionCell extends StatelessWidget {
       ),
       onTap: () {
         if (enable) {
-          action.run(context, fieldContext);
+          action.run(context, fieldInfo);
           onTap();
         }
       },
@@ -196,7 +196,7 @@ extension _FieldActionExtension on FieldAction {
     }
   }
 
-  void run(BuildContext context, GridFieldCellContext fieldContext) {
+  void run(BuildContext context, GridFieldCellContext fieldInfo) {
     switch (this) {
       case FieldAction.hide:
         context
@@ -207,8 +207,8 @@ extension _FieldActionExtension on FieldAction {
         PopoverContainer.of(context).close();
 
         FieldService(
-          gridId: fieldContext.gridId,
-          fieldId: fieldContext.field.id,
+          gridId: fieldInfo.gridId,
+          fieldId: fieldInfo.field.id,
         ).duplicateField();
 
         break;
@@ -219,8 +219,8 @@ extension _FieldActionExtension on FieldAction {
           title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(),
           confirm: () {
             FieldService(
-              gridId: fieldContext.gridId,
-              fieldId: fieldContext.field.id,
+              gridId: fieldInfo.gridId,
+              fieldId: fieldInfo.field.id,
             ).deleteField();
           },
         ).show(context);

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart

@@ -115,7 +115,7 @@ class _FieldTypeOptionCell extends StatelessWidget {
       builder: (context, state) {
         return state.field.fold(
           () => const SizedBox(),
-          (fieldContext) {
+          (fieldInfo) {
             final dataController =
                 context.read<FieldEditorBloc>().dataController;
             return FieldTypeOptionEditor(

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart

@@ -87,7 +87,7 @@ class _SwitchFieldButton extends StatelessWidget {
     final widget = AppFlowyPopover(
       constraints: BoxConstraints.loose(const Size(460, 540)),
       asBarrier: true,
-      triggerActions: PopoverTriggerFlags.click | PopoverTriggerFlags.hover,
+      triggerActions: PopoverTriggerFlags.click,
       mutex: popoverMutex,
       offset: const Offset(20, 0),
       popupBuilder: (popOverContext) {

+ 5 - 6
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart

@@ -11,7 +11,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart';
 import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart';
-import 'package:protobuf/protobuf.dart';
+import 'package:protobuf/protobuf.dart' hide FieldInfo;
 import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
 import 'package:flutter/material.dart';
 import 'checkbox.dart';
@@ -130,18 +130,17 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({
 
 TypeOptionContext<T> makeTypeOptionContext<T extends GeneratedMessage>({
   required String gridId,
-  required GridFieldContext fieldContext,
+  required FieldInfo fieldInfo,
 }) {
-  final loader =
-      FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field);
+  final loader = FieldTypeOptionLoader(gridId: gridId, field: fieldInfo.field);
   final dataController = TypeOptionDataController(
     gridId: gridId,
     loader: loader,
-    fieldContext: fieldContext,
+    fieldInfo: fieldInfo,
   );
   return makeTypeOptionContextWithDataController(
     gridId: gridId,
-    fieldType: fieldContext.fieldType,
+    fieldType: fieldInfo.fieldType,
     dataController: dataController,
   );
 }

+ 2 - 2
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart

@@ -224,13 +224,13 @@ class RowContent extends StatelessWidget {
         final GridCellWidget child = builder.build(cellId);
 
         return CellContainer(
-          width: cellId.fieldContext.width.toDouble(),
+          width: cellId.fieldInfo.width.toDouble(),
           rowStateNotifier:
               Provider.of<RegionStateNotifier>(context, listen: false),
           accessoryBuilder: (buildContext) {
             final builder = child.accessoryBuilder;
             List<GridCellAccessoryBuilder> accessories = [];
-            if (cellId.fieldContext.isPrimary) {
+            if (cellId.fieldInfo.isPrimary) {
               accessories.add(
                 GridCellAccessoryBuilder(
                   builder: (key) => PrimaryCellAccessory(

+ 4 - 4
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart

@@ -281,7 +281,7 @@ class _RowDetailCellState extends State<_RowDetailCell> {
                 width: 150,
                 child: FieldCellButton(
                   maxLines: null,
-                  field: widget.cellId.fieldContext.field,
+                  field: widget.cellId.fieldInfo.field,
                   onTap: () => popover.show(),
                 ),
               ),
@@ -297,11 +297,11 @@ class _RowDetailCellState extends State<_RowDetailCell> {
   Widget buildFieldEditor() {
     return FieldEditor(
       gridId: widget.cellId.gridId,
-      fieldName: widget.cellId.fieldContext.field.name,
-      isGroupField: widget.cellId.fieldContext.isGroupField,
+      fieldName: widget.cellId.fieldInfo.field.name,
+      isGroupField: widget.cellId.fieldInfo.isGroupField,
       typeOptionLoader: FieldTypeOptionLoader(
         gridId: widget.cellId.gridId,
-        field: widget.cellId.fieldContext.field,
+        field: widget.cellId.fieldInfo.field,
       ),
       onDeleted: (fieldId) {
         popover.close();

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

@@ -0,0 +1,76 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/color_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import '../filter/create_filter_list.dart';
+
+class FilterButton extends StatefulWidget {
+  const FilterButton({Key? key}) : super(key: key);
+
+  @override
+  State<FilterButton> createState() => _FilterButtonState();
+}
+
+class _FilterButtonState extends State<FilterButton> {
+  final _popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GridFilterMenuBloc, GridFilterMenuState>(
+      builder: (context, state) {
+        final textColor = state.filters.isEmpty
+            ? null
+            : Theme.of(context).colorScheme.primary;
+
+        return wrapPopover(
+          context,
+          SizedBox(
+            height: 26,
+            child: FlowyTextButton(
+              LocaleKeys.grid_settings_filter.tr(),
+              fontSize: 14,
+              fontColor: textColor,
+              fillColor: Colors.transparent,
+              hoverColor: AFThemeExtension.of(context).lightGreyHover,
+              padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
+              onPressed: () {
+                final bloc = context.read<GridFilterMenuBloc>();
+                if (bloc.state.filters.isEmpty) {
+                  _popoverController.show();
+                } else {
+                  bloc.add(const GridFilterMenuEvent.toggleMenu());
+                }
+              },
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  Widget wrapPopover(BuildContext buildContext, Widget child) {
+    return AppFlowyPopover(
+      controller: _popoverController,
+      direction: PopoverDirection.leftWithTopAligned,
+      constraints: BoxConstraints.loose(const Size(200, 300)),
+      offset: const Offset(0, 10),
+      margin: const EdgeInsets.all(6),
+      triggerActions: PopoverTriggerFlags.none,
+      child: child,
+      popupBuilder: (BuildContext context) {
+        final bloc = buildContext.read<GridFilterMenuBloc>();
+        return GridCreateFilterList(
+          viewId: bloc.viewId,
+          fieldController: bloc.fieldController,
+          onClosed: () => _popoverController.close(),
+        );
+      },
+    );
+  }
+}

+ 11 - 11
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart

@@ -30,14 +30,14 @@ class GridGroupList extends StatelessWidget {
       )..add(const GridGroupEvent.initial()),
       child: BlocBuilder<GridGroupBloc, GridGroupState>(
         builder: (context, state) {
-          final cells = state.fieldContexts.map((fieldContext) {
+          final cells = state.fieldContexts.map((fieldInfo) {
             Widget cell = _GridGroupCell(
-              fieldContext: fieldContext,
+              fieldInfo: fieldInfo,
               onSelected: () => onDismissed(),
-              key: ValueKey(fieldContext.id),
+              key: ValueKey(fieldInfo.id),
             );
 
-            if (!fieldContext.canGroup) {
+            if (!fieldInfo.canGroup) {
               cell = IgnorePointer(child: Opacity(opacity: 0.3, child: cell));
             }
             return cell;
@@ -61,9 +61,9 @@ class GridGroupList extends StatelessWidget {
 
 class _GridGroupCell extends StatelessWidget {
   final VoidCallback onSelected;
-  final GridFieldContext fieldContext;
+  final FieldInfo fieldInfo;
   const _GridGroupCell({
-    required this.fieldContext,
+    required this.fieldInfo,
     required this.onSelected,
     Key? key,
   }) : super(key: key);
@@ -71,7 +71,7 @@ class _GridGroupCell extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     Widget? rightIcon;
-    if (fieldContext.isGroupField) {
+    if (fieldInfo.isGroupField) {
       rightIcon = Padding(
         padding: const EdgeInsets.all(2.0),
         child: svgWidget("grid/checkmark"),
@@ -81,17 +81,17 @@ class _GridGroupCell extends StatelessWidget {
     return SizedBox(
       height: GridSize.typeOptionItemHeight,
       child: FlowyButton(
-        text: FlowyText.medium(fieldContext.name),
+        text: FlowyText.medium(fieldInfo.name),
         leftIcon: svgWidget(
-          fieldContext.fieldType.iconName(),
+          fieldInfo.fieldType.iconName(),
           color: Theme.of(context).colorScheme.onSurface,
         ),
         rightIcon: rightIcon,
         onTap: () {
           context.read<GridGroupBloc>().add(
                 GridGroupEvent.setGroupByField(
-                  fieldContext.id,
-                  fieldContext.fieldType,
+                  fieldInfo.id,
+                  fieldInfo.fieldType,
                 ),
               );
           onSelected();

+ 10 - 9
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart

@@ -51,7 +51,7 @@ class _GridPropertyListState extends State<GridPropertyList> {
             return _GridPropertyCell(
               popoverMutex: _popoverMutex,
               gridId: widget.gridId,
-              fieldContext: field,
+              fieldInfo: field,
               key: ValueKey(field.id),
             );
           }).toList();
@@ -74,12 +74,12 @@ class _GridPropertyListState extends State<GridPropertyList> {
 }
 
 class _GridPropertyCell extends StatelessWidget {
-  final GridFieldContext fieldContext;
+  final FieldInfo fieldInfo;
   final String gridId;
   final PopoverMutex popoverMutex;
   const _GridPropertyCell({
     required this.gridId,
-    required this.fieldContext,
+    required this.fieldInfo,
     required this.popoverMutex,
     Key? key,
   }) : super(key: key);
@@ -87,7 +87,7 @@ class _GridPropertyCell extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final checkmark = svgWidget(
-      fieldContext.visibility ? 'home/show' : 'home/hide',
+      fieldInfo.visibility ? 'home/show' : 'home/hide',
       color: Theme.of(context).colorScheme.onSurface,
     );
 
@@ -104,7 +104,7 @@ class _GridPropertyCell extends StatelessWidget {
           onPressed: () {
             context.read<GridPropertyBloc>().add(
                 GridPropertyEvent.setFieldVisibility(
-                    fieldContext.id, !fieldContext.visibility));
+                    fieldInfo.id, !fieldInfo.visibility));
           },
           icon: checkmark.padding(all: 6),
         )
@@ -116,21 +116,22 @@ class _GridPropertyCell extends StatelessWidget {
     return AppFlowyPopover(
       mutex: popoverMutex,
       offset: const Offset(20, 0),
+      direction: PopoverDirection.leftWithTopAligned,
       constraints: BoxConstraints.loose(const Size(240, 400)),
       child: FlowyButton(
-        text: FlowyText.medium(fieldContext.name),
+        text: FlowyText.medium(fieldInfo.name),
         leftIcon: svgWidget(
-          fieldContext.fieldType.iconName(),
+          fieldInfo.fieldType.iconName(),
           color: Theme.of(context).colorScheme.onSurface,
         ),
       ),
       popupBuilder: (BuildContext context) {
         return FieldEditor(
           gridId: gridId,
-          fieldName: fieldContext.name,
+          fieldName: fieldInfo.name,
           typeOptionLoader: FieldTypeOptionLoader(
             gridId: gridId,
-            field: fieldContext.field,
+            field: fieldInfo.field,
           ),
         );
       },

+ 11 - 43
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart

@@ -6,7 +6,6 @@ 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:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import '../../../application/field/field_controller.dart';
@@ -31,33 +30,11 @@ class GridSettingList extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return BlocProvider(
-      create: (context) => GridSettingBloc(gridId: settingContext.gridId),
-      child: BlocListener<GridSettingBloc, GridSettingState>(
-        listenWhen: (previous, current) =>
-            previous.selectedAction != current.selectedAction,
-        listener: (context, state) {
-          state.selectedAction.foldLeft(null, (_, action) {
-            onAction(action, settingContext);
-          });
-        },
-        child: BlocBuilder<GridSettingBloc, GridSettingState>(
-          builder: (context, state) {
-            return _renderList();
-          },
-        ),
-      ),
-    );
-  }
-
-  String identifier() {
-    return toString();
-  }
-
-  Widget _renderList() {
     final cells =
         GridSettingAction.values.where((value) => value.enable()).map((action) {
-      return _SettingItem(action: action);
+      return _SettingItem(
+          action: action,
+          onAction: (action) => onAction(action, settingContext));
     }).toList();
 
     return SizedBox(
@@ -80,33 +57,24 @@ class GridSettingList extends StatelessWidget {
 
 class _SettingItem extends StatelessWidget {
   final GridSettingAction action;
+  final Function(GridSettingAction) onAction;
 
   const _SettingItem({
     required this.action,
+    required this.onAction,
     Key? key,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    final isSelected = context
-        .read<GridSettingBloc>()
-        .state
-        .selectedAction
-        .foldLeft(false, (_, selectedAction) => selectedAction == action);
-
     return SizedBox(
       height: GridSize.typeOptionItemHeight,
       child: FlowyButton(
-        isSelected: isSelected,
         text: FlowyText.medium(
           action.title(),
           color: action.enable() ? null : Theme.of(context).disabledColor,
         ),
-        onTap: () {
-          context
-              .read<GridSettingBloc>()
-              .add(GridSettingEvent.performAction(action));
-        },
+        onTap: () => onAction(action),
         leftIcon: svgWidget(
           action.iconName(),
           color: Theme.of(context).colorScheme.onSurface,
@@ -119,29 +87,29 @@ class _SettingItem extends StatelessWidget {
 extension _GridSettingExtension on GridSettingAction {
   String iconName() {
     switch (this) {
-      case GridSettingAction.filter:
+      case GridSettingAction.showFilters:
         return 'grid/setting/filter';
       case GridSettingAction.sortBy:
         return 'grid/setting/sort';
-      case GridSettingAction.properties:
+      case GridSettingAction.showProperties:
         return 'grid/setting/properties';
     }
   }
 
   String title() {
     switch (this) {
-      case GridSettingAction.filter:
+      case GridSettingAction.showFilters:
         return LocaleKeys.grid_settings_filter.tr();
       case GridSettingAction.sortBy:
         return LocaleKeys.grid_settings_sortBy.tr();
-      case GridSettingAction.properties:
+      case GridSettingAction.showProperties:
         return LocaleKeys.grid_settings_Properties.tr();
     }
   }
 
   bool enable() {
     switch (this) {
-      case GridSettingAction.properties:
+      case GridSettingAction.showProperties:
         return true;
       default:
         return false;

+ 6 - 72
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart

@@ -1,14 +1,9 @@
-import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart';
-import 'package:flowy_infra/image.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flowy_infra_ui/style_widget/extension.dart';
-import 'package:flowy_infra_ui/style_widget/icon_button.dart';
 import 'package:flutter/material.dart';
 
 import '../../../application/field/field_controller.dart';
 import '../../layout/sizes.dart';
-import 'grid_property.dart';
-import 'grid_setting.dart';
+import 'filter_button.dart';
+import 'setting_button.dart';
 
 class GridToolbarContext {
   final String gridId;
@@ -20,82 +15,21 @@ class GridToolbarContext {
 }
 
 class GridToolbar extends StatelessWidget {
-  final GridToolbarContext toolbarContext;
-  const GridToolbar({required this.toolbarContext, Key? key}) : super(key: key);
+  const GridToolbar({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    final settingContext = GridSettingContext(
-      gridId: toolbarContext.gridId,
-      fieldController: toolbarContext.fieldController,
-    );
     return SizedBox(
       height: 40,
       child: Row(
+        mainAxisAlignment: MainAxisAlignment.center,
         children: [
           SizedBox(width: GridSize.leadingHeaderPadding),
-          _SettingButton(settingContext: settingContext),
           const Spacer(),
+          const FilterButton(),
+          const SettingButton(),
         ],
       ),
     );
   }
 }
-
-class _SettingButton extends StatelessWidget {
-  final GridSettingContext settingContext;
-  const _SettingButton({required this.settingContext, Key? key})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return AppFlowyPopover(
-      constraints: BoxConstraints.loose(const Size(260, 400)),
-      offset: const Offset(0, 10),
-      margin: const EdgeInsets.all(6),
-      child: FlowyIconButton(
-        width: 22,
-        icon: svgWidget(
-          "grid/setting/setting",
-          color: Theme.of(context).colorScheme.onSurface,
-        ).padding(horizontal: 3, vertical: 3),
-      ),
-      popupBuilder: (BuildContext context) {
-        return _GridSettingListPopover(settingContext: settingContext);
-      },
-    );
-  }
-}
-
-class _GridSettingListPopover extends StatefulWidget {
-  final GridSettingContext settingContext;
-
-  const _GridSettingListPopover({Key? key, required this.settingContext})
-      : super(key: key);
-
-  @override
-  State<StatefulWidget> createState() => _GridSettingListPopoverState();
-}
-
-class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
-  GridSettingAction? _action;
-
-  @override
-  Widget build(BuildContext context) {
-    if (_action == GridSettingAction.properties) {
-      return GridPropertyList(
-        gridId: widget.settingContext.gridId,
-        fieldController: widget.settingContext.fieldController,
-      );
-    }
-
-    return GridSettingList(
-      settingContext: widget.settingContext,
-      onAction: (action, settingContext) {
-        setState(() {
-          _action = action;
-        });
-      },
-    );
-  }
-}

+ 100 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart

@@ -0,0 +1,100 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/color_extension.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flowy_infra_ui/style_widget/button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'grid_property.dart';
+import 'grid_setting.dart';
+
+class SettingButton extends StatefulWidget {
+  const SettingButton({Key? key}) : super(key: key);
+
+  @override
+  State<SettingButton> createState() => _SettingButtonState();
+}
+
+class _SettingButtonState extends State<SettingButton> {
+  late PopoverController popoverController;
+
+  @override
+  void initState() {
+    popoverController = PopoverController();
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocSelector<GridBloc, GridState, GridSettingContext>(
+      selector: (state) {
+        final fieldController =
+            context.read<GridBloc>().gridController.fieldController;
+        return GridSettingContext(
+          gridId: state.gridId,
+          fieldController: fieldController,
+        );
+      },
+      builder: (context, settingContext) {
+        return AppFlowyPopover(
+          controller: popoverController,
+          constraints: BoxConstraints.loose(const Size(260, 400)),
+          direction: PopoverDirection.leftWithTopAligned,
+          offset: const Offset(0, 10),
+          margin: const EdgeInsets.all(6),
+          triggerActions: PopoverTriggerFlags.none,
+          child: FlowyTextButton(
+            LocaleKeys.settings_title.tr(),
+            fontSize: 14,
+            fillColor: Colors.transparent,
+            hoverColor: AFThemeExtension.of(context).lightGreyHover,
+            padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6),
+            onPressed: () {
+              popoverController.show();
+            },
+          ),
+          popupBuilder: (BuildContext context) {
+            return _GridSettingListPopover(settingContext: settingContext);
+          },
+        );
+      },
+    );
+  }
+}
+
+class _GridSettingListPopover extends StatefulWidget {
+  final GridSettingContext settingContext;
+
+  const _GridSettingListPopover({Key? key, required this.settingContext})
+      : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _GridSettingListPopoverState();
+}
+
+class _GridSettingListPopoverState extends State<_GridSettingListPopover> {
+  GridSettingAction? _action;
+
+  @override
+  Widget build(BuildContext context) {
+    if (_action == GridSettingAction.showProperties) {
+      return GridPropertyList(
+        gridId: widget.settingContext.gridId,
+        fieldController: widget.settingContext.fieldController,
+      );
+    }
+
+    return GridSettingList(
+      settingContext: widget.settingContext,
+      onAction: (action, settingContext) {
+        setState(() {
+          _action = action;
+        });
+      },
+    );
+  }
+}

+ 0 - 5
frontend/app_flowy/lib/startup/deps_resolver.dart

@@ -127,11 +127,6 @@ void _resolveDocDeps(GetIt getIt) {
 }
 
 void _resolveGridDeps(GetIt getIt) {
-  // GridPB
-  getIt.registerFactoryParam<GridBloc, ViewPB, void>(
-    (view, _) => GridBloc(view: view),
-  );
-
   getIt.registerFactoryParam<GridHeaderBloc, String, GridFieldController>(
     (gridId, fieldController) => GridHeaderBloc(
       gridId: gridId,

+ 0 - 49
frontend/app_flowy/lib/workspace/application/home/home_bloc.dart

@@ -1,5 +1,4 @@
 import 'package:app_flowy/user/application/user_listener.dart';
-import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart';
 import 'package:flowy_infra/time/duration.dart';
 import 'package:flowy_sdk/log.dart';
 import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart';
@@ -38,40 +37,12 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
           showLoading: (e) async {
             emit(state.copyWith(isLoading: e.isLoading));
           },
-          setEditPanel: (e) async {
-            emit(state.copyWith(panelContext: some(e.editContext)));
-          },
-          dismissEditPanel: (value) async {
-            emit(state.copyWith(panelContext: none()));
-          },
-          forceCollapse: (e) async {
-            emit(state.copyWith(forceCollapse: e.forceCollapse));
-          },
           didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
             emit(state.copyWith(workspaceSetting: value.setting));
           },
           unauthorized: (_Unauthorized value) {
             emit(state.copyWith(unauthorized: true));
           },
-          collapseMenu: (_CollapseMenu e) {
-            emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
-          },
-          editPanelResizeStart: (_EditPanelResizeStart e) {
-            emit(state.copyWith(
-              resizeType: MenuResizeType.drag,
-              resizeStart: state.resizeOffset,
-            ));
-          },
-          editPanelResized: (_EditPanelResized e) {
-            final newPosition =
-                (e.offset + state.resizeStart).clamp(-50, 200).toDouble();
-            if (state.resizeOffset != newPosition) {
-              emit(state.copyWith(resizeOffset: newPosition));
-            }
-          },
-          editPanelResizeEnd: (_EditPanelResizeEnd e) {
-            emit(state.copyWith(resizeType: MenuResizeType.slide));
-          },
         );
       },
     );
@@ -112,42 +83,22 @@ extension MenuResizeTypeExtension on MenuResizeType {
 class HomeEvent with _$HomeEvent {
   const factory HomeEvent.initial() = _Initial;
   const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading;
-  const factory HomeEvent.forceCollapse(bool forceCollapse) = _ForceCollapse;
-  const factory HomeEvent.setEditPanel(EditPanelContext editContext) =
-      _ShowEditPanel;
-  const factory HomeEvent.dismissEditPanel() = _DismissEditPanel;
   const factory HomeEvent.didReceiveWorkspaceSetting(
       WorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting;
   const factory HomeEvent.unauthorized(String msg) = _Unauthorized;
-  const factory HomeEvent.collapseMenu() = _CollapseMenu;
-  const factory HomeEvent.editPanelResized(double offset) = _EditPanelResized;
-  const factory HomeEvent.editPanelResizeStart() = _EditPanelResizeStart;
-  const factory HomeEvent.editPanelResizeEnd() = _EditPanelResizeEnd;
 }
 
 @freezed
 class HomeState with _$HomeState {
   const factory HomeState({
     required bool isLoading,
-    required bool forceCollapse,
-    required Option<EditPanelContext> panelContext,
     required WorkspaceSettingPB workspaceSetting,
     required bool unauthorized,
-    required bool isMenuCollapsed,
-    required double resizeOffset,
-    required double resizeStart,
-    required MenuResizeType resizeType,
   }) = _HomeState;
 
   factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState(
         isLoading: false,
-        forceCollapse: false,
-        panelContext: none(),
         workspaceSetting: workspaceSetting,
         unauthorized: false,
-        isMenuCollapsed: false,
-        resizeOffset: 0,
-        resizeStart: 0,
-        resizeType: MenuResizeType.slide,
       );
 }

+ 124 - 0
frontend/app_flowy/lib/workspace/application/home/home_setting_bloc.dart

@@ -0,0 +1,124 @@
+import 'package:app_flowy/user/application/user_listener.dart';
+import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart';
+import 'package:flowy_infra/time/duration.dart';
+import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart'
+    show WorkspaceSettingPB;
+import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:dartz/dartz.dart';
+part 'home_setting_bloc.freezed.dart';
+
+class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
+  final UserWorkspaceListener _listener;
+
+  HomeSettingBloc(
+    UserProfilePB user,
+    WorkspaceSettingPB workspaceSetting,
+  )   : _listener = UserWorkspaceListener(userProfile: user),
+        super(HomeSettingState.initial(workspaceSetting)) {
+    on<HomeSettingEvent>(
+      (event, emit) async {
+        await event.map(
+          initial: (_Initial value) {},
+          setEditPanel: (e) async {
+            emit(state.copyWith(panelContext: some(e.editContext)));
+          },
+          dismissEditPanel: (value) async {
+            emit(state.copyWith(panelContext: none()));
+          },
+          forceCollapse: (e) async {
+            emit(state.copyWith(forceCollapse: e.forceCollapse));
+          },
+          didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) {
+            emit(state.copyWith(workspaceSetting: value.setting));
+          },
+          collapseMenu: (_CollapseMenu e) {
+            emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed));
+          },
+          editPanelResizeStart: (_EditPanelResizeStart e) {
+            emit(state.copyWith(
+              resizeType: MenuResizeType.drag,
+              resizeStart: state.resizeOffset,
+            ));
+          },
+          editPanelResized: (_EditPanelResized e) {
+            final newPosition =
+                (e.offset + state.resizeStart).clamp(-50, 200).toDouble();
+            if (state.resizeOffset != newPosition) {
+              emit(state.copyWith(resizeOffset: newPosition));
+            }
+          },
+          editPanelResizeEnd: (_EditPanelResizeEnd e) {
+            emit(state.copyWith(resizeType: MenuResizeType.slide));
+          },
+        );
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    await _listener.stop();
+    return super.close();
+  }
+}
+
+enum MenuResizeType {
+  slide,
+  drag,
+}
+
+extension MenuResizeTypeExtension on MenuResizeType {
+  Duration duration() {
+    switch (this) {
+      case MenuResizeType.drag:
+        return 30.milliseconds;
+      case MenuResizeType.slide:
+        return 350.milliseconds;
+    }
+  }
+}
+
+@freezed
+class HomeSettingEvent with _$HomeSettingEvent {
+  const factory HomeSettingEvent.initial() = _Initial;
+  const factory HomeSettingEvent.forceCollapse(bool forceCollapse) =
+      _ForceCollapse;
+  const factory HomeSettingEvent.setEditPanel(EditPanelContext editContext) =
+      _ShowEditPanel;
+  const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel;
+  const factory HomeSettingEvent.didReceiveWorkspaceSetting(
+      WorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting;
+  const factory HomeSettingEvent.collapseMenu() = _CollapseMenu;
+  const factory HomeSettingEvent.editPanelResized(double offset) =
+      _EditPanelResized;
+  const factory HomeSettingEvent.editPanelResizeStart() = _EditPanelResizeStart;
+  const factory HomeSettingEvent.editPanelResizeEnd() = _EditPanelResizeEnd;
+}
+
+@freezed
+class HomeSettingState with _$HomeSettingState {
+  const factory HomeSettingState({
+    required bool forceCollapse,
+    required Option<EditPanelContext> panelContext,
+    required WorkspaceSettingPB workspaceSetting,
+    required bool unauthorized,
+    required bool isMenuCollapsed,
+    required double resizeOffset,
+    required double resizeStart,
+    required MenuResizeType resizeType,
+  }) = _HomeSettingState;
+
+  factory HomeSettingState.initial(WorkspaceSettingPB workspaceSetting) =>
+      HomeSettingState(
+        forceCollapse: false,
+        panelContext: none(),
+        workspaceSetting: workspaceSetting,
+        unauthorized: false,
+        isMenuCollapsed: false,
+        resizeOffset: 0,
+        resizeStart: 0,
+        resizeType: MenuResizeType.slide,
+      );
+}

+ 7 - 8
frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart

@@ -1,6 +1,6 @@
 import 'dart:io' show Platform;
 
-import 'package:app_flowy/workspace/application/home/home_bloc.dart';
+import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
 import 'package:flowy_infra/size.dart';
 import 'package:flutter/material.dart';
 // ignore: import_of_legacy_library_into_null_safe
@@ -20,20 +20,19 @@ class HomeLayout {
   late double menuSpacing;
   late Duration animDuration;
 
-  HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint,
-      bool forceCollapse) {
-    final homeBlocState = context.read<HomeBloc>().state;
+  HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint) {
+    final homeSetting = context.read<HomeSettingBloc>().state;
 
-    showEditPanel = homeBlocState.panelContext.isSome();
+    showEditPanel = homeSetting.panelContext.isSome();
 
     menuWidth = Sizes.sideBarMed;
     if (context.widthPx >= PageBreaks.desktop) {
       menuWidth = Sizes.sideBarLg;
     }
 
-    menuWidth += homeBlocState.resizeOffset;
+    menuWidth += homeSetting.resizeOffset;
 
-    if (forceCollapse) {
+    if (homeSetting.forceCollapse) {
       showMenu = false;
     } else {
       showMenu = true;
@@ -43,7 +42,7 @@ class HomeLayout {
     homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0;
 
     menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0;
-    animDuration = homeBlocState.resizeType.duration();
+    animDuration = homeSetting.resizeType.duration();
 
     editPanelWidth = HomeSizes.editPanelWidth;
     homePageROffset = showEditPanel ? editPanelWidth : 0;

+ 33 - 31
frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart

@@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/blank/blank.dart';
 import 'package:app_flowy/startup/plugin/plugin.dart';
 import 'package:app_flowy/workspace/application/home/home_bloc.dart';
 import 'package:app_flowy/workspace/application/home/home_service.dart';
+import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
 
 import 'package:app_flowy/workspace/presentation/home/hotkeys.dart';
 import 'package:app_flowy/workspace/application/view/view_ext.dart';
@@ -44,6 +45,12 @@ class _HomeScreenState extends State<HomeScreen> {
               ..add(const HomeEvent.initial());
           },
         ),
+        BlocProvider<HomeSettingBloc>(
+          create: (context) {
+            return HomeSettingBloc(widget.user, widget.workspaceSetting)
+              ..add(const HomeSettingEvent.initial());
+          },
+        ),
       ],
       child: HomeHotKeys(
           child: Scaffold(
@@ -54,20 +61,20 @@ class _HomeScreenState extends State<HomeScreen> {
               Log.error("Push to login screen when user token was invalid");
             }
           },
-          child: BlocBuilder<HomeBloc, HomeState>(
+          child: BlocBuilder<HomeSettingBloc, HomeSettingState>(
             buildWhen: (previous, current) => previous != current,
             builder: (context, state) {
               final collapsedNotifier =
                   getIt<HomeStackManager>().collapsedNotifier;
               collapsedNotifier.addPublishListener((isCollapsed) {
                 context
-                    .read<HomeBloc>()
-                    .add(HomeEvent.forceCollapse(isCollapsed));
+                    .read<HomeSettingBloc>()
+                    .add(HomeSettingEvent.forceCollapse(isCollapsed));
               });
               return FlowyContainer(
                 Theme.of(context).colorScheme.surface,
                 // Colors.white,
-                child: _buildBody(context, state),
+                child: _buildBody(context),
               );
             },
           ),
@@ -76,25 +83,22 @@ class _HomeScreenState extends State<HomeScreen> {
     );
   }
 
-  Widget _buildBody(BuildContext context, HomeState state) {
+  Widget _buildBody(BuildContext context) {
     return LayoutBuilder(
       builder: (BuildContext context, BoxConstraints constraints) {
-        final layout = HomeLayout(context, constraints, state.forceCollapse);
+        final layout = HomeLayout(context, constraints);
         final homeStack = HomeStack(
           layout: layout,
           delegate: HomeScreenStackAdaptor(
             buildContext: context,
-            homeState: state,
           ),
         );
         final menu = _buildHomeMenu(
           layout: layout,
           context: context,
-          state: state,
         );
         final homeMenuResizer = _buildHomeMenuResizer(context: context);
         final editPanel = _buildEditPanel(
-          homeState: state,
           layout: layout,
           context: context,
         );
@@ -111,11 +115,11 @@ class _HomeScreenState extends State<HomeScreen> {
     );
   }
 
-  Widget _buildHomeMenu(
-      {required HomeLayout layout,
-      required BuildContext context,
-      required HomeState state}) {
-    final workspaceSetting = state.workspaceSetting;
+  Widget _buildHomeMenu({
+    required HomeLayout layout,
+    required BuildContext context,
+  }) {
+    final workspaceSetting = widget.workspaceSetting;
     final homeMenu = HomeMenu(
       user: widget.user,
       workspaceSetting: workspaceSetting,
@@ -144,12 +148,12 @@ class _HomeScreenState extends State<HomeScreen> {
     return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu));
   }
 
-  Widget _buildEditPanel(
-      {required HomeState homeState,
-      required BuildContext context,
-      required HomeLayout layout}) {
-    final homeBloc = context.read<HomeBloc>();
-    return BlocBuilder<HomeBloc, HomeState>(
+  Widget _buildEditPanel({
+    required BuildContext context,
+    required HomeLayout layout,
+  }) {
+    final homeBloc = context.read<HomeSettingBloc>();
+    return BlocBuilder<HomeSettingBloc, HomeSettingState>(
       buildWhen: (previous, current) =>
           previous.panelContext != current.panelContext,
       builder: (context, state) {
@@ -160,7 +164,7 @@ class _HomeScreenState extends State<HomeScreen> {
               child: EditPanel(
                 panelContext: panelContext,
                 onEndEdit: () =>
-                    homeBloc.add(const HomeEvent.dismissEditPanel()),
+                    homeBloc.add(const HomeSettingEvent.dismissEditPanel()),
               ),
             ),
           ),
@@ -177,17 +181,17 @@ class _HomeScreenState extends State<HomeScreen> {
       child: GestureDetector(
           dragStartBehavior: DragStartBehavior.down,
           onHorizontalDragStart: (details) => context
-              .read<HomeBloc>()
-              .add(const HomeEvent.editPanelResizeStart()),
+              .read<HomeSettingBloc>()
+              .add(const HomeSettingEvent.editPanelResizeStart()),
           onHorizontalDragUpdate: (details) => context
-              .read<HomeBloc>()
-              .add(HomeEvent.editPanelResized(details.localPosition.dx)),
+              .read<HomeSettingBloc>()
+              .add(HomeSettingEvent.editPanelResized(details.localPosition.dx)),
           onHorizontalDragEnd: (details) => context
-              .read<HomeBloc>()
-              .add(const HomeEvent.editPanelResizeEnd()),
+              .read<HomeSettingBloc>()
+              .add(const HomeSettingEvent.editPanelResizeEnd()),
           onHorizontalDragCancel: () => context
-              .read<HomeBloc>()
-              .add(const HomeEvent.editPanelResizeEnd()),
+              .read<HomeSettingBloc>()
+              .add(const HomeSettingEvent.editPanelResizeEnd()),
           behavior: HitTestBehavior.translucent,
           child: SizedBox(
             width: 10,
@@ -252,11 +256,9 @@ class _HomeScreenState extends State<HomeScreen> {
 
 class HomeScreenStackAdaptor extends HomeStackDelegate {
   final BuildContext buildContext;
-  final HomeState homeState;
 
   HomeScreenStackAdaptor({
     required this.buildContext,
-    required this.homeState,
   });
 
   @override

+ 4 - 2
frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart

@@ -1,7 +1,7 @@
 import 'dart:io';
 
 import 'package:app_flowy/startup/startup.dart';
-import 'package:app_flowy/workspace/application/home/home_bloc.dart';
+import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:flutter/material.dart';
 import 'package:hotkey_manager/hotkey_manager.dart';
@@ -22,7 +22,9 @@ class HomeHotKeys extends StatelessWidget {
     hotKeyManager.register(
       hotKey,
       keyDownHandler: (hotKey) {
-        context.read<HomeBloc>().add(const HomeEvent.collapseMenu());
+        context
+            .read<HomeSettingBloc>()
+            .add(const HomeSettingEvent.collapseMenu());
         getIt<HomeStackManager>().collapsedNotifier.value =
             !getIt<HomeStackManager>().collapsedNotifier.currentValue!;
       },

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart

@@ -54,7 +54,7 @@ class AddButtonActionWrapper extends ActionCell {
   AddButtonActionWrapper({required this.pluginBuilder});
 
   @override
-  Widget? icon(Color iconColor) =>
+  Widget? leftIcon(Color iconColor) =>
       svgWidget(pluginBuilder.menuIcon, color: iconColor);
 
   @override

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart

@@ -187,7 +187,7 @@ class DisclosureActionWrapper extends ActionCell {
 
   DisclosureActionWrapper(this.inner);
   @override
-  Widget? icon(Color iconColor) => inner.icon(iconColor);
+  Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
 
   @override
   String get name => inner.name;

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart

@@ -211,7 +211,7 @@ class ViewDisclosureActionWrapper extends ActionCell {
 
   ViewDisclosureActionWrapper(this.inner);
   @override
-  Widget? icon(Color iconColor) => inner.icon(iconColor);
+  Widget? leftIcon(Color iconColor) => inner.icon(iconColor);
 
   @override
   String get name => inner.name;

+ 4 - 4
frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart

@@ -4,6 +4,7 @@ export './app/menu_app.dart';
 import 'dart:io' show Platform;
 import 'package:app_flowy/generated/locale_keys.g.dart';
 import 'package:app_flowy/plugins/trash/menu.dart';
+import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
 import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -21,7 +22,6 @@ import 'package:expandable/expandable.dart';
 import 'package:flowy_infra/time/duration.dart';
 import 'package:app_flowy/startup/startup.dart';
 import 'package:app_flowy/workspace/application/menu/menu_bloc.dart';
-import 'package:app_flowy/workspace/application/home/home_bloc.dart';
 import 'package:app_flowy/core/frameless_window.dart';
 // import 'package:app_flowy/workspace/presentation/home/home_sizes.dart';
 import 'package:flowy_infra/image.dart';
@@ -68,7 +68,7 @@ class HomeMenu extends StatelessWidget {
               getIt<HomeStackManager>().setPlugin(state.plugin);
             },
           ),
-          BlocListener<HomeBloc, HomeState>(
+          BlocListener<HomeSettingBloc, HomeSettingState>(
             listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed,
             listener: (context, state) {
               _collapsedNotifier.value = state.isMenuCollapsed;
@@ -231,8 +231,8 @@ class MenuTopBar extends StatelessWidget {
                     width: 28,
                     hoverColor: Colors.transparent,
                     onPressed: () => context
-                        .read<HomeBloc>()
-                        .add(const HomeEvent.collapseMenu()),
+                        .read<HomeSettingBloc>()
+                        .add(const HomeSettingEvent.collapseMenu()),
                     iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
                     icon: svgWidget(
                       "home/hide_menu",

+ 4 - 22
frontend/app_flowy/lib/workspace/presentation/home/navigation.dart

@@ -1,7 +1,7 @@
 import 'dart:io';
 
 import 'package:app_flowy/generated/locale_keys.g.dart';
-import 'package:app_flowy/workspace/application/home/home_bloc.dart';
+import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart';
 import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
 import 'package:flowy_infra/color_extension.dart';
 import 'package:flowy_infra/image.dart';
@@ -36,26 +36,6 @@ class NavigationNotifier with ChangeNotifier {
   }
 }
 
-// [[diagram: HomeStack navigation flow]]
-//                                                                              ┌───────────────────────┐
-//                     2.notify listeners                                ┌──────│DefaultHomeStackContext│
-//  ┌────────────────┐           ┌───────────┐   ┌────────────────┐      │      └───────────────────────┘
-//  │HomeStackNotifie│◀──────────│ HomeStack │◀──│HomeStackContext│◀─ impl
-//  └────────────────┘           └───────────┘   └────────────────┘      │       ┌───────────────────┐
-//           │                         ▲                                 └───────│  DocStackContext  │
-//           │                         │                                         └───────────────────┘
-//    3.notify change            1.set context
-//           │                         │
-//           ▼                         │
-// ┌───────────────────┐     ┌──────────────────┐
-// │NavigationNotifier │     │ ViewSectionItem  │
-// └───────────────────┘     └──────────────────┘
-//           │
-//           │
-//           ▼
-//  ┌─────────────────┐
-//  │ FlowyNavigation │   4.render navigation items
-//  └─────────────────┘
 class FlowyNavigation extends StatelessWidget {
   const FlowyNavigation({Key? key}) : super(key: key);
 
@@ -109,7 +89,9 @@ class FlowyNavigation extends StatelessWidget {
                     hoverColor: Colors.transparent,
                     onPressed: () {
                       notifier.value = false;
-                      ctx.read<HomeBloc>().add(const HomeEvent.collapseMenu());
+                      ctx
+                          .read<HomeSettingBloc>()
+                          .add(const HomeSettingEvent.collapseMenu());
                     },
                     iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
                     icon: svgWidget(

+ 0 - 2
frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart

@@ -94,7 +94,6 @@ class _BuildEmojiPickerViewState extends State<BuildEmojiPickerView> {
     return Stack(
       children: [
         Positioned(
-          //TODO @gaganyadav80: Not sure about the calculated position.
           top: widget.offset!.dy -
               MediaQuery.of(context).size.height / 2.83 -
               30,
@@ -103,7 +102,6 @@ class _BuildEmojiPickerViewState extends State<BuildEmojiPickerView> {
           child: Material(
             borderRadius: BorderRadius.circular(8.0),
             child: SizedBox(
-              //TODO @gaganyadav80: FIXIT: Gets too large when fullscreen.
               height: MediaQuery.of(context).size.height / 2.83 + 20,
               width: MediaQuery.of(context).size.width / 3.92,
               child: ClipRRect(

+ 1 - 1
frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart

@@ -167,7 +167,7 @@ class BubbleActionWrapper extends ActionCell {
 
   BubbleActionWrapper(this.inner);
   @override
-  Widget? icon(Color iconColor) => FlowyText.regular(inner.emoji);
+  Widget? leftIcon(Color iconColor) => FlowyText.regular(inner.emoji);
 
   @override
   String get name => inner.name;

+ 22 - 4
frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart

@@ -8,21 +8,25 @@ import 'package:styled_widget/styled_widget.dart';
 
 class PopoverActionList<T extends PopoverAction> extends StatefulWidget {
   final List<T> actions;
+  final PopoverMutex? mutex;
   final Function(T, PopoverController) onSelected;
   final BoxConstraints constraints;
   final PopoverDirection direction;
   final Widget Function(PopoverController) buildChild;
   final VoidCallback? onClosed;
+  final bool asBarrier;
 
   const PopoverActionList({
     required this.actions,
     required this.buildChild,
     required this.onSelected,
+    this.mutex,
     this.onClosed,
     this.direction = PopoverDirection.rightWithTopAligned,
+    this.asBarrier = false,
     this.constraints = const BoxConstraints(
       minWidth: 120,
-      maxWidth: 360,
+      maxWidth: 460,
       maxHeight: 300,
     ),
     Key? key,
@@ -47,9 +51,11 @@ class _PopoverActionListState<T extends PopoverAction>
     final child = widget.buildChild(popoverController);
 
     return AppFlowyPopover(
+      asBarrier: widget.asBarrier,
       controller: popoverController,
       constraints: widget.constraints,
       direction: widget.direction,
+      mutex: widget.mutex,
       triggerActions: PopoverTriggerFlags.none,
       onClose: widget.onClosed,
       popupBuilder: (BuildContext popoverContext) {
@@ -82,7 +88,8 @@ class _PopoverActionListState<T extends PopoverAction>
 }
 
 abstract class ActionCell extends PopoverAction {
-  Widget? icon(Color iconColor);
+  Widget? leftIcon(Color iconColor) => null;
+  Widget? rightIcon(Color iconColor) => null;
   String get name;
 }
 
@@ -113,7 +120,11 @@ class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     final actionCell = action as ActionCell;
-    final icon = actionCell.icon(Theme.of(context).colorScheme.onSurface);
+    final leftIcon =
+        actionCell.leftIcon(Theme.of(context).colorScheme.onSurface);
+
+    final rightIcon =
+        actionCell.rightIcon(Theme.of(context).colorScheme.onSurface);
 
     return FlowyHover(
       child: GestureDetector(
@@ -123,13 +134,20 @@ class ActionCellWidget<T extends PopoverAction> extends StatelessWidget {
           height: itemHeight,
           child: Row(
             children: [
-              if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)],
+              if (leftIcon != null) ...[
+                leftIcon,
+                HSpace(ActionListSizes.itemHPadding)
+              ],
               Expanded(
                 child: FlowyText.medium(
                   actionCell.name,
                   overflow: TextOverflow.visible,
                 ),
               ),
+              if (rightIcon != null) ...[
+                HSpace(ActionListSizes.itemHPadding),
+                rightIcon,
+              ],
             ],
           ),
         ).padding(

+ 22 - 21
frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart

@@ -16,6 +16,8 @@ class FlowyButton extends StatelessWidget {
   final Color? hoverColor;
   final bool isSelected;
   final BorderRadius radius;
+  final BoxDecoration? decoration;
+  final bool useIntrinsicWidth;
 
   const FlowyButton({
     Key? key,
@@ -28,6 +30,8 @@ class FlowyButton extends StatelessWidget {
     this.hoverColor,
     this.isSelected = false,
     this.radius = const BorderRadius.all(Radius.circular(6)),
+    this.decoration,
+    this.useIntrinsicWidth = false,
   }) : super(key: key);
 
   @override
@@ -63,12 +67,21 @@ class FlowyButton extends StatelessWidget {
           SizedBox.fromSize(size: const Size.square(16), child: rightIcon!));
     }
 
-    return Padding(
-      padding: margin,
-      child: Row(
-        mainAxisAlignment: MainAxisAlignment.center,
-        crossAxisAlignment: CrossAxisAlignment.center,
-        children: children,
+    Widget child = Row(
+      mainAxisAlignment: MainAxisAlignment.center,
+      crossAxisAlignment: CrossAxisAlignment.center,
+      children: children,
+    );
+
+    if (useIntrinsicWidth) {
+      child = IntrinsicWidth(child: child);
+    }
+
+    return Container(
+      decoration: decoration,
+      child: Padding(
+        padding: margin,
+        child: child,
       ),
     );
   }
@@ -89,6 +102,7 @@ class FlowyTextButton extends StatelessWidget {
   final BorderRadius? radius;
   final MainAxisAlignment mainAxisAlignment;
   final String? tooltip;
+  final BoxConstraints constraints;
 
   // final HoverDisplayConfig? hoverDisplay;
   const FlowyTextButton(
@@ -106,6 +120,7 @@ class FlowyTextButton extends StatelessWidget {
     this.radius,
     this.mainAxisAlignment = MainAxisAlignment.start,
     this.tooltip,
+    this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0),
   }) : super(key: key);
 
   @override
@@ -146,6 +161,7 @@ class FlowyTextButton extends StatelessWidget {
       splashColor: Colors.transparent,
       highlightColor: Colors.transparent,
       elevation: 0,
+      constraints: constraints,
       onPressed: onPressed,
       child: child,
     );
@@ -161,18 +177,3 @@ class FlowyTextButton extends StatelessWidget {
     return child;
   }
 }
-// return TextButton(
-//   style: ButtonStyle(
-//     textStyle: MaterialStateProperty.all(TextStyle(fontSize: fontSize)),
-//     alignment: Alignment.centerLeft,
-//     foregroundColor: MaterialStateProperty.all(Colors.black),
-//     padding: MaterialStateProperty.all<EdgeInsets>(
-//         const EdgeInsets.symmetric(horizontal: 2)),
-//   ),
-//   onPressed: onPressed,
-//   child: Text(
-//     text,
-//     overflow: TextOverflow.ellipsis,
-//     softWrap: false,
-//   ),
-// );

+ 4 - 4
frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart

@@ -25,16 +25,16 @@ void main() {
       boardBloc = BoardBloc(view: context.gridView)
         ..add(const BoardEvent.initial());
 
-      final fieldContext = context.singleSelectFieldContext();
+      final fieldInfo = context.singleSelectFieldContext();
       final loader = FieldTypeOptionLoader(
         gridId: context.gridView.id,
-        field: fieldContext.field,
+        field: fieldInfo.field,
       );
 
       editorBloc = FieldEditorBloc(
         gridId: context.gridView.id,
-        fieldName: fieldContext.name,
-        isGroupField: fieldContext.isGroupField,
+        fieldName: fieldInfo.name,
+        isGroupField: fieldInfo.isGroupField,
         loader: loader,
       )..add(const FieldEditorEvent.initial());
 

+ 2 - 2
frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart

@@ -14,9 +14,9 @@ void main() {
   setUpAll(() async {
     boardTest = await AppFlowyBoardTest.ensureInitialized();
     context = await boardTest.createTestBoard();
-    final fieldContext = context.singleSelectFieldContext();
+    final fieldInfo = context.singleSelectFieldContext();
     editorBloc = context.createFieldEditor(
-      fieldContext: fieldContext,
+      fieldInfo: fieldInfo,
     )..add(const FieldEditorEvent.initial());
 
     await boardResponseFuture();

+ 15 - 15
frontend/app_flowy/test/bloc_test/board_test/util.dart

@@ -78,26 +78,26 @@ class BoardTestContext {
     return _boardDataController.blocks;
   }
 
-  List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
+  List<FieldInfo> get fieldContexts => fieldController.fieldInfos;
 
   GridFieldController get fieldController {
     return _boardDataController.fieldController;
   }
 
   FieldEditorBloc createFieldEditor({
-    GridFieldContext? fieldContext,
+    FieldInfo? fieldInfo,
   }) {
     IFieldTypeOptionLoader loader;
-    if (fieldContext == null) {
+    if (fieldInfo == null) {
       loader = NewFieldTypeOptionLoader(gridId: gridView.id);
     } else {
       loader =
-          FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field);
+          FieldTypeOptionLoader(gridId: gridView.id, field: fieldInfo.field);
     }
 
     final editorBloc = FieldEditorBloc(
-      fieldName: fieldContext?.name ?? '',
-      isGroupField: fieldContext?.isGroupField ?? false,
+      fieldName: fieldInfo?.name ?? '',
+      isGroupField: fieldInfo?.isGroupField ?? false,
       loader: loader,
       gridId: gridView.id,
     );
@@ -146,10 +146,10 @@ class BoardTestContext {
     return Future(() => editorBloc);
   }
 
-  GridFieldContext singleSelectFieldContext() {
-    final fieldContext = fieldContexts
+  FieldInfo singleSelectFieldContext() {
+    final fieldInfo = fieldContexts
         .firstWhere((element) => element.fieldType == FieldType.SingleSelect);
-    return fieldContext;
+    return fieldInfo;
   }
 
   GridFieldCellContext singleSelectFieldCellContext() {
@@ -157,15 +157,15 @@ class BoardTestContext {
     return GridFieldCellContext(gridId: gridView.id, field: field);
   }
 
-  GridFieldContext textFieldContext() {
-    final fieldContext = fieldContexts
+  FieldInfo textFieldContext() {
+    final fieldInfo = fieldContexts
         .firstWhere((element) => element.fieldType == FieldType.RichText);
-    return fieldContext;
+    return fieldInfo;
   }
 
-  GridFieldContext checkboxFieldContext() {
-    final fieldContext = fieldContexts
+  FieldInfo checkboxFieldContext() {
+    final fieldInfo = fieldContexts
         .firstWhere((element) => element.fieldType == FieldType.Checkbox);
-    return fieldContext;
+    return fieldInfo;
   }
 }

+ 146 - 0
frontend/app_flowy/test/bloc_test/grid_test/create_filter_test.dart

@@ -0,0 +1,146 @@
+import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
+import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'util.dart';
+
+void main() {
+  late AppFlowyGridTest gridTest;
+  setUpAll(() async {
+    gridTest = await AppFlowyGridTest.ensureInitialized();
+  });
+
+  test('create a text filter)', () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(context.fieldController.filterInfos.length == 1);
+  });
+
+  test('delete a text filter)', () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    final filterInfo = context.fieldController.filterInfos.first;
+    service.deleteFilter(
+      fieldId: textField.id,
+      filterId: filterInfo.filter.id,
+      fieldType: textField.fieldType,
+    );
+    await gridResponseFuture();
+
+    assert(context.fieldController.filterInfos.isEmpty);
+  });
+
+  test('filter rows with condition: text is empty', () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+    await gridResponseFuture();
+
+    final textField = context.textFieldContext();
+    service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    assert(gridBloc.state.rowInfos.length == 3);
+  });
+
+  test('filter rows with condition: text is empty(After edit the row)',
+      () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+    await gridResponseFuture();
+
+    final textField = context.textFieldContext();
+    service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+
+    final controller = await context.makeTextCellController();
+    controller.saveCellData("edit text cell content");
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.length == 2);
+
+    controller.saveCellData("");
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.length == 3);
+  });
+
+  test('filter rows with condition: text is not empty', () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+    await gridResponseFuture();
+    service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsNotEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(context.rowInfos.isEmpty);
+  });
+
+  test('filter rows with condition: checkbox uncheck', () async {
+    final context = await gridTest.createTestGrid();
+    final checkboxField = context.checkboxFieldContext();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+
+    await gridResponseFuture();
+    service.insertCheckboxFilter(
+      fieldId: checkboxField.id,
+      condition: CheckboxFilterCondition.IsUnChecked,
+    );
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.length == 3);
+  });
+
+  test('filter rows with condition: checkbox check', () async {
+    final context = await gridTest.createTestGrid();
+    final checkboxField = context.checkboxFieldContext();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final gridController = GridController(view: context.gridView);
+    final gridBloc = GridBloc(
+      view: context.gridView,
+      gridController: gridController,
+    )..add(const GridEvent.initial());
+
+    await gridResponseFuture();
+    service.insertCheckboxFilter(
+      fieldId: checkboxField.id,
+      condition: CheckboxFilterCondition.IsChecked,
+    );
+    await gridResponseFuture();
+    assert(gridBloc.state.rowInfos.isEmpty);
+  });
+}

+ 56 - 0
frontend/app_flowy/test/bloc_test/grid_test/edit_field_change_filter_test.dart

@@ -0,0 +1,56 @@
+import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'util.dart';
+
+void main() {
+  late AppFlowyGridTest gridTest;
+  setUpAll(() async {
+    gridTest = await AppFlowyGridTest.ensureInitialized();
+  });
+
+  test("create a text filter and then alter the filter's field)", () async {
+    final context = await gridTest.createTestGrid();
+    final service = FilterFFIService(viewId: context.gridView.id);
+    final textField = context.textFieldContext();
+
+    // Create the filter menu bloc
+    final menuBloc = GridFilterMenuBloc(
+      fieldController: context.fieldController,
+      viewId: context.gridView.id,
+    )..add(const GridFilterMenuEvent.initial());
+
+    // Insert filter for the text field
+    service.insertTextFilter(
+        fieldId: textField.id,
+        condition: TextFilterCondition.TextIsEmpty,
+        content: "");
+    await gridResponseFuture();
+    assert(menuBloc.state.filters.length == 1);
+
+    // Edit the text field
+    final loader = FieldTypeOptionLoader(
+      gridId: context.gridView.id,
+      field: textField.field,
+    );
+
+    final editorBloc = FieldEditorBloc(
+      gridId: context.gridView.id,
+      fieldName: textField.field.name,
+      isGroupField: false,
+      loader: loader,
+    )..add(const FieldEditorEvent.initial());
+    await gridResponseFuture();
+
+    // Alter the field type to Number
+    editorBloc.add(const FieldEditorEvent.switchToField(FieldType.Number));
+    await gridResponseFuture();
+
+    // Check the number of filters
+    assert(menuBloc.state.filters.isEmpty);
+  });
+}

+ 8 - 8
frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart → frontend/app_flowy/test/bloc_test/grid_test/edit_field_edit_test.dart

@@ -7,16 +7,16 @@ import 'util.dart';
 
 Future<FieldEditorBloc> createEditorBloc(AppFlowyGridTest gridTest) async {
   final context = await gridTest.createTestGrid();
-  final fieldContext = context.singleSelectFieldContext();
+  final fieldInfo = context.singleSelectFieldContext();
   final loader = FieldTypeOptionLoader(
     gridId: context.gridView.id,
-    field: fieldContext.field,
+    field: fieldInfo.field,
   );
 
   return FieldEditorBloc(
     gridId: context.gridView.id,
-    fieldName: fieldContext.name,
-    isGroupField: fieldContext.isGroupField,
+    fieldName: fieldInfo.name,
+    isGroupField: fieldInfo.isGroupField,
     loader: loader,
   )..add(const FieldEditorEvent.initial());
 }
@@ -33,16 +33,16 @@ void main() {
 
     setUp(() async {
       final context = await gridTest.createTestGrid();
-      final fieldContext = context.singleSelectFieldContext();
+      final fieldInfo = context.singleSelectFieldContext();
       final loader = FieldTypeOptionLoader(
         gridId: context.gridView.id,
-        field: fieldContext.field,
+        field: fieldInfo.field,
       );
 
       editorBloc = FieldEditorBloc(
         gridId: context.gridView.id,
-        fieldName: fieldContext.name,
-        isGroupField: fieldContext.isGroupField,
+        fieldName: fieldInfo.name,
+        isGroupField: fieldInfo.isGroupField,
         loader: loader,
       )..add(const FieldEditorEvent.initial());
 

+ 0 - 144
frontend/app_flowy/test/bloc_test/grid_test/filter_bloc_test.dart

@@ -1,144 +0,0 @@
-import 'package:app_flowy/plugins/grid/application/filter/filter_bloc.dart';
-import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
-import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:bloc_test/bloc_test.dart';
-import 'util.dart';
-
-void main() {
-  late AppFlowyGridTest gridTest;
-  setUpAll(() async {
-    gridTest = await AppFlowyGridTest.ensureInitialized();
-  });
-
-  group('$GridFilterBloc', () {
-    late GridTestContext context;
-    setUp(() async {
-      context = await gridTest.createTestGrid();
-    });
-
-    blocTest<GridFilterBloc, GridFilterState>(
-      "create a text filter",
-      build: () => GridFilterBloc(viewId: context.gridView.id)
-        ..add(const GridFilterEvent.initial()),
-      act: (bloc) async {
-        final textField = context.textFieldContext();
-        bloc.add(
-          GridFilterEvent.createTextFilter(
-              fieldId: textField.id,
-              condition: TextFilterCondition.TextIsEmpty,
-              content: ""),
-        );
-      },
-      wait: const Duration(milliseconds: 300),
-      verify: (bloc) {
-        assert(bloc.state.filters.length == 1);
-      },
-    );
-
-    blocTest<GridFilterBloc, GridFilterState>(
-      "delete a text filter",
-      build: () => GridFilterBloc(viewId: context.gridView.id)
-        ..add(const GridFilterEvent.initial()),
-      act: (bloc) async {
-        final textField = context.textFieldContext();
-        bloc.add(
-          GridFilterEvent.createTextFilter(
-              fieldId: textField.id,
-              condition: TextFilterCondition.TextIsEmpty,
-              content: ""),
-        );
-        await gridResponseFuture();
-        final filter = bloc.state.filters.first;
-        bloc.add(
-          GridFilterEvent.deleteFilter(
-            fieldId: textField.id,
-            filterId: filter.id,
-            fieldType: textField.fieldType,
-          ),
-        );
-      },
-      wait: const Duration(milliseconds: 300),
-      verify: (bloc) {
-        assert(bloc.state.filters.isEmpty);
-      },
-    );
-  });
-
-  test('filter rows with condition: text is empty', () async {
-    final context = await gridTest.createTestGrid();
-    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
-      ..add(const GridFilterEvent.initial());
-
-    final gridBloc = GridBloc(view: context.gridView)
-      ..add(const GridEvent.initial());
-
-    final textField = context.textFieldContext();
-    await gridResponseFuture();
-    filterBloc.add(
-      GridFilterEvent.createTextFilter(
-          fieldId: textField.id,
-          condition: TextFilterCondition.TextIsEmpty,
-          content: ""),
-    );
-
-    await gridResponseFuture();
-    assert(gridBloc.state.rowInfos.length == 3);
-  });
-
-  test('filter rows with condition: text is not empty', () async {
-    final context = await gridTest.createTestGrid();
-    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
-      ..add(const GridFilterEvent.initial());
-
-    final textField = context.textFieldContext();
-    await gridResponseFuture();
-    filterBloc.add(
-      GridFilterEvent.createTextFilter(
-          fieldId: textField.id,
-          condition: TextFilterCondition.TextIsNotEmpty,
-          content: ""),
-    );
-    await gridResponseFuture();
-    assert(context.rowInfos.isEmpty);
-  });
-
-  test('filter rows with condition: checkbox uncheck', () async {
-    final context = await gridTest.createTestGrid();
-    final checkboxField = context.checkboxFieldContext();
-    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
-      ..add(const GridFilterEvent.initial());
-    final gridBloc = GridBloc(view: context.gridView)
-      ..add(const GridEvent.initial());
-
-    await gridResponseFuture();
-    filterBloc.add(
-      GridFilterEvent.createCheckboxFilter(
-        fieldId: checkboxField.id,
-        condition: CheckboxFilterCondition.IsUnChecked,
-      ),
-    );
-    await gridResponseFuture();
-    assert(gridBloc.state.rowInfos.length == 3);
-  });
-
-  test('filter rows with condition: checkbox check', () async {
-    final context = await gridTest.createTestGrid();
-    final checkboxField = context.checkboxFieldContext();
-    final filterBloc = GridFilterBloc(viewId: context.gridView.id)
-      ..add(const GridFilterEvent.initial());
-    final gridBloc = GridBloc(view: context.gridView)
-      ..add(const GridEvent.initial());
-
-    await gridResponseFuture();
-    filterBloc.add(
-      GridFilterEvent.createCheckboxFilter(
-        fieldId: checkboxField.id,
-        condition: CheckboxFilterCondition.IsChecked,
-      ),
-    );
-    await gridResponseFuture();
-    assert(gridBloc.state.rowInfos.isEmpty);
-  });
-}

+ 9 - 4
frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart

@@ -1,4 +1,5 @@
 import 'package:app_flowy/plugins/grid/application/grid_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:bloc_test/bloc_test.dart';
 import 'util.dart';
@@ -17,8 +18,10 @@ void main() {
     // The initial number of rows is 3 for each grid.
     blocTest<GridBloc, GridState>(
       "create a row",
-      build: () =>
-          GridBloc(view: context.gridView)..add(const GridEvent.initial()),
+      build: () => GridBloc(
+          view: context.gridView,
+          gridController: GridController(view: context.gridView))
+        ..add(const GridEvent.initial()),
       act: (bloc) => bloc.add(const GridEvent.createRow()),
       wait: const Duration(milliseconds: 300),
       verify: (bloc) {
@@ -28,8 +31,10 @@ void main() {
 
     blocTest<GridBloc, GridState>(
       "delete the last row",
-      build: () =>
-          GridBloc(view: context.gridView)..add(const GridEvent.initial()),
+      build: () => GridBloc(
+          view: context.gridView,
+          gridController: GridController(view: context.gridView))
+        ..add(const GridEvent.initial()),
       act: (bloc) async {
         await gridResponseFuture();
         bloc.add(GridEvent.deleteRow(bloc.state.rowInfos.last));

+ 45 - 33
frontend/app_flowy/test/bloc_test/grid_test/util.dart

@@ -18,42 +18,42 @@ import '../../util.dart';
 
 class GridTestContext {
   final ViewPB gridView;
-  final GridDataController _gridDataController;
+  final GridController _gridController;
 
-  GridTestContext(this.gridView, this._gridDataController);
+  GridTestContext(this.gridView, this._gridController);
 
   List<RowInfo> get rowInfos {
-    return _gridDataController.rowInfos;
+    return _gridController.rowInfos;
   }
 
   UnmodifiableMapView<String, GridBlockCache> get blocks {
-    return _gridDataController.blocks;
+    return _gridController.blocks;
   }
 
-  List<GridFieldContext> get fieldContexts => fieldController.fieldContexts;
+  List<FieldInfo> get fieldContexts => fieldController.fieldInfos;
 
   GridFieldController get fieldController {
-    return _gridDataController.fieldController;
+    return _gridController.fieldController;
   }
 
   Future<void> createRow() async {
-    return _gridDataController.createRow();
+    return _gridController.createRow();
   }
 
   FieldEditorBloc createFieldEditor({
-    GridFieldContext? fieldContext,
+    FieldInfo? fieldInfo,
   }) {
     IFieldTypeOptionLoader loader;
-    if (fieldContext == null) {
+    if (fieldInfo == null) {
       loader = NewFieldTypeOptionLoader(gridId: gridView.id);
     } else {
       loader =
-          FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field);
+          FieldTypeOptionLoader(gridId: gridView.id, field: fieldInfo.field);
     }
 
     final editorBloc = FieldEditorBloc(
-      fieldName: fieldContext?.name ?? '',
-      isGroupField: fieldContext?.isGroupField ?? false,
+      fieldName: fieldInfo?.name ?? '',
+      isGroupField: fieldInfo?.isGroupField ?? false,
       loader: loader,
       gridId: gridView.id,
     );
@@ -71,7 +71,7 @@ class GridTestContext {
     final RowInfo rowInfo = rowInfos.last;
     final blockCache = blocks[rowInfo.rowPB.blockId];
     final rowCache = blockCache?.rowCache;
-    final fieldController = _gridDataController.fieldController;
+    final fieldController = _gridController.fieldController;
 
     final rowDataController = GridRowDataController(
       rowInfo: rowInfo,
@@ -101,10 +101,10 @@ class GridTestContext {
     return Future(() => editorBloc);
   }
 
-  GridFieldContext singleSelectFieldContext() {
-    final fieldContext = fieldContexts
+  FieldInfo singleSelectFieldContext() {
+    final fieldInfo = fieldContexts
         .firstWhere((element) => element.fieldType == FieldType.SingleSelect);
-    return fieldContext;
+    return fieldInfo;
   }
 
   GridFieldCellContext singleSelectFieldCellContext() {
@@ -112,16 +112,36 @@ class GridTestContext {
     return GridFieldCellContext(gridId: gridView.id, field: field);
   }
 
-  GridFieldContext textFieldContext() {
-    final fieldContext = fieldContexts
+  FieldInfo textFieldContext() {
+    final fieldInfo = fieldContexts
         .firstWhere((element) => element.fieldType == FieldType.RichText);
-    return fieldContext;
+    return fieldInfo;
   }
 
-  GridFieldContext checkboxFieldContext() {
-    final fieldContext = fieldContexts
+  FieldInfo checkboxFieldContext() {
+    final fieldInfo = fieldContexts
         .firstWhere((element) => element.fieldType == FieldType.Checkbox);
-    return fieldContext;
+    return fieldInfo;
+  }
+
+  Future<GridSelectOptionCellController> makeSelectOptionCellController(
+      FieldType fieldType) async {
+    assert(fieldType == FieldType.SingleSelect ||
+        fieldType == FieldType.MultiSelect);
+
+    final field =
+        fieldContexts.firstWhere((element) => element.fieldType == fieldType);
+    final cellController =
+        await makeCellController(field.id) as GridSelectOptionCellController;
+    return cellController;
+  }
+
+  Future<GridCellController> makeTextCellController() async {
+    final field = fieldContexts
+        .firstWhere((element) => element.fieldType == FieldType.RichText);
+    final cellController =
+        await makeCellController(field.id) as GridCellController;
+    return cellController;
   }
 }
 
@@ -150,8 +170,8 @@ class AppFlowyGridTest {
         .then((result) {
       return result.fold(
         (view) async {
-          final context = GridTestContext(view, GridDataController(view: view));
-          final result = await context._gridDataController.openGrid();
+          final context = GridTestContext(view, GridController(view: view));
+          final result = await context._gridController.openGrid();
           result.fold((l) => null, (r) => throw Exception(r));
           return context;
         },
@@ -186,15 +206,7 @@ class AppFlowyGridCellTest {
 
   Future<GridSelectOptionCellController> makeCellController(
       FieldType fieldType) async {
-    assert(fieldType == FieldType.SingleSelect ||
-        fieldType == FieldType.MultiSelect);
-
-    final fieldContexts = context.fieldContexts;
-    final field =
-        fieldContexts.firstWhere((element) => element.fieldType == fieldType);
-    final cellController = await context.makeCellController(field.id)
-        as GridSelectOptionCellController;
-    return cellController;
+    return context.makeSelectOptionCellController(fieldType);
   }
 }
 

+ 1 - 1
frontend/rust-lib/Cargo.toml

@@ -21,7 +21,7 @@ members = [
 [profile.dev]
 opt-level = 0
 #https://doc.rust-lang.org/rustc/codegen-options/index.html#debug-assertions
-split-debuginfo = "unpacked"
+#split-debuginfo = "unpacked"
 
 [profile.release]
 opt-level = 3

+ 9 - 1
frontend/rust-lib/flowy-grid/src/entities/block_entities.rs

@@ -133,6 +133,14 @@ impl InsertedRowPB {
             is_new: false,
         }
     }
+
+    pub fn with_index(row: RowPB, index: i32) -> Self {
+        Self {
+            row,
+            index: Some(index),
+            is_new: false,
+        }
+    }
 }
 
 impl std::convert::From<RowPB> for InsertedRowPB {
@@ -167,7 +175,7 @@ pub struct GridBlockChangesetPB {
     pub updated_rows: Vec<RowPB>,
 
     #[pb(index = 5)]
-    pub visible_rows: Vec<String>,
+    pub visible_rows: Vec<InsertedRowPB>,
 
     #[pb(index = 6)]
     pub invisible_rows: Vec<String>,

+ 21 - 15
frontend/rust-lib/flowy-grid/src/entities/field_entities.rs

@@ -349,13 +349,13 @@ pub struct GetFieldPayloadPB {
     #[pb(index = 1)]
     pub grid_id: String,
 
-    #[pb(index = 2)]
-    pub field_ids: RepeatedFieldIdPB,
+    #[pb(index = 2, one_of)]
+    pub field_ids: Option<RepeatedFieldIdPB>,
 }
 
 pub struct GetFieldParams {
     pub grid_id: String,
-    pub field_ids: RepeatedFieldIdPB,
+    pub field_ids: Option<Vec<String>>,
 }
 
 impl TryInto<GetFieldParams> for GetFieldPayloadPB {
@@ -363,9 +363,17 @@ impl TryInto<GetFieldParams> for GetFieldPayloadPB {
 
     fn try_into(self) -> Result<GetFieldParams, Self::Error> {
         let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?;
+        let field_ids = self.field_ids.map(|repeated| {
+            repeated
+                .items
+                .into_iter()
+                .map(|item| item.field_id)
+                .collect::<Vec<String>>()
+        });
+
         Ok(GetFieldParams {
             grid_id: grid_id.0,
-            field_ids: self.field_ids,
+            field_ids,
         })
     }
 }
@@ -401,9 +409,8 @@ pub struct FieldChangesetPB {
 
     #[pb(index = 8, one_of)]
     pub width: Option<i32>,
-
-    #[pb(index = 9, one_of)]
-    pub type_option_data: Option<Vec<u8>>,
+    // #[pb(index = 9, one_of)]
+    // pub type_option_data: Option<Vec<u8>>,
 }
 
 impl TryInto<FieldChangesetParams> for FieldChangesetPB {
@@ -413,11 +420,11 @@ impl TryInto<FieldChangesetParams> for FieldChangesetPB {
         let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?;
         let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?;
         let field_type = self.field_type.map(FieldTypeRevision::from);
-        if let Some(type_option_data) = self.type_option_data.as_ref() {
-            if type_option_data.is_empty() {
-                return Err(ErrorCode::TypeOptionDataIsEmpty);
-            }
-        }
+        // if let Some(type_option_data) = self.type_option_data.as_ref() {
+        //     if type_option_data.is_empty() {
+        //         return Err(ErrorCode::TypeOptionDataIsEmpty);
+        //     }
+        // }
 
         Ok(FieldChangesetParams {
             field_id: field_id.0,
@@ -428,7 +435,7 @@ impl TryInto<FieldChangesetParams> for FieldChangesetPB {
             frozen: self.frozen,
             visibility: self.visibility,
             width: self.width,
-            type_option_data: self.type_option_data,
+            // type_option_data: self.type_option_data,
         })
     }
 }
@@ -450,8 +457,7 @@ pub struct FieldChangesetParams {
     pub visibility: Option<bool>,
 
     pub width: Option<i32>,
-
-    pub type_option_data: Option<Vec<u8>>,
+    // pub type_option_data: Option<Vec<u8>>,
 }
 /// Certain field types have user-defined options such as color, date format, number format,
 /// or a list of values for a multi-select list. These options are defined within a specialization

+ 23 - 0
frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs

@@ -11,6 +11,18 @@ pub struct FilterChangesetNotificationPB {
 
     #[pb(index = 3)]
     pub delete_filters: Vec<FilterPB>,
+
+    #[pb(index = 4)]
+    pub update_filters: Vec<UpdatedFilter>,
+}
+
+#[derive(Debug, Default, ProtoBuf)]
+pub struct UpdatedFilter {
+    #[pb(index = 1)]
+    pub filter_id: String,
+
+    #[pb(index = 2, one_of)]
+    pub filter: Option<FilterPB>,
 }
 
 impl FilterChangesetNotificationPB {
@@ -19,6 +31,7 @@ impl FilterChangesetNotificationPB {
             view_id: view_id.to_string(),
             insert_filters: filters,
             delete_filters: Default::default(),
+            update_filters: Default::default(),
         }
     }
     pub fn from_delete(view_id: &str, filters: Vec<FilterPB>) -> Self {
@@ -26,6 +39,16 @@ impl FilterChangesetNotificationPB {
             view_id: view_id.to_string(),
             insert_filters: Default::default(),
             delete_filters: filters,
+            update_filters: Default::default(),
+        }
+    }
+
+    pub fn from_update(view_id: &str, filters: Vec<UpdatedFilter>) -> Self {
+        Self {
+            view_id: view_id.to_string(),
+            insert_filters: Default::default(),
+            delete_filters: Default::default(),
+            update_filters: filters,
         }
     }
 }

+ 26 - 12
frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs

@@ -17,15 +17,18 @@ pub struct FilterPB {
     pub id: String,
 
     #[pb(index = 2)]
-    pub ty: FieldType,
+    pub field_id: String,
 
     #[pb(index = 3)]
+    pub field_type: FieldType,
+
+    #[pb(index = 4)]
     pub data: Vec<u8>,
 }
 
 impl std::convert::From<&FilterRevision> for FilterPB {
     fn from(rev: &FilterRevision) -> Self {
-        let field_type: FieldType = rev.field_type_rev.into();
+        let field_type: FieldType = rev.field_type.into();
         let bytes: Bytes = match field_type {
             FieldType::RichText => TextFilterPB::from(rev).try_into().unwrap(),
             FieldType::Number => NumberFilterPB::from(rev).try_into().unwrap(),
@@ -37,7 +40,8 @@ impl std::convert::From<&FilterRevision> for FilterPB {
         };
         Self {
             id: rev.id.clone(),
-            ty: rev.field_type_rev.into(),
+            field_id: rev.field_id.clone(),
+            field_type: rev.field_type.into(),
             data: bytes.to_vec(),
         }
     }
@@ -103,36 +107,44 @@ pub struct DeleteFilterParams {
 }
 
 #[derive(ProtoBuf, Debug, Default, Clone)]
-pub struct CreateFilterPayloadPB {
+pub struct AlterFilterPayloadPB {
     #[pb(index = 1)]
     pub field_id: String,
 
     #[pb(index = 2)]
     pub field_type: FieldType,
 
-    #[pb(index = 3)]
+    #[pb(index = 3, one_of)]
+    pub filter_id: Option<String>,
+
+    #[pb(index = 4)]
     pub data: Vec<u8>,
 }
 
-impl CreateFilterPayloadPB {
+impl AlterFilterPayloadPB {
     #[allow(dead_code)]
     pub fn new<T: TryInto<Bytes, Error = ::protobuf::ProtobufError>>(field_rev: &FieldRevision, data: T) -> Self {
         let data = data.try_into().unwrap_or_else(|_| Bytes::new());
         Self {
             field_id: field_rev.id.clone(),
             field_type: field_rev.ty.into(),
+            filter_id: None,
             data: data.to_vec(),
         }
     }
 }
 
-impl TryInto<CreateFilterParams> for CreateFilterPayloadPB {
+impl TryInto<AlterFilterParams> for AlterFilterPayloadPB {
     type Error = ErrorCode;
 
-    fn try_into(self) -> Result<CreateFilterParams, Self::Error> {
+    fn try_into(self) -> Result<AlterFilterParams, Self::Error> {
         let field_id = NotEmptyStr::parse(self.field_id)
             .map_err(|_| ErrorCode::FieldIdIsEmpty)?
             .0;
+        let filter_id = match self.filter_id {
+            None => None,
+            Some(filter_id) => Some(NotEmptyStr::parse(filter_id).map_err(|_| ErrorCode::FilterIdIsEmpty)?.0),
+        };
         let condition;
         let mut content = "".to_string();
         let bytes: &[u8] = self.data.as_ref();
@@ -169,9 +181,10 @@ impl TryInto<CreateFilterParams> for CreateFilterPayloadPB {
             }
         }
 
-        Ok(CreateFilterParams {
+        Ok(AlterFilterParams {
             field_id,
-            field_type_rev: self.field_type.into(),
+            filter_id,
+            field_type: self.field_type.into(),
             condition,
             content,
         })
@@ -179,9 +192,10 @@ impl TryInto<CreateFilterParams> for CreateFilterPayloadPB {
 }
 
 #[derive(Debug)]
-pub struct CreateFilterParams {
+pub struct AlterFilterParams {
     pub field_id: String,
-    pub field_type_rev: FieldTypeRevision,
+    pub filter_id: Option<String>,
+    pub field_type: FieldTypeRevision,
     pub condition: u8,
     pub content: String,
 }

+ 12 - 12
frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs

@@ -39,7 +39,7 @@ impl TryInto<CreateRowParams> for CreateBoardCardPayloadPB {
 }
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
-pub struct GridGroupConfigurationPB {
+pub struct GroupConfigurationPB {
     #[pb(index = 1)]
     pub id: String,
 
@@ -47,9 +47,9 @@ pub struct GridGroupConfigurationPB {
     pub field_id: String,
 }
 
-impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationPB {
+impl std::convert::From<&GroupConfigurationRevision> for GroupConfigurationPB {
     fn from(rev: &GroupConfigurationRevision) -> Self {
-        GridGroupConfigurationPB {
+        GroupConfigurationPB {
             id: rev.id.clone(),
             field_id: rev.field_id.clone(),
         }
@@ -57,19 +57,19 @@ impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationP
 }
 
 #[derive(ProtoBuf, Debug, Default, Clone)]
-pub struct RepeatedGridGroupPB {
+pub struct RepeatedGroupPB {
     #[pb(index = 1)]
     pub items: Vec<GroupPB>,
 }
 
-impl std::ops::Deref for RepeatedGridGroupPB {
+impl std::ops::Deref for RepeatedGroupPB {
     type Target = Vec<GroupPB>;
     fn deref(&self) -> &Self::Target {
         &self.items
     }
 }
 
-impl std::ops::DerefMut for RepeatedGridGroupPB {
+impl std::ops::DerefMut for RepeatedGroupPB {
     fn deref_mut(&mut self) -> &mut Self::Target {
         &mut self.items
     }
@@ -110,20 +110,20 @@ impl std::convert::From<Group> for GroupPB {
 }
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
-pub struct RepeatedGridGroupConfigurationPB {
+pub struct RepeatedGroupConfigurationPB {
     #[pb(index = 1)]
-    pub items: Vec<GridGroupConfigurationPB>,
+    pub items: Vec<GroupConfigurationPB>,
 }
 
-impl std::convert::From<Vec<GridGroupConfigurationPB>> for RepeatedGridGroupConfigurationPB {
-    fn from(items: Vec<GridGroupConfigurationPB>) -> Self {
+impl std::convert::From<Vec<GroupConfigurationPB>> for RepeatedGroupConfigurationPB {
+    fn from(items: Vec<GroupConfigurationPB>) -> Self {
         Self { items }
     }
 }
 
-impl std::convert::From<Vec<Arc<GroupConfigurationRevision>>> for RepeatedGridGroupConfigurationPB {
+impl std::convert::From<Vec<Arc<GroupConfigurationRevision>>> for RepeatedGroupConfigurationPB {
     fn from(revs: Vec<Arc<GroupConfigurationRevision>>) -> Self {
-        RepeatedGridGroupConfigurationPB {
+        RepeatedGroupConfigurationPB {
             items: revs.iter().map(|rev| rev.as_ref().into()).collect(),
         }
     }

+ 6 - 6
frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs

@@ -1,7 +1,7 @@
 use crate::entities::parser::NotEmptyStr;
 use crate::entities::{
-    CreateFilterParams, CreateFilterPayloadPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams,
-    DeleteGroupPayloadPB, InsertGroupParams, InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGridGroupConfigurationPB,
+    AlterFilterParams, AlterFilterPayloadPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams,
+    DeleteGroupPayloadPB, InsertGroupParams, InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGroupConfigurationPB,
 };
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
@@ -20,10 +20,10 @@ pub struct GridSettingPB {
     pub layout_type: GridLayout,
 
     #[pb(index = 3)]
-    pub filter_configurations: RepeatedFilterPB,
+    pub filters: RepeatedFilterPB,
 
     #[pb(index = 4)]
-    pub group_configurations: RepeatedGridGroupConfigurationPB,
+    pub group_configurations: RepeatedGroupConfigurationPB,
 }
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
@@ -83,7 +83,7 @@ pub struct GridSettingChangesetPB {
     pub layout_type: GridLayout,
 
     #[pb(index = 3, one_of)]
-    pub insert_filter: Option<CreateFilterPayloadPB>,
+    pub insert_filter: Option<AlterFilterPayloadPB>,
 
     #[pb(index = 4, one_of)]
     pub delete_filter: Option<DeleteFilterPayloadPB>,
@@ -137,7 +137,7 @@ impl TryInto<GridSettingChangesetParams> for GridSettingChangesetPB {
 pub struct GridSettingChangesetParams {
     pub grid_id: String,
     pub layout_type: LayoutRevision,
-    pub insert_filter: Option<CreateFilterParams>,
+    pub insert_filter: Option<AlterFilterParams>,
     pub delete_filter: Option<DeleteFilterParams>,
     pub insert_group: Option<InsertGroupParams>,
     pub delete_group: Option<DeleteGroupParams>,

+ 15 - 14
frontend/rust-lib/flowy-grid/src/event_handler.rs

@@ -51,8 +51,8 @@ pub(crate) async fn update_grid_setting_handler(
         let _ = editor.delete_group(delete_params).await?;
     }
 
-    if let Some(create_filter) = params.insert_filter {
-        let _ = editor.create_filter(create_filter).await?;
+    if let Some(alter_filter) = params.insert_filter {
+        let _ = editor.create_or_update_filter(alter_filter).await?;
     }
 
     if let Some(delete_filter) = params.delete_filter {
@@ -92,13 +92,7 @@ pub(crate) async fn get_fields_handler(
 ) -> DataResult<RepeatedFieldPB, FlowyError> {
     let params: GetFieldParams = data.into_inner().try_into()?;
     let editor = manager.get_grid_editor(&params.grid_id).await?;
-    let field_orders = params
-        .field_ids
-        .items
-        .into_iter()
-        .map(|field_order| field_order.field_id)
-        .collect();
-    let field_revs = editor.get_field_revs(Some(field_orders)).await?;
+    let field_revs = editor.get_field_revs(params.field_ids).await?;
     let repeated_field: RepeatedFieldPB = field_revs.into_iter().map(FieldPB::from).collect::<Vec<_>>().into();
     data_result(repeated_field)
 }
@@ -121,8 +115,14 @@ pub(crate) async fn update_field_type_option_handler(
 ) -> Result<(), FlowyError> {
     let params: TypeOptionChangesetParams = data.into_inner().try_into()?;
     let editor = manager.get_grid_editor(&params.grid_id).await?;
+    let old_field_rev = editor.get_field_rev(&params.field_id).await;
     let _ = editor
-        .update_field_type_option(&params.grid_id, &params.field_id, params.type_option_data)
+        .did_update_field_type_option(
+            &params.grid_id,
+            &params.field_id,
+            params.type_option_data,
+            old_field_rev,
+        )
         .await?;
     Ok(())
 }
@@ -145,20 +145,21 @@ pub(crate) async fn switch_to_field_handler(
 ) -> Result<(), FlowyError> {
     let params: EditFieldParams = data.into_inner().try_into()?;
     let editor = manager.get_grid_editor(&params.grid_id).await?;
+    let old_field_rev = editor.get_field_rev(&params.field_id).await;
     editor
         .switch_to_field_type(&params.field_id, &params.field_type)
         .await?;
 
     // Get the field_rev with field_id, if it doesn't exist, we create the default FieldRevision from the FieldType.
-    let field_rev = editor
+    let new_field_rev = editor
         .get_field_rev(&params.field_id)
         .await
         .unwrap_or(Arc::new(editor.next_field_rev(&params.field_type).await?));
 
     // Update the type-option data after the field type has been changed
-    let type_option_data = get_type_option_data(&field_rev, &params.field_type).await?;
+    let type_option_data = get_type_option_data(&new_field_rev, &params.field_type).await?;
     let _ = editor
-        .update_field_type_option(&params.grid_id, &field_rev.id, type_option_data)
+        .did_update_field_type_option(&params.grid_id, &new_field_rev.id, type_option_data, old_field_rev)
         .await?;
 
     Ok(())
@@ -462,7 +463,7 @@ pub(crate) async fn update_date_cell_handler(
 pub(crate) async fn get_groups_handler(
     data: Data<GridIdPB>,
     manager: AppData<Arc<GridManager>>,
-) -> DataResult<RepeatedGridGroupPB, FlowyError> {
+) -> DataResult<RepeatedGroupPB, FlowyError> {
     let params: GridIdPB = data.into_inner();
     let editor = manager.get_grid_editor(&params.value).await?;
     let group = editor.load_groups().await?;

+ 1 - 1
frontend/rust-lib/flowy-grid/src/event_map.rs

@@ -212,7 +212,7 @@ pub enum GridEvent {
     #[event(input = "DateChangesetPB")]
     UpdateDateCell = 80,
 
-    #[event(input = "GridIdPB", output = "RepeatedGridGroupPB")]
+    #[event(input = "GridIdPB", output = "RepeatedGroupPB")]
     GetGroup = 100,
 
     #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")]

+ 2 - 3
frontend/rust-lib/flowy-grid/src/services/block_editor.rs

@@ -109,9 +109,8 @@ impl GridBlockRevisionEditor {
         self.pad.read().await.index_of_row(row_id)
     }
 
-    pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult<Option<Arc<RowRevision>>> {
-        let row_ids = vec![Cow::Borrowed(row_id)];
-        let row_rev = self.get_row_revs(Some(row_ids)).await?.pop();
+    pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult<Option<(usize, Arc<RowRevision>)>> {
+        let row_rev = self.pad.read().await.get_row_rev(row_id);
         Ok(row_rev)
     }
 

+ 5 - 11
frontend/rust-lib/flowy-grid/src/services/block_manager.rs

@@ -110,8 +110,8 @@ impl GridBlockManager {
         let _ = editor.update_row(changeset.clone()).await?;
         match editor.get_row_rev(&changeset.row_id).await? {
             None => tracing::error!("Update row failed, can't find the row with id: {}", changeset.row_id),
-            Some(row_rev) => {
-                let row_pb = make_row_from_row_rev(row_rev.clone());
+            Some((_, row_rev)) => {
+                let row_pb = make_row_from_row_rev(row_rev);
                 let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_pb]);
                 let _ = self
                     .notify_did_update_block(&editor.block_id, block_order_changeset)
@@ -128,7 +128,7 @@ impl GridBlockManager {
         let editor = self.get_block_editor(&block_id).await?;
         match editor.get_row_rev(&row_id).await? {
             None => Ok(None),
-            Some(row_rev) => {
+            Some((_, row_rev)) => {
                 let _ = editor.delete_rows(vec![Cow::Borrowed(&row_id)]).await?;
                 let _ = self
                     .notify_did_update_block(
@@ -198,15 +198,9 @@ impl GridBlockManager {
         Ok(())
     }
 
-    pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult<Option<Arc<RowRevision>>> {
+    pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult<Option<(usize, Arc<RowRevision>)>> {
         let editor = self.get_editor_from_row_id(row_id).await?;
-        let row_ids = vec![Cow::Borrowed(row_id)];
-        let mut row_revs = editor.get_row_revs(Some(row_ids)).await?;
-        if row_revs.is_empty() {
-            Ok(None)
-        } else {
-            Ok(row_revs.pop())
-        }
+        editor.get_row_rev(row_id).await
     }
 
     pub async fn get_row_revs(&self, block_id: &str) -> FlowyResult<Vec<Arc<RowRevision>>> {

+ 3 - 1
frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs

@@ -18,10 +18,12 @@ where
     };
 
     if let Some(mut type_option) = get_type_option.await {
+        let old_field_rev = editor.get_field_rev(field_id).await;
+
         action(&mut type_option);
         let bytes = type_option.protobuf_bytes().to_vec();
         let _ = editor
-            .update_field_type_option(&editor.grid_id, field_id, bytes)
+            .did_update_field_type_option(&editor.grid_id, field_id, bytes, old_field_rev)
             .await?;
     }
 

部分文件因文件數量過多而無法顯示