Ver código fonte

Feat/sort UI (#1642)

* feat: implement sort UI

* chore: config sort listener

* chore: config sort ui

* chore: config sort ui

* feat: support ascending & descending

* fix: reorder rows bugs

* chore: add tests

Co-authored-by: nathan <[email protected]>
Nathan.fooo 2 anos atrás
pai
commit
b7ba189642
77 arquivos alterados com 2803 adições e 411 exclusões
  1. 7 0
      frontend/app_flowy/assets/translations/en.json
  2. 1 0
      frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart
  3. 313 69
      frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart
  4. 0 2
      frontend/app_flowy/lib/plugins/grid/application/filter/filter_service.dart
  5. 47 0
      frontend/app_flowy/lib/plugins/grid/application/grid_accessory_bloc.dart
  6. 22 0
      frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart
  7. 23 2
      frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart
  8. 132 0
      frontend/app_flowy/lib/plugins/grid/application/sort/sort_create_bloc.dart
  9. 128 0
      frontend/app_flowy/lib/plugins/grid/application/sort/sort_editor_bloc.dart
  10. 51 0
      frontend/app_flowy/lib/plugins/grid/application/sort/sort_listener.dart
  11. 113 0
      frontend/app_flowy/lib/plugins/grid/application/sort/sort_menu_bloc.dart
  12. 116 0
      frontend/app_flowy/lib/plugins/grid/application/sort/sort_service.dart
  13. 7 0
      frontend/app_flowy/lib/plugins/grid/application/sort/util.dart
  14. 12 0
      frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart
  15. 38 7
      frontend/app_flowy/lib/plugins/grid/application/view/grid_view_listener.dart
  16. 23 3
      frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart
  17. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/layout/sizes.dart
  18. 89 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/accessory_menu.dart
  19. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_cell_editor.dart
  20. 5 5
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart
  21. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart
  22. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart
  23. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart
  24. 25 56
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_menu.dart
  25. 0 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_menu_item.dart
  26. 2 2
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart
  27. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart
  28. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart
  29. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart
  30. 5 5
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart
  31. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart
  32. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart
  33. 3 3
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart
  34. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart
  35. 168 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/create_sort_list.dart
  36. 46 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/order_panel.dart
  37. 51 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_choice_button.dart
  38. 293 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_editor.dart
  39. 11 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_info.dart
  40. 77 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_menu.dart
  41. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart
  42. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart
  43. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart
  44. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart
  45. 2 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart
  46. 1 1
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart
  47. 80 0
      frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/sort_button.dart
  48. 20 27
      frontend/app_flowy/packages/flowy_infra/pubspec.lock
  49. 29 36
      frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock
  50. 13 20
      frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock
  51. 13 20
      frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock
  52. 1 1
      frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart
  53. 21 13
      frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart
  54. 28 35
      frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock
  55. 490 0
      frontend/app_flowy/packages/flowy_sdk/pubspec.lock
  56. 3 0
      frontend/rust-lib/flowy-error/src/code.rs
  57. 4 1
      frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs
  58. 42 8
      frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs
  59. 24 0
      frontend/rust-lib/flowy-grid/src/event_handler.rs
  60. 8 0
      frontend/rust-lib/flowy-grid/src/event_map.rs
  61. 9 5
      frontend/rust-lib/flowy-grid/src/manager.rs
  62. 10 12
      frontend/rust-lib/flowy-grid/src/services/field/type_options/type_option_cell.rs
  63. 1 1
      frontend/rust-lib/flowy-grid/src/services/filter/controller.rs
  64. 22 8
      frontend/rust-lib/flowy-grid/src/services/grid_editor.rs
  65. 16 9
      frontend/rust-lib/flowy-grid/src/services/sort/controller.rs
  66. 1 0
      frontend/rust-lib/flowy-grid/src/services/sort/entities.rs
  67. 1 0
      frontend/rust-lib/flowy-grid/src/services/view_editor/changed_notifier.rs
  68. 54 29
      frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs
  69. 10 0
      frontend/rust-lib/flowy-grid/src/services/view_editor/editor_manager.rs
  70. 4 2
      frontend/rust-lib/flowy-grid/src/services/view_editor/trait_impl.rs
  71. 1 3
      frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs
  72. 50 0
      frontend/rust-lib/flowy-grid/tests/grid/sort_test/checkbox_and_text_test.rs
  73. 1 0
      frontend/rust-lib/flowy-grid/tests/grid/sort_test/mod.rs
  74. 1 1
      frontend/rust-lib/flowy-revision/src/rev_manager.rs
  75. 11 4
      frontend/rust-lib/flowy-sync/src/client_grid/view_revision_pad.rs
  76. 2 0
      frontend/rust-lib/flowy-task/src/scheduler.rs
  77. 1 0
      frontend/rust-lib/flowy-task/src/task.rs

+ 7 - 0
frontend/app_flowy/assets/translations/en.json

@@ -193,6 +193,7 @@
   "grid": {
     "settings": {
       "filter": "Filter",
+      "sort": "Sort",
       "sortBy": "Sort by",
       "Properties": "Properties",
       "group": "Group",
@@ -273,6 +274,12 @@
       "newColumn": "New column",
       "deleteFieldPromptMessage": "Are you sure? This property will be deleted"
     },
+    "sort": {
+      "ascending": "Ascending",
+      "descending": "Descending",
+      "deleteSort": "Delete sort",
+      "addSort": "Add sort"
+    },
     "row": {
       "duplicate": "Duplicate",
       "delete": "Delete",

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

@@ -132,6 +132,7 @@ class BoardDataController {
   }
 
   Future<void> dispose() async {
+    await _viewCache.dispose();
     await _gridFFIService.closeGrid();
     await fieldController.dispose();
   }

+ 313 - 69
frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart

@@ -5,7 +5,12 @@ 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/application/sort/sort_listener.dart';
+import 'package:app_flowy/plugins/grid/application/sort/sort_service.dart';
 import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/sort/sort_info.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/filter_changeset.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:appflowy_backend/log.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -46,21 +51,39 @@ class _GridFilterNotifier extends ChangeNotifier {
   List<FilterInfo> get filters => _filters;
 }
 
+class _GridSortNotifier extends ChangeNotifier {
+  List<SortInfo> _sorts = [];
+
+  set sorts(List<SortInfo> sorts) {
+    _sorts = sorts;
+    notifyListeners();
+  }
+
+  void notify() {
+    notifyListeners();
+  }
+
+  List<SortInfo> get sorts => _sorts;
+}
+
 typedef OnReceiveUpdateFields = void Function(List<FieldInfo>);
 typedef OnReceiveFields = void Function(List<FieldInfo>);
 typedef OnReceiveFilters = void Function(List<FilterInfo>);
+typedef OnReceiveSorts = void Function(List<SortInfo>);
 
 class GridFieldController {
   final String gridId;
   // Listeners
   final GridFieldsListener _fieldListener;
   final SettingListener _settingListener;
-  final FiltersListener _filterListener;
+  final FiltersListener _filtersListener;
+  final SortsListener _sortsListener;
 
   // FFI services
   final GridFFIService _gridFFIService;
   final SettingFFIService _settingFFIService;
   final FilterFFIService _filterFFIService;
+  final SortFFIService _sortFFIService;
 
   // Field callbacks
   final Map<OnReceiveFields, VoidCallback> _fieldCallbacks = {};
@@ -78,9 +101,16 @@ class GridFieldController {
   _GridFilterNotifier? _filterNotifier = _GridFilterNotifier();
   final Map<String, FilterPB> _filterPBByFieldId = {};
 
+  // Sort callbacks
+  final Map<OnReceiveSorts, VoidCallback> _sortCallbacks = {};
+  _GridSortNotifier? _sortNotifier = _GridSortNotifier();
+  final Map<String, SortPB> _sortPBByFieldId = {};
+
   // Getters
   List<FieldInfo> get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []];
   List<FilterInfo> get filterInfos => [..._filterNotifier?.filters ?? []];
+  List<SortInfo> get sortInfos => [..._sortNotifier?.sorts ?? []];
+
   FieldInfo? getField(String fieldId) {
     final fields = _fieldNotifier?.fieldInfos
             .where((element) => element.id == fieldId)
@@ -105,12 +135,26 @@ class GridFieldController {
     return filters.first;
   }
 
+  SortInfo? getSort(String sortId) {
+    final sorts = _sortNotifier?.sorts
+            .where((element) => element.sortId == sortId)
+            .toList() ??
+        [];
+    if (sorts.isEmpty) {
+      return null;
+    }
+    assert(sorts.length == 1);
+    return sorts.first;
+  }
+
   GridFieldController({required this.gridId})
       : _fieldListener = GridFieldsListener(gridId: gridId),
         _settingListener = SettingListener(gridId: gridId),
-        _filterListener = FiltersListener(viewId: gridId),
-        _gridFFIService = GridFFIService(gridId: gridId),
         _filterFFIService = FilterFFIService(viewId: gridId),
+        _filtersListener = FiltersListener(viewId: gridId),
+        _gridFFIService = GridFFIService(gridId: gridId),
+        _sortFFIService = SortFFIService(viewId: gridId),
+        _sortsListener = SortsListener(viewId: gridId),
         _settingFFIService = SettingFFIService(viewId: gridId) {
     //Listen on field's changes
     _listenOnFieldChanges();
@@ -121,9 +165,12 @@ class GridFieldController {
     //Listen on the fitler changes
     _listenOnFilterChanges();
 
+    //Listen on the sort changes
+    _listenOnSortChanged();
+
     _settingFFIService.getSetting().then((result) {
       result.fold(
-        (setting) => _updateSettingConfiguration(setting),
+        (setting) => _updateSetting(setting),
         (err) => Log.error(err),
       );
     });
@@ -131,70 +178,185 @@ class GridFieldController {
 
   void _listenOnFilterChanges() {
     //Listen on the fitler changes
-    _filterListener.start(onFilterChanged: (result) {
+
+    deleteFilterFromChangeset(
+      List<FilterInfo> filters,
+      FilterChangesetNotificationPB changeset,
+    ) {
+      final deleteFilterIds = changeset.deleteFilters.map((e) => e.id).toList();
+      if (deleteFilterIds.isNotEmpty) {
+        filters.retainWhere(
+          (element) => !deleteFilterIds.contains(element.filter.id),
+        );
+
+        _filterPBByFieldId
+            .removeWhere((key, value) => deleteFilterIds.contains(value.id));
+      }
+    }
+
+    insertFilterFromChangeset(
+      List<FilterInfo> filters,
+      FilterChangesetNotificationPB changeset,
+    ) {
+      for (final newFilter in changeset.insertFilters) {
+        final filterIndex =
+            filters.indexWhere((element) => element.filter.id == newFilter.id);
+        if (filterIndex == -1) {
+          final fieldInfo = _findFieldInfo(
+            fieldInfos: fieldInfos,
+            fieldId: newFilter.fieldId,
+            fieldType: newFilter.fieldType,
+          );
+          if (fieldInfo != null) {
+            _filterPBByFieldId[fieldInfo.id] = newFilter;
+            filters.add(FilterInfo(gridId, newFilter, fieldInfo));
+          }
+        }
+      }
+    }
+
+    updateFilterFromChangeset(
+      List<FilterInfo> filters,
+      FilterChangesetNotificationPB changeset,
+    ) {
+      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 = _findFieldInfo(
+            fieldInfos: fieldInfos,
+            fieldId: updatedFilter.filter.fieldId,
+            fieldType: updatedFilter.filter.fieldType,
+          );
+
+          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;
+          }
+        }
+      }
+    }
+
+    _filtersListener.start(onFilterChanged: (result) {
       result.fold(
-        (changeset) {
+        (FilterChangesetNotificationPB 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),
-            );
-
-            _filterPBByFieldId.removeWhere(
-                (key, value) => deleteFilterIds.contains(value.id));
-          }
+          deleteFilterFromChangeset(filters, changeset);
 
           // 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) {
-                _filterPBByFieldId[fieldInfo.id] = newFilter;
-                filters.add(FilterInfo(gridId, newFilter, fieldInfo));
-              }
-            }
+          insertFilterFromChangeset(filters, changeset);
+
+          updateFilterFromChangeset(filters, changeset);
+
+          _updateFieldInfos();
+          _filterNotifier?.filters = filters;
+        },
+        (err) => Log.error(err),
+      );
+    });
+  }
+
+  void _listenOnSortChanged() {
+    deleteSortFromChangeset(
+      List<SortInfo> newSortInfos,
+      SortChangesetNotificationPB changeset,
+    ) {
+      final deleteSortIds = changeset.deleteSorts.map((e) => e.id).toList();
+      if (deleteSortIds.isNotEmpty) {
+        newSortInfos.retainWhere(
+          (element) => !deleteSortIds.contains(element.sortId),
+        );
+
+        _sortPBByFieldId
+            .removeWhere((key, value) => deleteSortIds.contains(value.id));
+      }
+    }
+
+    insertSortFromChangeset(
+      List<SortInfo> newSortInfos,
+      SortChangesetNotificationPB changeset,
+    ) {
+      for (final newSortPB in changeset.insertSorts) {
+        final sortIndex = newSortInfos
+            .indexWhere((element) => element.sortId == newSortPB.id);
+        if (sortIndex == -1) {
+          final fieldInfo = _findFieldInfo(
+            fieldInfos: fieldInfos,
+            fieldId: newSortPB.fieldId,
+            fieldType: newSortPB.fieldType,
+          );
+
+          if (fieldInfo != null) {
+            _sortPBByFieldId[newSortPB.fieldId] = newSortPB;
+            newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: 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);
-            }
+    updateSortFromChangeset(
+      List<SortInfo> newSortInfos,
+      SortChangesetNotificationPB changeset,
+    ) {
+      for (final updatedSort in changeset.updateSorts) {
+        final sortIndex = newSortInfos.indexWhere(
+          (element) => element.sortId == updatedSort.id,
+        );
+        // Remove the old filter
+        if (sortIndex != -1) {
+          newSortInfos.removeAt(sortIndex);
+        }
 
-            // 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;
-              }
-            }
+        final fieldInfo = _findFieldInfo(
+          fieldInfos: fieldInfos,
+          fieldId: updatedSort.fieldId,
+          fieldType: updatedSort.fieldType,
+        );
+
+        if (fieldInfo != null) {
+          final newSortInfo = SortInfo(
+            sortPB: updatedSort,
+            fieldInfo: fieldInfo,
+          );
+          if (sortIndex != -1) {
+            newSortInfos.insert(sortIndex, newSortInfo);
+          } else {
+            newSortInfos.add(newSortInfo);
           }
+          _sortPBByFieldId[updatedSort.fieldId] = updatedSort;
+        }
+      }
+    }
+
+    _sortsListener.start(onSortChanged: (result) {
+      result.fold(
+        (SortChangesetNotificationPB changeset) {
+          final List<SortInfo> newSortInfos = sortInfos;
+          deleteSortFromChangeset(newSortInfos, changeset);
+          insertSortFromChangeset(newSortInfos, changeset);
+          updateSortFromChangeset(newSortInfos, changeset);
+
           _updateFieldInfos();
-          _filterNotifier?.filters = filters;
+          _sortNotifier?.sorts = newSortInfos;
         },
         (err) => Log.error(err),
       );
@@ -205,7 +367,7 @@ class GridFieldController {
     //Listen on setting changes
     _settingListener.start(onSettingUpdated: (result) {
       result.fold(
-        (setting) => _updateSettingConfiguration(setting),
+        (setting) => _updateSetting(setting),
         (r) => Log.error(r),
       );
     });
@@ -229,14 +391,18 @@ class GridFieldController {
     });
   }
 
-  void _updateSettingConfiguration(GridSettingPB setting) {
+  void _updateSetting(GridSettingPB setting) {
     _groupConfigurationByFieldId.clear();
     for (final configuration in setting.groupConfigurations.items) {
       _groupConfigurationByFieldId[configuration.fieldId] = configuration;
     }
 
-    for (final configuration in setting.filters.items) {
-      _filterPBByFieldId[configuration.fieldId] = configuration;
+    for (final filter in setting.filters.items) {
+      _filterPBByFieldId[filter.fieldId] = filter;
+    }
+
+    for (final sort in setting.sorts.items) {
+      _sortPBByFieldId[sort.fieldId] = sort;
     }
 
     _updateFieldInfos();
@@ -247,6 +413,7 @@ class GridFieldController {
       for (var field in _fieldNotifier!.fieldInfos) {
         field._isGroupField = _groupConfigurationByFieldId[field.id] != null;
         field._hasFilter = _filterPBByFieldId[field.id] != null;
+        field._hasSort = _sortPBByFieldId[field.id] != null;
       }
       _fieldNotifier?.notify();
     }
@@ -254,8 +421,9 @@ class GridFieldController {
 
   Future<void> dispose() async {
     await _fieldListener.stop();
-    await _filterListener.stop();
+    await _filtersListener.stop();
     await _settingListener.stop();
+    await _sortsListener.stop();
 
     for (final callback in _fieldCallbacks.values) {
       _fieldNotifier?.removeListener(callback);
@@ -266,8 +434,15 @@ class GridFieldController {
     for (final callback in _filterCallbacks.values) {
       _filterNotifier?.removeListener(callback);
     }
+    for (final callback in _sortCallbacks.values) {
+      _sortNotifier?.removeListener(callback);
+    }
+
     _filterNotifier?.dispose();
     _filterNotifier = null;
+
+    _sortNotifier?.dispose();
+    _sortNotifier = null;
   }
 
   Future<Either<Unit, FlowyError>> loadFields({
@@ -280,6 +455,7 @@ class GridFieldController {
           _fieldNotifier?.fieldInfos =
               newFields.map((field) => FieldInfo(field: field)).toList();
           _loadFilters();
+          _loadSorts();
           _updateFieldInfos();
           return left(unit);
         },
@@ -294,14 +470,17 @@ class GridFieldController {
         (filterPBs) {
           final List<FilterInfo> filters = [];
           for (final filterPB in filterPBs) {
-            final fieldInfo = _findFieldInfoForFilter(fieldInfos, filterPB);
+            final fieldInfo = _findFieldInfo(
+              fieldInfos: fieldInfos,
+              fieldId: filterPB.fieldId,
+              fieldType: filterPB.fieldType,
+            );
             if (fieldInfo != null) {
               final filterInfo = FilterInfo(gridId, filterPB, fieldInfo);
               filters.add(filterInfo);
             }
           }
 
-          _updateFieldInfos();
           _filterNotifier?.filters = filters;
           return left(unit);
         },
@@ -310,10 +489,38 @@ class GridFieldController {
     });
   }
 
+  Future<Either<Unit, FlowyError>> _loadSorts() async {
+    return _sortFFIService.getAllSorts().then((result) {
+      return result.fold(
+        (sortPBs) {
+          final List<SortInfo> sortInfos = [];
+          for (final sortPB in sortPBs) {
+            final fieldInfo = _findFieldInfo(
+              fieldInfos: fieldInfos,
+              fieldId: sortPB.fieldId,
+              fieldType: sortPB.fieldType,
+            );
+
+            if (fieldInfo != null) {
+              final sortInfo = SortInfo(sortPB: sortPB, fieldInfo: fieldInfo);
+              sortInfos.add(sortInfo);
+            }
+          }
+
+          _updateFieldInfos();
+          _sortNotifier?.sorts = sortInfos;
+          return left(unit);
+        },
+        (err) => right(err),
+      );
+    });
+  }
+
   void addListener({
     OnReceiveFields? onFields,
     OnReceiveUpdateFields? onFieldsUpdated,
     OnReceiveFilters? onFilters,
+    OnReceiveSorts? onSorts,
     bool Function()? listenWhen,
   }) {
     if (onFieldsUpdated != null) {
@@ -350,10 +557,23 @@ class GridFieldController {
       _filterCallbacks[onFilters] = callback;
       _filterNotifier?.addListener(callback);
     }
+
+    if (onSorts != null) {
+      callback() {
+        if (listenWhen != null && listenWhen() == false) {
+          return;
+        }
+        onSorts(sortInfos);
+      }
+
+      _sortCallbacks[onSorts] = callback;
+      _sortNotifier?.addListener(callback);
+    }
   }
 
   void removeListener({
     OnReceiveFields? onFieldsListener,
+    OnReceiveSorts? onSortsListener,
     OnReceiveFilters? onFiltersListener,
     OnReceiveUpdateFields? onChangesetListener,
   }) {
@@ -369,6 +589,13 @@ class GridFieldController {
         _filterNotifier?.removeListener(callback);
       }
     }
+
+    if (onSortsListener != null) {
+      final callback = _sortCallbacks.remove(onSortsListener);
+      if (callback != null) {
+        _sortNotifier?.removeListener(callback);
+      }
+    }
   }
 
   void _deleteFields(List<FieldIdPB> deletedFields) {
@@ -466,11 +693,13 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier {
   }
 }
 
-FieldInfo? _findFieldInfoForFilter(
-    List<FieldInfo> fieldInfos, FilterPB filter) {
+FieldInfo? _findFieldInfo({
+  required List<FieldInfo> fieldInfos,
+  required String fieldId,
+  required FieldType fieldType,
+}) {
   final fieldIndex = fieldInfos.indexWhere((element) {
-    return element.id == filter.fieldId &&
-        element.fieldType == filter.fieldType;
+    return element.id == fieldId && element.fieldType == fieldType;
   });
   if (fieldIndex != -1) {
     return fieldInfos[fieldIndex];
@@ -485,6 +714,8 @@ class FieldInfo {
 
   bool _hasFilter = false;
 
+  bool _hasSort = false;
+
   String get id => _field.id;
 
   FieldType get fieldType => _field.fieldType;
@@ -529,5 +760,18 @@ class FieldInfo {
     }
   }
 
+  bool get canCreateSort {
+    if (_hasSort) return false;
+
+    switch (_field.fieldType) {
+      case FieldType.RichText:
+      case FieldType.Checkbox:
+      case FieldType.Number:
+        return true;
+      default:
+        return false;
+    }
+  }
+
   FieldInfo({required FieldPB field}) : _field = field;
 }

+ 0 - 2
frontend/app_flowy/lib/plugins/grid/application/filter/filter_service.dart

@@ -197,8 +197,6 @@ class FilterFFIService {
     required String filterId,
     required FieldType fieldType,
   }) {
-    TextFilterConditionPB.DoesNotContain.value;
-
     final deleteFilterPayload = DeleteFilterPayloadPB.create()
       ..fieldId = fieldId
       ..filterId = filterId

+ 47 - 0
frontend/app_flowy/lib/plugins/grid/application/grid_accessory_bloc.dart

@@ -0,0 +1,47 @@
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'grid_accessory_bloc.freezed.dart';
+
+class GridAccessoryMenuBloc
+    extends Bloc<GridAccessoryMenuEvent, GridAccessoryMenuState> {
+  final String viewId;
+
+  GridAccessoryMenuBloc({required this.viewId})
+      : super(GridAccessoryMenuState.initial(
+          viewId,
+        )) {
+    on<GridAccessoryMenuEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {},
+          toggleMenu: () {
+            emit(state.copyWith(isVisible: !state.isVisible));
+          },
+        );
+      },
+    );
+  }
+}
+
+@freezed
+class GridAccessoryMenuEvent with _$GridAccessoryMenuEvent {
+  const factory GridAccessoryMenuEvent.initial() = _Initial;
+  const factory GridAccessoryMenuEvent.toggleMenu() = _MenuVisibleChange;
+}
+
+@freezed
+class GridAccessoryMenuState with _$GridAccessoryMenuState {
+  const factory GridAccessoryMenuState({
+    required String viewId,
+    required bool isVisible,
+  }) = _GridAccessoryMenuState;
+
+  factory GridAccessoryMenuState.initial(
+    String viewId,
+  ) =>
+      GridAccessoryMenuState(
+        viewId: viewId,
+        isVisible: false,
+      );
+}

+ 22 - 0
frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart

@@ -81,6 +81,23 @@ class GridRowCache {
     _showRows(changeset.visibleRows);
   }
 
+  void reorderAllRows(List<String> rowIds) {
+    _rowList.reorderWithRowIds(rowIds);
+    _rowChangeReasonNotifier.receive(const RowsChangedReason.reorderRows());
+  }
+
+  void reorderSingleRow(ReorderSingleRowPB reorderRow) {
+    final rowInfo = _rowList.get(reorderRow.rowId);
+    if (rowInfo != null) {
+      _rowList.moveRow(
+          reorderRow.rowId, reorderRow.oldIndex, reorderRow.newIndex);
+      _rowChangeReasonNotifier.receive(RowsChangedReason.reorderSingleRow(
+        reorderRow,
+        rowInfo,
+      ));
+    }
+  }
+
   void _deleteRows(List<String> deletedRowIds) {
     for (final rowId in deletedRowIds) {
       final deletedRow = _rowList.remove(rowId);
@@ -266,6 +283,8 @@ class _RowChangesetNotifier extends ChangeNotifier {
       update: (_) => notifyListeners(),
       fieldDidChange: (_) => notifyListeners(),
       initial: (_) {},
+      reorderRows: (_) => notifyListeners(),
+      reorderSingleRow: (_) => notifyListeners(),
     );
   }
 }
@@ -292,6 +311,9 @@ class RowsChangedReason with _$RowsChangedReason {
   const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update;
   const factory RowsChangedReason.fieldDidChange() = _FieldDidChange;
   const factory RowsChangedReason.initial() = InitialListState;
+  const factory RowsChangedReason.reorderRows() = _ReorderRows;
+  const factory RowsChangedReason.reorderSingleRow(
+      ReorderSingleRowPB reorderRow, RowInfo rowInfo) = _ReorderSingleRow;
 }
 
 class InsertedIndex {

+ 23 - 2
frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart

@@ -5,8 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-grid/row_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.
+  /// Use List to reverse the order of the row.
   List<RowInfo> _rowInfos = [];
 
   List<RowInfo> get rows => List.from(_rowInfos);
@@ -150,6 +149,28 @@ class RowList {
     return deletedRows;
   }
 
+  void reorderWithRowIds(List<String> rowIds) {
+    _rowInfos.clear();
+
+    for (final rowId in rowIds) {
+      final rowInfo = _rowInfoByRowId[rowId];
+      if (rowInfo != null) {
+        _rowInfos.add(rowInfo);
+      }
+    }
+  }
+
+  void moveRow(String rowId, int oldIndex, int newIndex) {
+    final index = _rowInfos.indexWhere(
+      (rowInfo) => rowInfo.rowPB.id == rowId,
+    );
+    if (index != -1) {
+      assert(index == oldIndex);
+      final rowInfo = remove(rowId)!.rowInfo;
+      insert(newIndex, rowInfo);
+    }
+  }
+
   bool contains(String rowId) {
     return _rowInfoByRowId[rowId] != null;
   }

+ 132 - 0
frontend/app_flowy/lib/plugins/grid/application/sort/sort_create_bloc.dart

@@ -0,0 +1,132 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:dartz/dartz.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pbserver.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pbserver.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+import 'sort_service.dart';
+import 'util.dart';
+
+part 'sort_create_bloc.freezed.dart';
+
+class CreateSortBloc extends Bloc<CreateSortEvent, CreateSortState> {
+  final String viewId;
+  final SortFFIService _ffiService;
+  final GridFieldController fieldController;
+  void Function(List<FieldInfo>)? _onFieldFn;
+  CreateSortBloc({required this.viewId, required this.fieldController})
+      : _ffiService = SortFFIService(viewId: viewId),
+        super(CreateSortState.initial(fieldController.fieldInfos)) {
+    on<CreateSortEvent>(
+      (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),
+              ),
+            );
+          },
+          createDefaultSort: (FieldInfo field) {
+            emit(state.copyWith(didCreateSort: true));
+            _createDefaultSort(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.canCreateSort) {
+        return false;
+      }
+
+      if (filterText.isNotEmpty) {
+        return field.name.toLowerCase().contains(keyword);
+      }
+
+      return true;
+    });
+
+    return allFields;
+  }
+
+  void _startListening() {
+    _onFieldFn = (fields) {
+      fields.retainWhere((field) => field.canCreateSort);
+      add(CreateSortEvent.didReceiveFields(fields));
+    };
+    fieldController.addListener(onFields: _onFieldFn);
+  }
+
+  Future<Either<Unit, FlowyError>> _createDefaultSort(FieldInfo field) async {
+    final result = await _ffiService.insertSort(
+        fieldId: field.id,
+        fieldType: field.fieldType,
+        condition: GridSortConditionPB.Ascending);
+
+    return result;
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onFieldFn != null) {
+      fieldController.removeListener(onFieldsListener: _onFieldFn);
+      _onFieldFn = null;
+    }
+    return super.close();
+  }
+}
+
+@freezed
+class CreateSortEvent with _$CreateSortEvent {
+  const factory CreateSortEvent.initial() = _Initial;
+  const factory CreateSortEvent.didReceiveFields(List<FieldInfo> fields) =
+      _DidReceiveFields;
+
+  const factory CreateSortEvent.createDefaultSort(FieldInfo field) =
+      _CreateDefaultSort;
+
+  const factory CreateSortEvent.didReceiveFilterText(String text) =
+      _DidReceiveFilterText;
+}
+
+@freezed
+class CreateSortState with _$CreateSortState {
+  const factory CreateSortState({
+    required String filterText,
+    required List<FieldInfo> creatableFields,
+    required List<FieldInfo> allFields,
+    required bool didCreateSort,
+  }) = _CreateSortState;
+
+  factory CreateSortState.initial(List<FieldInfo> fields) {
+    return CreateSortState(
+      filterText: "",
+      creatableFields: getCreatableSorts(fields),
+      allFields: fields,
+      didCreateSort: false,
+    );
+  }
+}

+ 128 - 0
frontend/app_flowy/lib/plugins/grid/application/sort/sort_editor_bloc.dart

@@ -0,0 +1,128 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/sort/sort_info.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pbenum.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pbserver.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+
+import 'sort_service.dart';
+import 'util.dart';
+
+part 'sort_editor_bloc.freezed.dart';
+
+class SortEditorBloc extends Bloc<SortEditorEvent, SortEditorState> {
+  final String viewId;
+  final SortFFIService _ffiService;
+  final GridFieldController fieldController;
+  void Function(List<FieldInfo>)? _onFieldFn;
+  SortEditorBloc({
+    required this.viewId,
+    required this.fieldController,
+    required List<SortInfo> sortInfos,
+  })  : _ffiService = SortFFIService(viewId: viewId),
+        super(SortEditorState.initial(sortInfos, fieldController.fieldInfos)) {
+    on<SortEditorEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () async {
+            _startListening();
+          },
+          didReceiveFields: (List<FieldInfo> fields) {
+            final List<FieldInfo> allFields = List.from(fields);
+            final List<FieldInfo> creatableFields = List.from(fields);
+            creatableFields.retainWhere((field) => field.canCreateSort);
+            emit(
+              state.copyWith(
+                allFields: allFields,
+                creatableFields: creatableFields,
+              ),
+            );
+          },
+          setCondition:
+              (SortInfo sortInfo, GridSortConditionPB condition) async {
+            final result = await _ffiService.updateSort(
+              fieldId: sortInfo.fieldInfo.id,
+              sortId: sortInfo.sortId,
+              fieldType: sortInfo.fieldInfo.fieldType,
+              condition: condition,
+            );
+            result.fold((l) => {}, (err) => Log.error(err));
+          },
+          deleteAllSorts: () async {
+            final result = await _ffiService.deleteAllSorts();
+            result.fold((l) => {}, (err) => Log.error(err));
+          },
+          didReceiveSorts: (List<SortInfo> sortInfos) {
+            emit(state.copyWith(sortInfos: sortInfos));
+          },
+          deleteSort: (SortInfo sortInfo) async {
+            final result = await _ffiService.deleteSort(
+              fieldId: sortInfo.fieldInfo.id,
+              sortId: sortInfo.sortId,
+              fieldType: sortInfo.fieldInfo.fieldType,
+            );
+            result.fold((l) => null, (err) => Log.error(err));
+          },
+        );
+      },
+    );
+  }
+
+  void _startListening() {
+    _onFieldFn = (fields) {
+      add(SortEditorEvent.didReceiveFields(List.from(fields)));
+    };
+
+    fieldController.addListener(
+      listenWhen: () => !isClosed,
+      onFields: _onFieldFn,
+      onSorts: (sorts) {
+        add(SortEditorEvent.didReceiveSorts(sorts));
+      },
+    );
+  }
+
+  @override
+  Future<void> close() async {
+    if (_onFieldFn != null) {
+      fieldController.removeListener(onFieldsListener: _onFieldFn);
+      _onFieldFn = null;
+    }
+    return super.close();
+  }
+}
+
+@freezed
+class SortEditorEvent with _$SortEditorEvent {
+  const factory SortEditorEvent.initial() = _Initial;
+  const factory SortEditorEvent.didReceiveFields(List<FieldInfo> fieldInfos) =
+      _DidReceiveFields;
+  const factory SortEditorEvent.didReceiveSorts(List<SortInfo> sortInfos) =
+      _DidReceiveSorts;
+  const factory SortEditorEvent.setCondition(
+      SortInfo sortInfo, GridSortConditionPB condition) = _SetCondition;
+  const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort;
+  const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts;
+}
+
+@freezed
+class SortEditorState with _$SortEditorState {
+  const factory SortEditorState({
+    required List<SortInfo> sortInfos,
+    required List<FieldInfo> creatableFields,
+    required List<FieldInfo> allFields,
+  }) = _SortEditorState;
+
+  factory SortEditorState.initial(
+    List<SortInfo> sortInfos,
+    List<FieldInfo> fields,
+  ) {
+    return SortEditorState(
+      creatableFields: getCreatableSorts(fields),
+      allFields: fields,
+      sortInfos: sortInfos,
+    );
+  }
+}

+ 51 - 0
frontend/app_flowy/lib/plugins/grid/application/sort/sort_listener.dart

@@ -0,0 +1,51 @@
+import 'dart:typed_data';
+
+import 'package:app_flowy/core/grid_notification.dart';
+import 'package:flowy_infra/notifier.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:dartz/dartz.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/dart_notification.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pb.dart';
+
+typedef SortNotifiedValue = Either<SortChangesetNotificationPB, FlowyError>;
+
+class SortsListener {
+  final String viewId;
+  PublishNotifier<SortNotifiedValue>? _notifier = PublishNotifier();
+  GridNotificationListener? _listener;
+
+  SortsListener({required this.viewId});
+
+  void start({
+    required void Function(SortNotifiedValue) onSortChanged,
+  }) {
+    _notifier?.addPublishListener(onSortChanged);
+    _listener = GridNotificationListener(
+      objectId: viewId,
+      handler: _handler,
+    );
+  }
+
+  void _handler(
+    GridDartNotification ty,
+    Either<Uint8List, FlowyError> result,
+  ) {
+    switch (ty) {
+      case GridDartNotification.DidUpdateSort:
+        result.fold(
+          (payload) => _notifier?.value =
+              left(SortChangesetNotificationPB.fromBuffer(payload)),
+          (error) => _notifier?.value = right(error),
+        );
+        break;
+      default:
+        break;
+    }
+  }
+
+  Future<void> stop() async {
+    await _listener?.stop();
+    _notifier?.dispose();
+    _notifier = null;
+  }
+}

+ 113 - 0
frontend/app_flowy/lib/plugins/grid/application/sort/sort_menu_bloc.dart

@@ -0,0 +1,113 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/sort/sort_info.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'dart:async';
+import 'util.dart';
+
+part 'sort_menu_bloc.freezed.dart';
+
+class SortMenuBloc extends Bloc<SortMenuEvent, SortMenuState> {
+  final String viewId;
+  final GridFieldController fieldController;
+  void Function(List<SortInfo>)? _onSortChangeFn;
+  void Function(List<FieldInfo>)? _onFieldFn;
+
+  SortMenuBloc({required this.viewId, required this.fieldController})
+      : super(SortMenuState.initial(
+          viewId,
+          fieldController.sortInfos,
+          fieldController.fieldInfos,
+        )) {
+    on<SortMenuEvent>(
+      (event, emit) async {
+        event.when(
+          initial: () {
+            _startListening();
+          },
+          didReceiveSortInfos: (sortInfos) {
+            emit(state.copyWith(sortInfos: sortInfos));
+          },
+          toggleMenu: () {
+            final isVisible = !state.isVisible;
+            emit(state.copyWith(isVisible: isVisible));
+          },
+          didReceiveFields: (List<FieldInfo> fields) {
+            emit(
+              state.copyWith(
+                fields: fields,
+                creatableFields: getCreatableSorts(fields),
+              ),
+            );
+          },
+        );
+      },
+    );
+  }
+
+  void _startListening() {
+    _onSortChangeFn = (sortInfos) {
+      add(SortMenuEvent.didReceiveSortInfos(sortInfos));
+    };
+
+    _onFieldFn = (fields) {
+      add(SortMenuEvent.didReceiveFields(fields));
+    };
+
+    fieldController.addListener(
+      onSorts: (sortInfos) {
+        _onSortChangeFn?.call(sortInfos);
+      },
+      onFields: (fields) {
+        _onFieldFn?.call(fields);
+      },
+    );
+  }
+
+  @override
+  Future<void> close() {
+    if (_onSortChangeFn != null) {
+      fieldController.removeListener(onSortsListener: _onSortChangeFn!);
+      _onSortChangeFn = null;
+    }
+    if (_onFieldFn != null) {
+      fieldController.removeListener(onFieldsListener: _onFieldFn!);
+      _onFieldFn = null;
+    }
+    return super.close();
+  }
+}
+
+@freezed
+class SortMenuEvent with _$SortMenuEvent {
+  const factory SortMenuEvent.initial() = _Initial;
+  const factory SortMenuEvent.didReceiveSortInfos(List<SortInfo> sortInfos) =
+      _DidReceiveSortInfos;
+  const factory SortMenuEvent.didReceiveFields(List<FieldInfo> fields) =
+      _DidReceiveFields;
+  const factory SortMenuEvent.toggleMenu() = _SetMenuVisibility;
+}
+
+@freezed
+class SortMenuState with _$SortMenuState {
+  const factory SortMenuState({
+    required String viewId,
+    required List<SortInfo> sortInfos,
+    required List<FieldInfo> fields,
+    required List<FieldInfo> creatableFields,
+    required bool isVisible,
+  }) = _SortMenuState;
+
+  factory SortMenuState.initial(
+    String viewId,
+    List<SortInfo> sortInfos,
+    List<FieldInfo> fields,
+  ) =>
+      SortMenuState(
+        viewId: viewId,
+        sortInfos: sortInfos,
+        fields: fields,
+        creatableFields: getCreatableSorts(fields),
+        isVisible: false,
+      );
+}

+ 116 - 0
frontend/app_flowy/lib/plugins/grid/application/sort/sort_service.dart

@@ -0,0 +1,116 @@
+import 'package:dartz/dartz.dart';
+import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/field_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/grid_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/setting_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pb.dart';
+
+class SortFFIService {
+  final String viewId;
+
+  SortFFIService({required this.viewId});
+
+  Future<Either<List<SortPB>, FlowyError>> getAllSorts() {
+    final payload = GridIdPB()..value = viewId;
+
+    return GridEventGetAllSorts(payload).send().then((result) {
+      return result.fold(
+        (repeated) => left(repeated.items),
+        (r) => right(r),
+      );
+    });
+  }
+
+  Future<Either<Unit, FlowyError>> updateSort({
+    required String fieldId,
+    required String sortId,
+    required FieldType fieldType,
+    required GridSortConditionPB condition,
+  }) {
+    var insertSortPayload = AlterSortPayloadPB.create()
+      ..fieldId = fieldId
+      ..fieldType = fieldType
+      ..viewId = viewId
+      ..condition = condition
+      ..sortId = sortId;
+
+    final payload = GridSettingChangesetPB.create()
+      ..gridId = viewId
+      ..alterSort = insertSortPayload;
+    return GridEventUpdateGridSetting(payload).send().then((result) {
+      return result.fold(
+        (l) => left(l),
+        (err) {
+          Log.error(err);
+          return right(err);
+        },
+      );
+    });
+  }
+
+  Future<Either<Unit, FlowyError>> insertSort({
+    required String fieldId,
+    required FieldType fieldType,
+    required GridSortConditionPB condition,
+  }) {
+    var insertSortPayload = AlterSortPayloadPB.create()
+      ..fieldId = fieldId
+      ..fieldType = fieldType
+      ..viewId = viewId
+      ..condition = condition;
+
+    final payload = GridSettingChangesetPB.create()
+      ..gridId = viewId
+      ..alterSort = insertSortPayload;
+    return GridEventUpdateGridSetting(payload).send().then((result) {
+      return result.fold(
+        (l) => left(l),
+        (err) {
+          Log.error(err);
+          return right(err);
+        },
+      );
+    });
+  }
+
+  Future<Either<Unit, FlowyError>> deleteSort({
+    required String fieldId,
+    required String sortId,
+    required FieldType fieldType,
+  }) {
+    final deleteFilterPayload = DeleteSortPayloadPB.create()
+      ..fieldId = fieldId
+      ..sortId = sortId
+      ..viewId = viewId
+      ..fieldType = fieldType;
+
+    final payload = GridSettingChangesetPB.create()
+      ..gridId = viewId
+      ..deleteSort = deleteFilterPayload;
+
+    return GridEventUpdateGridSetting(payload).send().then((result) {
+      return result.fold(
+        (l) => left(l),
+        (err) {
+          Log.error(err);
+          return right(err);
+        },
+      );
+    });
+  }
+
+  Future<Either<Unit, FlowyError>> deleteAllSorts() {
+    final payload = GridIdPB(value: viewId);
+    return GridEventDeleteAllSorts(payload).send().then((result) {
+      return result.fold(
+        (l) => left(l),
+        (err) {
+          Log.error(err);
+          return right(err);
+        },
+      );
+    });
+  }
+}

+ 7 - 0
frontend/app_flowy/lib/plugins/grid/application/sort/util.dart

@@ -0,0 +1,7 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+
+List<FieldInfo> getCreatableSorts(List<FieldInfo> fieldInfos) {
+  final List<FieldInfo> creatableFields = List.from(fieldInfos);
+  creatableFields.retainWhere((element) => element.canCreateSort);
+  return creatableFields;
+}

+ 12 - 0
frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart

@@ -37,6 +37,18 @@ class GridViewCache {
           (err) => Log.error(err),
         );
       },
+      onReorderAllRows: (result) {
+        result.fold(
+          (rowIds) => _rowCache.reorderAllRows(rowIds),
+          (err) => Log.error(err),
+        );
+      },
+      onReorderSingleRow: (result) {
+        result.fold(
+          (reorderRow) => _rowCache.reorderSingleRow(reorderRow),
+          (err) => Log.error(err),
+        );
+      },
     );
   }
 

+ 38 - 7
frontend/app_flowy/lib/plugins/grid/application/view/grid_view_listener.dart

@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'dart:typed_data';
 import 'package:app_flowy/core/grid_notification.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pb.dart';
 import 'package:dartz/dartz.dart';
 import 'package:flowy_infra/notifier.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -11,11 +12,17 @@ typedef GridRowsVisibilityNotifierValue
     = Either<GridRowsVisibilityChangesetPB, FlowyError>;
 
 typedef GridViewRowsNotifierValue = Either<GridViewRowsChangesetPB, FlowyError>;
+typedef GridViewReorderAllRowsNotifierValue = Either<List<String>, FlowyError>;
+typedef GridViewSingleRowNotifierValue = Either<ReorderSingleRowPB, FlowyError>;
 
 class GridViewListener {
   final String viewId;
   PublishNotifier<GridViewRowsNotifierValue>? _rowsNotifier = PublishNotifier();
-  PublishNotifier<GridRowsVisibilityNotifierValue>? _rowsVisibilityNotifier =
+  PublishNotifier<GridViewReorderAllRowsNotifierValue>? _reorderAllRows =
+      PublishNotifier();
+  PublishNotifier<GridViewSingleRowNotifierValue>? _reorderSingleRow =
+      PublishNotifier();
+  PublishNotifier<GridRowsVisibilityNotifierValue>? _rowsVisibility =
       PublishNotifier();
 
   GridNotificationListener? _listener;
@@ -23,6 +30,9 @@ class GridViewListener {
 
   void start({
     required void Function(GridViewRowsNotifierValue) onRowsChanged,
+    required void Function(GridViewReorderAllRowsNotifierValue)
+        onReorderAllRows,
+    required void Function(GridViewSingleRowNotifierValue) onReorderSingleRow,
     required void Function(GridRowsVisibilityNotifierValue)
         onRowsVisibilityChanged,
   }) {
@@ -36,16 +46,18 @@ class GridViewListener {
     );
 
     _rowsNotifier?.addPublishListener(onRowsChanged);
-    _rowsVisibilityNotifier?.addPublishListener(onRowsVisibilityChanged);
+    _rowsVisibility?.addPublishListener(onRowsVisibilityChanged);
+    _reorderAllRows?.addPublishListener(onReorderAllRows);
+    _reorderSingleRow?.addPublishListener(onReorderSingleRow);
   }
 
   void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
     switch (ty) {
       case GridDartNotification.DidUpdateGridViewRowsVisibility:
         result.fold(
-          (payload) => _rowsVisibilityNotifier?.value =
+          (payload) => _rowsVisibility?.value =
               left(GridRowsVisibilityChangesetPB.fromBuffer(payload)),
-          (error) => _rowsVisibilityNotifier?.value = right(error),
+          (error) => _rowsVisibility?.value = right(error),
         );
         break;
       case GridDartNotification.DidUpdateGridViewRows:
@@ -55,7 +67,20 @@ class GridViewListener {
           (error) => _rowsNotifier?.value = right(error),
         );
         break;
-
+      case GridDartNotification.DidReorderRows:
+        result.fold(
+          (payload) => _reorderAllRows?.value =
+              left(ReorderAllRowsPB.fromBuffer(payload).rowOrders),
+          (error) => _reorderAllRows?.value = right(error),
+        );
+        break;
+      case GridDartNotification.DidReorderSingleRow:
+        result.fold(
+          (payload) => _reorderSingleRow?.value =
+              left(ReorderSingleRowPB.fromBuffer(payload)),
+          (error) => _reorderSingleRow?.value = right(error),
+        );
+        break;
       default:
         break;
     }
@@ -63,10 +88,16 @@ class GridViewListener {
 
   Future<void> stop() async {
     await _listener?.stop();
-    _rowsVisibilityNotifier?.dispose();
-    _rowsVisibilityNotifier = null;
+    _rowsVisibility?.dispose();
+    _rowsVisibility = null;
 
     _rowsNotifier?.dispose();
     _rowsNotifier = null;
+
+    _reorderAllRows?.dispose();
+    _reorderAllRows = null;
+
+    _reorderSingleRow?.dispose();
+    _reorderSingleRow = null;
   }
 }

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

@@ -4,6 +4,7 @@ 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/plugins/grid/application/grid_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/sort/sort_menu_bloc.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
 import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
@@ -20,13 +21,13 @@ import '../application/setting/setting_bloc.dart';
 import 'controller/grid_scroll.dart';
 import 'layout/layout.dart';
 import 'layout/sizes.dart';
+import 'widgets/accessory_menu.dart';
 import 'widgets/cell/cell_builder.dart';
 import 'widgets/row/grid_row.dart';
 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 {
@@ -62,6 +63,12 @@ class _GridPageState extends State<GridPage> {
             fieldController: widget.gridController.fieldController,
           )..add(const GridFilterMenuEvent.initial()),
         ),
+        BlocProvider<SortMenuBloc>(
+          create: (context) => SortMenuBloc(
+            viewId: widget.view.id,
+            fieldController: widget.gridController.fieldController,
+          )..add(const SortMenuEvent.initial()),
+        ),
         BlocProvider<GridSettingBloc>(
           create: (context) => GridSettingBloc(gridId: widget.view.id),
         ),
@@ -139,7 +146,7 @@ class _FlowyGridState extends State<FlowyGrid> {
           crossAxisAlignment: CrossAxisAlignment.start,
           children: [
             const GridToolbar(),
-            const GridFilterMenu(),
+            GridAccessoryMenu(viewId: state.gridId),
             _gridHeader(context, state.gridId),
             Flexible(child: child),
             const RowCountBadge(),
@@ -218,9 +225,22 @@ class _GridRowsState extends State<_GridRows> {
                   _renderRow(context, item.rowInfo, animation),
             );
           },
+          reorderSingleRow: (reorderRow, rowInfo) {
+            // _key.currentState?.removeItem(
+            //   reorderRow.oldIndex,
+            //   (context, animation) => _renderRow(context, rowInfo, animation),
+            // );
+            // _key.currentState?.insertItem(reorderRow.newIndex);
+          },
         );
       },
-      buildWhen: (previous, current) => false,
+      buildWhen: (previous, current) {
+        return current.reason.whenOrNull(
+              reorderRows: () => true,
+              reorderSingleRow: (reorderRow, rowInfo) => true,
+            ) ??
+            false;
+      },
       builder: (context, state) {
         return SliverAnimatedList(
           key: _key,

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

@@ -11,7 +11,7 @@ class GridSize {
   static double get headerContainerPadding => 0 * scale;
   static double get cellHPadding => 10 * scale;
   static double get cellVPadding => 10 * scale;
-  static double get typeOptionItemHeight => 32 * scale;
+  static double get popoverItemHeight => 32 * scale;
   static double get typeOptionSeparatorHeight => 4 * scale;
 
   static EdgeInsets get headerContentInsets => EdgeInsets.symmetric(

+ 89 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/accessory_menu.dart

@@ -0,0 +1,89 @@
+import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/grid_accessory_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/sort/sort_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/sort/sort_menu.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+import 'filter/filter_menu.dart';
+
+class GridAccessoryMenu extends StatelessWidget {
+  final String viewId;
+  const GridAccessoryMenu({required this.viewId, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => GridAccessoryMenuBloc(viewId: viewId),
+      child: MultiBlocListener(
+        listeners: [
+          BlocListener<GridFilterMenuBloc, GridFilterMenuState>(
+              listenWhen: (p, c) => p.isVisible != c.isVisible,
+              listener: (context, state) => context
+                  .read<GridAccessoryMenuBloc>()
+                  .add(const GridAccessoryMenuEvent.toggleMenu())),
+          BlocListener<SortMenuBloc, SortMenuState>(
+            listenWhen: (p, c) => p.isVisible != c.isVisible,
+            listener: (context, state) => context
+                .read<GridAccessoryMenuBloc>()
+                .add(const GridAccessoryMenuEvent.toggleMenu()),
+          ),
+        ],
+        child: BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
+          builder: (context, state) {
+            if (state.isVisible) {
+              return const _AccessoryMenu();
+            } else {
+              return const SizedBox();
+            }
+          },
+        ),
+      ),
+    );
+  }
+}
+
+class _AccessoryMenu extends StatelessWidget {
+  const _AccessoryMenu({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<GridAccessoryMenuBloc, GridAccessoryMenuState>(
+      builder: (context, state) {
+        return _wrapPadding(
+          Column(
+            children: [
+              Divider(
+                height: 1.0,
+                color: AFThemeExtension.of(context).toggleOffFill,
+              ),
+              const VSpace(6),
+              IntrinsicHeight(
+                child: Row(
+                  children: const [
+                    SortMenu(),
+                    HSpace(6),
+                    FilterMenu(),
+                  ],
+                ),
+              ),
+            ],
+          ),
+        );
+      },
+    );
+  }
+
+  Widget _wrapPadding(Widget child) {
+    return Padding(
+      padding: EdgeInsets.symmetric(
+        horizontal: GridSize.leadingHeaderPadding,
+        vertical: 6,
+      ),
+      child: child,
+    );
+  }
+}

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_cell_editor.dart

@@ -114,7 +114,7 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
         : svgWidget('editor/editor_uncheck');
     return _wrapPopover(
       SizedBox(
-        height: GridSize.typeOptionItemHeight,
+        height: GridSize.popoverItemHeight,
         child: Row(
           children: [
             Expanded(

+ 5 - 5
frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart

@@ -163,9 +163,9 @@ class _CellCalendarWidgetState extends State<_CellCalendarWidget> {
           firstDay: kFirstDay,
           lastDay: kLastDay,
           focusedDay: state.focusedDay,
-          rowHeight: GridSize.typeOptionItemHeight,
+          rowHeight: GridSize.popoverItemHeight,
           calendarFormat: state.format,
-          daysOfWeekHeight: GridSize.typeOptionItemHeight,
+          daysOfWeekHeight: GridSize.popoverItemHeight,
           headerStyle: HeaderStyle(
             formatButtonVisible: false,
             titleCentered: true,
@@ -243,7 +243,7 @@ class _IncludeTimeButton extends StatelessWidget {
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 12.0),
           child: SizedBox(
-            height: GridSize.typeOptionItemHeight,
+            height: GridSize.popoverItemHeight,
             child: Padding(
               padding: GridSize.typeOptionContentInsets,
               child: Row(
@@ -327,7 +327,7 @@ class _TimeTextFieldState extends State<_TimeTextField> {
       child: Padding(
         padding: GridSize.typeOptionContentInsets,
         child: RoundedInputField(
-          height: GridSize.typeOptionItemHeight,
+          height: GridSize.popoverItemHeight,
           focusNode: _focusNode,
           autoFocus: true,
           hintText: widget.bloc.state.timeHintText,
@@ -376,7 +376,7 @@ class _DateTypeOptionButton extends StatelessWidget {
           child: Padding(
             padding: const EdgeInsets.symmetric(horizontal: 12.0),
             child: SizedBox(
-              height: GridSize.typeOptionItemHeight,
+              height: GridSize.popoverItemHeight,
               child: FlowyButton(
                 text: FlowyText.medium(title),
                 margin: GridSize.typeOptionContentInsets,

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

@@ -181,7 +181,7 @@ class _Title extends StatelessWidget {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12.0),
       child: SizedBox(
-        height: GridSize.typeOptionItemHeight,
+        height: GridSize.popoverItemHeight,
         child: FlowyText.medium(
           LocaleKeys.grid_selectOption_panelTitle.tr(),
           color: Theme.of(context).hintColor,
@@ -200,7 +200,7 @@ class _CreateOptionCell extends StatelessWidget {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12.0),
       child: SizedBox(
-        height: GridSize.typeOptionItemHeight,
+        height: GridSize.popoverItemHeight,
         child: Row(
           children: [
             FlowyText.medium(
@@ -254,7 +254,7 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
   @override
   Widget build(BuildContext context) {
     final child = SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: SelectOptionTagCell(
         option: widget.option,
         onSelected: (option) {

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart

@@ -98,7 +98,7 @@ class _SelectOptionFilterCellState extends State<_SelectOptionFilterCell> {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: SelectOptionTagCell(
         option: widget.option,
         onSelected: (option) {

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

@@ -57,7 +57,7 @@ class _GridCreateFilterListState extends State<GridCreateFilterList> {
           builder: (context, state) {
             final cells = state.creatableFields.map((fieldInfo) {
               return SizedBox(
-                height: GridSize.typeOptionItemHeight,
+                height: GridSize.popoverItemHeight,
                 child: _FilterPropertyCell(
                   fieldInfo: fieldInfo,
                   onTap: (fieldInfo) => createFilter(fieldInfo),

+ 25 - 56
frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart → frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_menu.dart

@@ -1,80 +1,49 @@
 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/theme_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/widget/spacing.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
 import 'create_filter_list.dart';
-import 'menu_item.dart';
+import 'filter_menu_item.dart';
 
-class GridFilterMenu extends StatelessWidget {
-  const GridFilterMenu({Key? key}) : super(key: key);
+class FilterMenu extends StatelessWidget {
+  const FilterMenu({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),
-            ],
-          ));
-        } 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, GridFilterMenuState state) {
-    final List<Widget> children = [];
-    children.addAll(
-      state.filters
-          .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
-          .toList(),
-    );
+        final List<Widget> children = [];
+        children.addAll(
+          state.filters
+              .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo))
+              .toList(),
+        );
 
-    if (state.creatableFields.isNotEmpty) {
-      children.add(AddFilterButton(viewId: viewId));
-    }
+        if (state.creatableFields.isNotEmpty) {
+          children.add(AddFilterButton(viewId: state.viewId));
+        }
 
-    return Row(
-      children: [
-        Expanded(
-          child: Wrap(
-            spacing: 6,
-            runSpacing: 4,
-            children: children,
+        return Expanded(
+          child: Row(
+            children: [
+              Expanded(
+                child: Wrap(
+                  spacing: 6,
+                  runSpacing: 4,
+                  children: children,
+                ),
+              ),
+            ],
           ),
-        ),
-      ],
+        );
+      },
     );
   }
 }

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


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

@@ -77,7 +77,7 @@ class _EditFieldButton extends StatelessWidget {
     return BlocBuilder<FieldActionSheetBloc, FieldActionSheetState>(
       builder: (context, state) {
         return SizedBox(
-          height: GridSize.typeOptionItemHeight,
+          height: GridSize.popoverItemHeight,
           child: FlowyButton(
             text: FlowyText.medium(
               LocaleKeys.grid_field_editProperty.tr(),
@@ -119,7 +119,7 @@ class _FieldOperationList extends StatelessWidget {
         }
 
         return SizedBox(
-          height: GridSize.typeOptionItemHeight,
+          height: GridSize.popoverItemHeight,
           width: cellWidth,
           child: FieldActionCell(
             fieldInfo: fieldInfo,

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

@@ -225,7 +225,7 @@ class _DeleteFieldButton extends StatelessWidget {
         );
         return Padding(
           padding: const EdgeInsets.only(bottom: 4.0),
-          child: SizedBox(height: GridSize.typeOptionItemHeight, child: button),
+          child: SizedBox(height: GridSize.popoverItemHeight, child: button),
         );
       },
     );

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

@@ -58,7 +58,7 @@ class FieldTypeCell extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(fieldType.title()),
         onTap: () => onSelectField(fieldType),

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

@@ -103,7 +103,7 @@ class _SwitchFieldButton extends StatelessWidget {
     );
 
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: widget,
     );
   }

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

@@ -147,7 +147,7 @@ class DateFormatButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
         margin: buttonMargins,
@@ -178,7 +178,7 @@ class TimeFormatButton extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
         margin: buttonMargins,
@@ -204,7 +204,7 @@ class _IncludeTimeButton extends StatelessWidget {
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 12.0),
           child: SizedBox(
-            height: GridSize.typeOptionItemHeight,
+            height: GridSize.popoverItemHeight,
             child: Padding(
               padding: GridSize.typeOptionContentInsets,
               child: Row(
@@ -286,7 +286,7 @@ class DateFormatCell extends StatelessWidget {
     }
 
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(dateFormat.title()),
         rightIcon: checkmark,
@@ -368,7 +368,7 @@ class TimeFormatCell extends StatelessWidget {
     }
 
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(timeFormat.title()),
         rightIcon: checkmark,

+ 3 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart

@@ -52,13 +52,13 @@ class NumberTypeOptionWidget extends TypeOptionWidget {
       create: (context) =>
           NumberTypeOptionBloc(typeOptionContext: typeOptionContext),
       child: SizedBox(
-        height: GridSize.typeOptionItemHeight,
+        height: GridSize.popoverItemHeight,
         child: BlocConsumer<NumberTypeOptionBloc, NumberTypeOptionState>(
           listener: (context, state) =>
               typeOptionContext.typeOption = state.typeOption,
           builder: (context, state) {
             final button = SizedBox(
-              height: GridSize.typeOptionItemHeight,
+              height: GridSize.popoverItemHeight,
               child: FlowyButton(
                 margin: GridSize.typeOptionContentInsets,
                 rightIcon: svgWidget(
@@ -177,7 +177,7 @@ class NumberFormatCell extends StatelessWidget {
     }
 
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(format.title()),
         onTap: () => onSelected(format),

+ 3 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart

@@ -85,7 +85,7 @@ class OptionTitle extends StatelessWidget {
         return Padding(
           padding: const EdgeInsets.symmetric(horizontal: 12.0),
           child: SizedBox(
-            height: GridSize.typeOptionItemHeight,
+            height: GridSize.popoverItemHeight,
             child: Row(children: children),
           ),
         );
@@ -185,7 +185,7 @@ class _OptionCellState extends State<_OptionCell> {
   @override
   Widget build(BuildContext context) {
     final child = SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: SelectOptionTagCell(
         option: widget.option,
         onSelected: (SelectOptionPB pb) {
@@ -243,7 +243,7 @@ class _AddOptionButton extends StatelessWidget {
     return Padding(
       padding: const EdgeInsets.symmetric(horizontal: 12.0),
       child: SizedBox(
-        height: GridSize.typeOptionItemHeight,
+        height: GridSize.popoverItemHeight,
         child: FlowyButton(
           text: FlowyText.medium(LocaleKeys.grid_field_addSelectOption.tr()),
           onTap: () {

+ 3 - 3
frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart

@@ -101,7 +101,7 @@ class _DeleteTag extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(LocaleKeys.grid_selectOption_deleteTag.tr()),
         leftIcon: svgWidget(
@@ -162,7 +162,7 @@ class SelectOptionColorList extends StatelessWidget {
         Padding(
           padding: GridSize.typeOptionContentInsets,
           child: SizedBox(
-            height: GridSize.typeOptionItemHeight,
+            height: GridSize.popoverItemHeight,
             child: FlowyText.medium(
               LocaleKeys.grid_selectOption_colorPanelTitle.tr(),
               textAlign: TextAlign.left,
@@ -212,7 +212,7 @@ class _SelectOptionColorCell extends StatelessWidget {
     );
 
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(color.optionName()),
         leftIcon: colorIcon,

+ 1 - 1
frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart

@@ -56,7 +56,7 @@ class _RowActionCell extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(
           action.title(),

+ 168 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/create_sort_list.dart

@@ -0,0 +1,168 @@
+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/sort/sort_create_bloc.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.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/style_widget/text_field.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class GridCreateSortList extends StatefulWidget {
+  final String viewId;
+  final GridFieldController fieldController;
+  final VoidCallback onClosed;
+  final VoidCallback? onCreateSort;
+
+  const GridCreateSortList({
+    required this.viewId,
+    required this.fieldController,
+    required this.onClosed,
+    this.onCreateSort,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _GridCreateSortListState();
+}
+
+class _GridCreateSortListState extends State<GridCreateSortList> {
+  late CreateSortBloc editBloc;
+
+  @override
+  void initState() {
+    editBloc = CreateSortBloc(
+      viewId: widget.viewId,
+      fieldController: widget.fieldController,
+    )..add(const CreateSortEvent.initial());
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider.value(
+      value: editBloc,
+      child: BlocListener<CreateSortBloc, CreateSortState>(
+        listener: (context, state) {
+          if (state.didCreateSort) {
+            widget.onClosed();
+          }
+        },
+        child: BlocBuilder<CreateSortBloc, CreateSortState>(
+          builder: (context, state) {
+            final cells = state.creatableFields.map((fieldInfo) {
+              return SizedBox(
+                height: GridSize.popoverItemHeight,
+                child: _SortPropertyCell(
+                  fieldInfo: fieldInfo,
+                  onTap: (fieldInfo) => createSort(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 createSort(FieldInfo field) {
+    editBloc.add(CreateSortEvent.createDefaultSort(field));
+    widget.onCreateSort?.call();
+  }
+}
+
+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: FlowyTextField(
+          hintText: LocaleKeys.grid_settings_filterBy.tr(),
+          onChanged: (text) {
+            context
+                .read<CreateSortBloc>()
+                .add(CreateSortEvent.didReceiveFilterText(text));
+          },
+        ),
+      ),
+    );
+  }
+
+  @override
+  double get maxExtent => fixHeight;
+
+  @override
+  double get minExtent => fixHeight;
+
+  @override
+  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
+    return false;
+  }
+}
+
+class _SortPropertyCell extends StatelessWidget {
+  final FieldInfo fieldInfo;
+  final Function(FieldInfo) onTap;
+  const _SortPropertyCell({
+    required this.fieldInfo,
+    required this.onTap,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return FlowyButton(
+      text: FlowyText.medium(fieldInfo.name),
+      onTap: () => onTap(fieldInfo),
+      leftIcon: svgWidget(
+        fieldInfo.fieldType.iconName(),
+        color: Theme.of(context).colorScheme.onSurface,
+      ),
+    );
+  }
+}

+ 46 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/order_panel.dart

@@ -0,0 +1,46 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pbenum.dart';
+import 'package:easy_localization/easy_localization.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 OrderPanel extends StatelessWidget {
+  final Function(GridSortConditionPB) onCondition;
+  const OrderPanel({required this.onCondition, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> children = GridSortConditionPB.values.map((condition) {
+      return SizedBox(
+        height: GridSize.popoverItemHeight,
+        child: FlowyButton(
+          text: FlowyText.medium(textFromCondition(condition)),
+          onTap: () => onCondition(condition),
+        ),
+      );
+    }).toList();
+
+    return ConstrainedBox(
+      constraints: const BoxConstraints(minWidth: 160),
+      child: IntrinsicWidth(
+        child: IntrinsicHeight(
+          child: Column(
+            children: children,
+          ),
+        ),
+      ),
+    );
+  }
+
+  String textFromCondition(GridSortConditionPB condition) {
+    switch (condition) {
+      case GridSortConditionPB.Ascending:
+        return LocaleKeys.grid_sort_ascending.tr();
+      case GridSortConditionPB.Descending:
+        return LocaleKeys.grid_sort_descending.tr();
+    }
+    return "";
+  }
+}

+ 51 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_choice_button.dart

@@ -0,0 +1,51 @@
+import 'package:flowy_infra/theme_extension.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 SortChoiceButton extends StatelessWidget {
+  final String text;
+  final VoidCallback? onTap;
+  final Widget? leftIcon;
+  final Widget? rightIcon;
+  final Radius radius;
+  final bool editable;
+
+  const SortChoiceButton({
+    required this.text,
+    this.onTap,
+    this.radius = const Radius.circular(14),
+    this.leftIcon,
+    this.rightIcon,
+    this.editable = true,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    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 FlowyButton(
+      decoration: decoration,
+      useIntrinsicWidth: true,
+      text: FlowyText(text),
+      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
+      radius: BorderRadius.all(radius),
+      leftIcon: leftIcon,
+      rightIcon: rightIcon,
+      hoverColor: AFThemeExtension.of(context).lightGreyHover,
+      onTap: onTap,
+      disable: !editable,
+      disableOpacity: 1.0,
+    );
+  }
+}

+ 293 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_editor.dart

@@ -0,0 +1,293 @@
+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/sort/sort_editor_bloc.dart';
+import 'package:app_flowy/plugins/grid/application/sort/util.dart';
+import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/sort/create_sort_list.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pbenum.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/theme_extension.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/icon_button.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'dart:math' as math;
+
+import 'order_panel.dart';
+import 'sort_choice_button.dart';
+import 'sort_info.dart';
+
+class SortEditor extends StatefulWidget {
+  final String viewId;
+  final List<SortInfo> sortInfos;
+  final GridFieldController fieldController;
+  const SortEditor({
+    required this.viewId,
+    required this.fieldController,
+    required this.sortInfos,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<SortEditor> createState() => _SortEditorState();
+}
+
+class _SortEditorState extends State<SortEditor> {
+  final popoverMutex = PopoverMutex();
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocProvider(
+      create: (context) => SortEditorBloc(
+        viewId: widget.viewId,
+        fieldController: widget.fieldController,
+        sortInfos: widget.sortInfos,
+      )..add(const SortEditorEvent.initial()),
+      child: BlocBuilder<SortEditorBloc, SortEditorState>(
+        builder: (context, state) {
+          return IntrinsicWidth(
+            child: IntrinsicHeight(
+              child: Column(
+                children: [
+                  _SortList(popoverMutex: popoverMutex),
+                  _AddSortButton(
+                    viewId: widget.viewId,
+                    fieldController: widget.fieldController,
+                    popoverMutex: popoverMutex,
+                  ),
+                  _DeleteSortButton(popoverMutex: popoverMutex),
+                ],
+              ),
+            ),
+          );
+        },
+      ),
+    );
+  }
+}
+
+class _SortList extends StatelessWidget {
+  final PopoverMutex popoverMutex;
+  const _SortList({required this.popoverMutex, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SortEditorBloc, SortEditorState>(
+      builder: (context, state) {
+        final List<Widget> children = state.sortInfos
+            .map((info) => Padding(
+                  padding: const EdgeInsets.symmetric(vertical: 6),
+                  child: _SortItem(
+                    sortInfo: info,
+                    popoverMutex: popoverMutex,
+                  ),
+                ))
+            .toList();
+
+        return Column(
+          children: children,
+        );
+      },
+    );
+  }
+}
+
+class _SortItem extends StatelessWidget {
+  final SortInfo sortInfo;
+  final PopoverMutex popoverMutex;
+  const _SortItem({
+    required this.popoverMutex,
+    required this.sortInfo,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final nameButton = SortChoiceButton(
+      text: sortInfo.fieldInfo.name,
+      editable: false,
+      onTap: () {},
+    );
+    final orderButton = _OrderButton(
+      sortInfo: sortInfo,
+      popoverMutex: popoverMutex,
+    );
+
+    final deleteButton = FlowyIconButton(
+      width: 26,
+      onPressed: () {
+        context
+            .read<SortEditorBloc>()
+            .add(SortEditorEvent.deleteSort(sortInfo));
+      },
+      iconPadding: const EdgeInsets.all(5),
+      hoverColor: AFThemeExtension.of(context).lightGreyHover,
+      icon: svgWidget(
+        "home/close",
+        color: Theme.of(context).colorScheme.onSurface,
+      ),
+    );
+
+    return Row(
+      children: [
+        SizedBox(height: 26, child: nameButton),
+        const HSpace(6),
+        SizedBox(height: 26, child: orderButton),
+        const HSpace(16),
+        deleteButton
+      ],
+    );
+  }
+
+  String textFromCondition(GridSortConditionPB condition) {
+    switch (condition) {
+      case GridSortConditionPB.Ascending:
+        return LocaleKeys.grid_sort_ascending.tr();
+      case GridSortConditionPB.Descending:
+        return LocaleKeys.grid_sort_descending.tr();
+    }
+    return "";
+  }
+}
+
+class _AddSortButton extends StatefulWidget {
+  final String viewId;
+  final GridFieldController fieldController;
+  final PopoverMutex popoverMutex;
+  const _AddSortButton({
+    required this.viewId,
+    required this.fieldController,
+    required this.popoverMutex,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  State<_AddSortButton> createState() => _AddSortButtonState();
+}
+
+class _AddSortButtonState extends State<_AddSortButton> {
+  final _popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    return AppFlowyPopover(
+      controller: _popoverController,
+      mutex: widget.popoverMutex,
+      direction: PopoverDirection.bottomWithLeftAligned,
+      constraints: BoxConstraints.loose(const Size(200, 300)),
+      offset: const Offset(0, 10),
+      triggerActions: PopoverTriggerFlags.none,
+      asBarrier: true,
+      child: SizedBox(
+        height: GridSize.popoverItemHeight,
+        child: FlowyButton(
+          disable: getCreatableSorts(widget.fieldController.fieldInfos).isEmpty,
+          text: FlowyText.medium(LocaleKeys.grid_sort_addSort.tr()),
+          onTap: () => _popoverController.show(),
+          leftIcon: svgWidget(
+            "home/add",
+            color: Theme.of(context).colorScheme.onSurface,
+          ),
+        ),
+      ),
+      popupBuilder: (BuildContext context) {
+        return GridCreateSortList(
+          viewId: widget.viewId,
+          fieldController: widget.fieldController,
+          onClosed: () => _popoverController.close(),
+        );
+      },
+    );
+  }
+}
+
+class _DeleteSortButton extends StatelessWidget {
+  final PopoverMutex popoverMutex;
+  const _DeleteSortButton({required this.popoverMutex, Key? key})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SortEditorBloc, SortEditorState>(
+      builder: (context, state) {
+        return SizedBox(
+          height: GridSize.popoverItemHeight,
+          child: FlowyButton(
+            text: FlowyText.medium(LocaleKeys.grid_sort_deleteSort.tr()),
+            onTap: () {
+              context
+                  .read<SortEditorBloc>()
+                  .add(const SortEditorEvent.deleteAllSorts());
+            },
+            leftIcon: svgWidget(
+              "editor/delete",
+              color: Theme.of(context).colorScheme.onSurface,
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
+
+class _OrderButton extends StatefulWidget {
+  final SortInfo sortInfo;
+  final PopoverMutex popoverMutex;
+  const _OrderButton({
+    required this.popoverMutex,
+    required this.sortInfo,
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  _OrderButtonState createState() => _OrderButtonState();
+}
+
+class _OrderButtonState extends State<_OrderButton> {
+  final PopoverController popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    final arrow = Transform.rotate(
+      angle: -math.pi / 2,
+      child: svgWidget("home/arrow_left"),
+    );
+
+    return AppFlowyPopover(
+      controller: popoverController,
+      mutex: widget.popoverMutex,
+      constraints: BoxConstraints.loose(const Size(340, 200)),
+      direction: PopoverDirection.bottomWithLeftAligned,
+      triggerActions: PopoverTriggerFlags.none,
+      popupBuilder: (BuildContext popoverContext) {
+        return OrderPanel(
+          onCondition: (condition) {
+            context
+                .read<SortEditorBloc>()
+                .add(SortEditorEvent.setCondition(widget.sortInfo, condition));
+            popoverController.close();
+          },
+        );
+      },
+      child: SortChoiceButton(
+        text: textFromCondition(widget.sortInfo.sortPB.condition),
+        rightIcon: arrow,
+        onTap: () => popoverController.show(),
+      ),
+    );
+  }
+
+  String textFromCondition(GridSortConditionPB condition) {
+    switch (condition) {
+      case GridSortConditionPB.Ascending:
+        return LocaleKeys.grid_sort_ascending.tr();
+      case GridSortConditionPB.Descending:
+        return LocaleKeys.grid_sort_descending.tr();
+    }
+    return "";
+  }
+}

+ 11 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_info.dart

@@ -0,0 +1,11 @@
+import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
+import 'package:appflowy_backend/protobuf/flowy-grid/sort_entities.pb.dart';
+
+class SortInfo {
+  final SortPB sortPB;
+  final FieldInfo fieldInfo;
+
+  SortInfo({required this.sortPB, required this.fieldInfo});
+
+  String get sortId => sortPB.id;
+}

+ 77 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_menu.dart

@@ -0,0 +1,77 @@
+import 'package:app_flowy/plugins/grid/application/sort/sort_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/sort/sort_info.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/image.dart';
+import 'dart:math' as math;
+
+import 'sort_choice_button.dart';
+import 'sort_editor.dart';
+
+class SortMenu extends StatelessWidget {
+  const SortMenu({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SortMenuBloc, SortMenuState>(
+      builder: (context, state) {
+        if (state.sortInfos.isNotEmpty) {
+          return AppFlowyPopover(
+            controller: PopoverController(),
+            constraints: BoxConstraints.loose(const Size(340, 200)),
+            direction: PopoverDirection.bottomWithLeftAligned,
+            popupBuilder: (BuildContext popoverContext) {
+              return SortEditor(
+                viewId: state.viewId,
+                fieldController: context.read<SortMenuBloc>().fieldController,
+                sortInfos: state.sortInfos,
+              );
+            },
+            child: SortChoiceChip(sortInfos: state.sortInfos),
+          );
+        } else {
+          return const SizedBox();
+        }
+      },
+    );
+  }
+}
+
+class SortChoiceChip extends StatelessWidget {
+  final List<SortInfo> sortInfos;
+  final VoidCallback? onTap;
+
+  const SortChoiceChip({
+    Key? key,
+    required this.sortInfos,
+    this.onTap,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final arrow = Transform.rotate(
+      angle: -math.pi / 2,
+      child: svgWidget("home/arrow_left"),
+    );
+
+    final text = LocaleKeys.grid_settings_sort.tr();
+    final leftIcon = svgWidget(
+      "grid/setting/sort",
+      color: Theme.of(context).colorScheme.onSurface,
+    );
+
+    return SizedBox(
+      height: 28,
+      child: SortChoiceButton(
+        text: text,
+        leftIcon: leftIcon,
+        rightIcon: arrow,
+        onTap: onTap,
+      ),
+    );
+  }
+}

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

@@ -54,7 +54,7 @@ class _FilterButtonState extends State<FilterButton> {
   Widget _wrapPopover(BuildContext buildContext, Widget child) {
     return AppFlowyPopover(
       controller: _popoverController,
-      direction: PopoverDirection.leftWithTopAligned,
+      direction: PopoverDirection.bottomWithLeftAligned,
       constraints: BoxConstraints.loose(const Size(200, 300)),
       offset: const Offset(0, 10),
       triggerActions: PopoverTriggerFlags.none,

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

@@ -77,7 +77,7 @@ class _GridGroupCell extends StatelessWidget {
     }
 
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(fieldInfo.name),
         leftIcon: svgWidget(

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

@@ -103,7 +103,7 @@ class _GridPropertyCellState extends State<_GridPropertyCell> {
     );
 
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: _editFieldButton(context, checkmark),
     );
   }

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

@@ -68,7 +68,7 @@ class _SettingItem extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     return SizedBox(
-      height: GridSize.typeOptionItemHeight,
+      height: GridSize.popoverItemHeight,
       child: FlowyButton(
         text: FlowyText.medium(
           action.title(),

+ 2 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart

@@ -1,3 +1,4 @@
+import 'package:app_flowy/plugins/grid/presentation/widgets/toolbar/sort_button.dart';
 import 'package:flutter/material.dart';
 
 import '../../../application/field/field_controller.dart';
@@ -27,6 +28,7 @@ class GridToolbar extends StatelessWidget {
           SizedBox(width: GridSize.leadingHeaderPadding),
           const Spacer(),
           const FilterButton(),
+          const SortButton(),
           const SettingButton(),
         ],
       ),

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

@@ -45,7 +45,7 @@ class _SettingButtonState extends State<SettingButton> {
         return AppFlowyPopover(
           controller: _popoverController,
           constraints: BoxConstraints.loose(const Size(260, 400)),
-          direction: PopoverDirection.leftWithTopAligned,
+          direction: PopoverDirection.bottomWithLeftAligned,
           offset: const Offset(0, 10),
           margin: EdgeInsets.zero,
           triggerActions: PopoverTriggerFlags.none,

+ 80 - 0
frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/sort_button.dart

@@ -0,0 +1,80 @@
+import 'package:app_flowy/generated/locale_keys.g.dart';
+import 'package:app_flowy/plugins/grid/application/sort/sort_menu_bloc.dart';
+import 'package:app_flowy/plugins/grid/presentation/widgets/sort/create_sort_list.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/theme_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';
+
+class SortButton extends StatefulWidget {
+  const SortButton({Key? key}) : super(key: key);
+
+  @override
+  State<SortButton> createState() => _SortButtonState();
+}
+
+class _SortButtonState extends State<SortButton> {
+  final _popoverController = PopoverController();
+
+  @override
+  Widget build(BuildContext context) {
+    return BlocBuilder<SortMenuBloc, SortMenuState>(
+      builder: (context, state) {
+        final textColor = state.sortInfos.isEmpty
+            ? null
+            : Theme.of(context).colorScheme.primary;
+
+        return wrapPopover(
+          context,
+          SizedBox(
+            height: 26,
+            child: FlowyTextButton(
+              LocaleKeys.grid_settings_sort.tr(),
+              fontSize: 13,
+              fontColor: textColor,
+              fillColor: Colors.transparent,
+              hoverColor: AFThemeExtension.of(context).lightGreyHover,
+              padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2),
+              onPressed: () {
+                final bloc = context.read<SortMenuBloc>();
+                if (bloc.state.sortInfos.isEmpty) {
+                  _popoverController.show();
+                } else {
+                  bloc.add(const SortMenuEvent.toggleMenu());
+                }
+              },
+            ),
+          ),
+        );
+      },
+    );
+  }
+
+  Widget wrapPopover(BuildContext buildContext, Widget child) {
+    return AppFlowyPopover(
+      controller: _popoverController,
+      direction: PopoverDirection.bottomWithLeftAligned,
+      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<SortMenuBloc>();
+        return GridCreateSortList(
+          viewId: bloc.viewId,
+          fieldController: bloc.fieldController,
+          onClosed: () => _popoverController.close(),
+          onCreateSort: () {
+            if (!bloc.state.isVisible) {
+              bloc.add(const SortMenuEvent.toggleMenu());
+            }
+          },
+        );
+      },
+    );
+  }
+}

+ 20 - 27
frontend/app_flowy/packages/flowy_infra/pubspec.lock

@@ -7,7 +7,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.8.2"
+    version: "2.9.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -21,21 +21,14 @@ packages:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
-  charcode:
-    dependency: transitive
-    description:
-      name: charcode
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.3.1"
+    version: "1.2.1"
   clock:
     dependency: transitive
     description:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   collection:
     dependency: transitive
     description:
@@ -49,14 +42,14 @@ packages:
       name: crypto
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.1"
+    version: "3.0.2"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -75,7 +68,7 @@ packages:
       name: flutter_svg
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.4"
+    version: "1.1.6"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -87,35 +80,35 @@ packages:
       name: lints
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0"
+    version: "2.0.1"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.11"
+    version: "0.12.12"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.4"
+    version: "0.1.5"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.0"
   path:
     dependency: transitive
     description:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.1"
+    version: "1.8.2"
   path_drawing:
     dependency: transitive
     description:
@@ -136,7 +129,7 @@ packages:
       name: petitparser
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "5.0.0"
+    version: "5.1.0"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -148,7 +141,7 @@ packages:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.2"
+    version: "1.9.0"
   stack_trace:
     dependency: transitive
     description:
@@ -169,21 +162,21 @@ packages:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.2.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.9"
+    version: "0.4.12"
   textstyle_extensions:
     dependency: "direct main"
     description:
@@ -197,21 +190,21 @@ packages:
       name: time
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0"
+    version: "2.1.3"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   uuid:
     dependency: "direct main"
     description:
       name: uuid
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.4"
+    version: "3.0.7"
   vector_math:
     dependency: transitive
     description:
@@ -227,5 +220,5 @@ packages:
     source: hosted
     version: "6.1.0"
 sdks:
-  dart: ">=2.17.0 <3.0.0"
+  dart: ">=2.18.0 <3.0.0"
   flutter: ">=2.11.0-0.1.pre"

+ 29 - 36
frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock

@@ -7,7 +7,7 @@ packages:
       name: animations
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.1"
+    version: "2.0.7"
   appflowy_popover:
     dependency: transitive
     description:
@@ -21,7 +21,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.8.2"
+    version: "2.9.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -35,21 +35,14 @@ packages:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
-  charcode:
-    dependency: transitive
-    description:
-      name: charcode
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.3.1"
+    version: "1.2.1"
   clock:
     dependency: transitive
     description:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   collection:
     dependency: transitive
     description:
@@ -63,35 +56,35 @@ packages:
       name: crypto
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.1"
+    version: "3.0.2"
   cupertino_icons:
     dependency: "direct main"
     description:
       name: cupertino_icons
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.0.3"
+    version: "1.0.5"
   dartz:
     dependency: transitive
     description:
       name: dartz
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.10.0-nullsafety.2"
+    version: "0.10.1"
   equatable:
     dependency: transitive
     description:
       name: equatable
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.3"
+    version: "2.0.5"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   flowy_infra:
     dependency: transitive
     description:
@@ -138,7 +131,7 @@ packages:
       name: flutter_svg
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.4"
+    version: "1.1.6"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -162,42 +155,42 @@ packages:
       name: lint
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.5.3"
+    version: "1.10.0"
   lints:
     dependency: transitive
     description:
       name: lints
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0"
+    version: "2.0.1"
   loading_indicator:
     dependency: transitive
     description:
       name: loading_indicator
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.1"
+    version: "3.1.0"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.11"
+    version: "0.12.12"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.4"
+    version: "0.1.5"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.0"
   nested:
     dependency: transitive
     description:
@@ -211,7 +204,7 @@ packages:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.1"
+    version: "1.8.2"
   path_drawing:
     dependency: transitive
     description:
@@ -232,21 +225,21 @@ packages:
       name: petitparser
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "5.0.0"
+    version: "5.1.0"
   plugin_platform_interface:
     dependency: transitive
     description:
       name: plugin_platform_interface
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.1"
+    version: "2.1.3"
   provider:
     dependency: "direct main"
     description:
       name: provider
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "6.0.1"
+    version: "6.0.5"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -258,7 +251,7 @@ packages:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.2"
+    version: "1.9.0"
   stack_trace:
     dependency: transitive
     description:
@@ -279,7 +272,7 @@ packages:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   styled_widget:
     dependency: transitive
     description:
@@ -293,14 +286,14 @@ packages:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.2.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.9"
+    version: "0.4.12"
   textstyle_extensions:
     dependency: transitive
     description:
@@ -314,21 +307,21 @@ packages:
       name: time
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0"
+    version: "2.1.3"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   uuid:
     dependency: transitive
     description:
       name: uuid
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.4"
+    version: "3.0.7"
   vector_math:
     dependency: transitive
     description:
@@ -344,5 +337,5 @@ packages:
     source: hosted
     version: "6.1.0"
 sdks:
-  dart: ">=2.17.0 <3.0.0"
-  flutter: ">=2.11.0-0.1.pre"
+  dart: ">=2.18.0 <3.0.0"
+  flutter: ">=3.0.0"

+ 13 - 20
frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock

@@ -7,7 +7,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.8.2"
+    version: "2.9.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -21,21 +21,14 @@ packages:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
-  charcode:
-    dependency: transitive
-    description:
-      name: charcode
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.3.1"
+    version: "1.2.1"
   clock:
     dependency: transitive
     description:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   collection:
     dependency: transitive
     description:
@@ -49,7 +42,7 @@ packages:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -80,35 +73,35 @@ packages:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.11"
+    version: "0.12.12"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.4"
+    version: "0.1.5"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.0"
   path:
     dependency: transitive
     description:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.1"
+    version: "1.8.2"
   plugin_platform_interface:
     dependency: "direct main"
     description:
       name: plugin_platform_interface
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.1"
+    version: "2.1.3"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -120,7 +113,7 @@ packages:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.2"
+    version: "1.9.0"
   stack_trace:
     dependency: transitive
     description:
@@ -141,21 +134,21 @@ packages:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.2.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.9"
+    version: "0.4.12"
   vector_math:
     dependency: transitive
     description:

+ 13 - 20
frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock

@@ -7,7 +7,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.8.2"
+    version: "2.9.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -21,21 +21,14 @@ packages:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
-  charcode:
-    dependency: transitive
-    description:
-      name: charcode
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.3.1"
+    version: "1.2.1"
   clock:
     dependency: transitive
     description:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   collection:
     dependency: transitive
     description:
@@ -49,7 +42,7 @@ packages:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   flowy_infra_ui_platform_interface:
     dependency: "direct main"
     description:
@@ -99,35 +92,35 @@ packages:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.11"
+    version: "0.12.12"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.4"
+    version: "0.1.5"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.0"
   path:
     dependency: transitive
     description:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.1"
+    version: "1.8.2"
   plugin_platform_interface:
     dependency: transitive
     description:
       name: plugin_platform_interface
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.1"
+    version: "2.1.3"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -139,7 +132,7 @@ packages:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.2"
+    version: "1.9.0"
   stack_trace:
     dependency: transitive
     description:
@@ -160,21 +153,21 @@ packages:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.2.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.9"
+    version: "0.4.12"
   vector_math:
     dependency: transitive
     description:

+ 1 - 1
frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart

@@ -45,7 +45,7 @@ class AppFlowyPopover extends StatelessWidget {
       windowPadding: windowPadding,
       popupBuilder: (context) {
         final child = popupBuilder(context);
-        debugPrint("show popover: $child");
+        debugPrint("Show popover: $child");
         return _PopoverContainer(
           constraints: constraints,
           margin: margin,

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

@@ -18,6 +18,8 @@ class FlowyButton extends StatelessWidget {
   final BorderRadius? radius;
   final BoxDecoration? decoration;
   final bool useIntrinsicWidth;
+  final bool disable;
+  final double disableOpacity;
 
   const FlowyButton({
     Key? key,
@@ -32,23 +34,29 @@ class FlowyButton extends StatelessWidget {
     this.radius,
     this.decoration,
     this.useIntrinsicWidth = false,
+    this.disable = false,
+    this.disableOpacity = 0.5,
   }) : super(key: key);
 
   @override
   Widget build(BuildContext context) {
-    return GestureDetector(
-      behavior: HitTestBehavior.opaque,
-      onTap: onTap,
-      child: FlowyHover(
-        style: HoverStyle(
-          borderRadius: radius ?? Corners.s6Border,
-          hoverColor: hoverColor ?? Theme.of(context).colorScheme.secondary,
+    if (!disable) {
+      return GestureDetector(
+        behavior: HitTestBehavior.opaque,
+        onTap: onTap,
+        child: FlowyHover(
+          style: HoverStyle(
+            borderRadius: radius ?? Corners.s6Border,
+            hoverColor: hoverColor ?? Theme.of(context).colorScheme.secondary,
+          ),
+          onHover: onHover,
+          isSelected: () => isSelected,
+          builder: (context, onHover) => _render(),
         ),
-        onHover: onHover,
-        isSelected: () => isSelected,
-        builder: (context, onHover) => _render(),
-      ),
-    );
+      );
+    } else {
+      return Opacity(opacity: disableOpacity, child: _render());
+    }
   }
 
   Widget _render() {
@@ -63,7 +71,7 @@ class FlowyButton extends StatelessWidget {
     children.add(Expanded(child: text));
 
     if (rightIcon != null) {
-      children.add(const HSpace(10));
+      children.add(const HSpace(6));
       // No need to define the size of rightIcon. Just use its intrinsic width
       children.add(rightIcon!);
     }

+ 28 - 35
frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock

@@ -7,7 +7,7 @@ packages:
       name: animations
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.1"
+    version: "2.0.7"
   appflowy_popover:
     dependency: "direct main"
     description:
@@ -21,7 +21,7 @@ packages:
       name: async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.8.2"
+    version: "2.9.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -35,21 +35,14 @@ packages:
       name: characters
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
-  charcode:
-    dependency: transitive
-    description:
-      name: charcode
-      url: "https://pub.dartlang.org"
-    source: hosted
-    version: "1.3.1"
+    version: "1.2.1"
   clock:
     dependency: transitive
     description:
       name: clock
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   collection:
     dependency: transitive
     description:
@@ -63,28 +56,28 @@ packages:
       name: crypto
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.1"
+    version: "3.0.2"
   dartz:
     dependency: "direct main"
     description:
       name: dartz
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.10.0-nullsafety.2"
+    version: "0.10.1"
   equatable:
     dependency: "direct main"
     description:
       name: equatable
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.3"
+    version: "2.0.5"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   flowy_infra:
     dependency: "direct main"
     description:
@@ -124,7 +117,7 @@ packages:
       name: flutter_svg
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.4"
+    version: "1.1.6"
   flutter_test:
     dependency: "direct dev"
     description: flutter
@@ -148,42 +141,42 @@ packages:
       name: lint
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.5.3"
+    version: "1.10.0"
   lints:
     dependency: transitive
     description:
       name: lints
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0"
+    version: "2.0.1"
   loading_indicator:
     dependency: "direct main"
     description:
       name: loading_indicator
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.1"
+    version: "3.1.0"
   matcher:
     dependency: transitive
     description:
       name: matcher
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.12.11"
+    version: "0.12.12"
   material_color_utilities:
     dependency: transitive
     description:
       name: material_color_utilities
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.1.4"
+    version: "0.1.5"
   meta:
     dependency: transitive
     description:
       name: meta
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.7.0"
+    version: "1.8.0"
   nested:
     dependency: transitive
     description:
@@ -197,7 +190,7 @@ packages:
       name: path
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.1"
+    version: "1.8.2"
   path_drawing:
     dependency: transitive
     description:
@@ -218,21 +211,21 @@ packages:
       name: petitparser
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "5.0.0"
+    version: "5.1.0"
   plugin_platform_interface:
     dependency: transitive
     description:
       name: plugin_platform_interface
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.1"
+    version: "2.1.3"
   provider:
     dependency: "direct main"
     description:
       name: provider
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "6.0.1"
+    version: "6.0.5"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -244,7 +237,7 @@ packages:
       name: source_span
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.8.2"
+    version: "1.9.0"
   stack_trace:
     dependency: transitive
     description:
@@ -265,7 +258,7 @@ packages:
       name: string_scanner
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.1.0"
+    version: "1.1.1"
   styled_widget:
     dependency: "direct main"
     description:
@@ -279,14 +272,14 @@ packages:
       name: term_glyph
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.2.0"
+    version: "1.2.1"
   test_api:
     dependency: transitive
     description:
       name: test_api
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "0.4.9"
+    version: "0.4.12"
   textstyle_extensions:
     dependency: "direct main"
     description:
@@ -300,21 +293,21 @@ packages:
       name: time
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "2.0.0"
+    version: "2.1.3"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "1.3.0"
+    version: "1.3.1"
   uuid:
     dependency: transitive
     description:
       name: uuid
       url: "https://pub.dartlang.org"
     source: hosted
-    version: "3.0.4"
+    version: "3.0.7"
   vector_math:
     dependency: transitive
     description:
@@ -330,5 +323,5 @@ packages:
     source: hosted
     version: "6.1.0"
 sdks:
-  dart: ">=2.17.0 <3.0.0"
-  flutter: ">=2.11.0-0.1.pre"
+  dart: ">=2.18.0 <3.0.0"
+  flutter: ">=3.0.0"

+ 490 - 0
frontend/app_flowy/packages/flowy_sdk/pubspec.lock

@@ -0,0 +1,490 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  _fe_analyzer_shared:
+    dependency: transitive
+    description:
+      name: _fe_analyzer_shared
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "50.0.0"
+  analyzer:
+    dependency: transitive
+    description:
+      name: analyzer
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.2.0"
+  args:
+    dependency: transitive
+    description:
+      name: args
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.1"
+  async:
+    dependency: transitive
+    description:
+      name: async
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.9.0"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  build:
+    dependency: transitive
+    description:
+      name: build
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.1"
+  build_config:
+    dependency: transitive
+    description:
+      name: build_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.1"
+  build_daemon:
+    dependency: transitive
+    description:
+      name: build_daemon
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
+  build_resolvers:
+    dependency: transitive
+    description:
+      name: build_resolvers
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  build_runner:
+    dependency: "direct dev"
+    description:
+      name: build_runner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.3"
+  build_runner_core:
+    dependency: transitive
+    description:
+      name: build_runner_core
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "7.2.7"
+  built_collection:
+    dependency: transitive
+    description:
+      name: built_collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.1"
+  built_value:
+    dependency: transitive
+    description:
+      name: built_value
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "8.4.2"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.1"
+  checked_yaml:
+    dependency: transitive
+    description:
+      name: checked_yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.2"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.1"
+  code_builder:
+    dependency: transitive
+    description:
+      name: code_builder
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.4.0"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.16.0"
+  convert:
+    dependency: transitive
+    description:
+      name: convert
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.1"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.2"
+  dart_style:
+    dependency: transitive
+    description:
+      name: dart_style
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.4"
+  dartz:
+    dependency: "direct main"
+    description:
+      name: dartz
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.10.1"
+  fake_async:
+    dependency: transitive
+    description:
+      name: fake_async
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.1"
+  ffi:
+    dependency: "direct main"
+    description:
+      name: ffi
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.1"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.4"
+  fixnum:
+    dependency: transitive
+    description:
+      name: fixnum
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_lints:
+    dependency: "direct dev"
+    description:
+      name: flutter_lints
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  flutter_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  freezed:
+    dependency: "direct dev"
+    description:
+      name: freezed
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.2"
+  freezed_annotation:
+    dependency: "direct main"
+    description:
+      name: freezed_annotation
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+  frontend_server_client:
+    dependency: transitive
+    description:
+      name: frontend_server_client
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.2.0"
+  glob:
+    dependency: transitive
+    description:
+      name: glob
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  graphs:
+    dependency: transitive
+    description:
+      name: graphs
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+  http_multi_server:
+    dependency: transitive
+    description:
+      name: http_multi_server
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.2.1"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.0.2"
+  io:
+    dependency: transitive
+    description:
+      name: io
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  isolates:
+    dependency: "direct main"
+    description:
+      name: isolates
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.3+8"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.5"
+  json_annotation:
+    dependency: transitive
+    description:
+      name: json_annotation
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.7.0"
+  lints:
+    dependency: transitive
+    description:
+      name: lints
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  logger:
+    dependency: "direct main"
+    description:
+      name: logger
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  logging:
+    dependency: transitive
+    description:
+      name: logging
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.0"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.12.12"
+  material_color_utilities:
+    dependency: transitive
+    description:
+      name: material_color_utilities
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.5"
+  meta:
+    dependency: transitive
+    description:
+      name: meta
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.8.0"
+  mime:
+    dependency: transitive
+    description:
+      name: mime
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  package_config:
+    dependency: transitive
+    description:
+      name: package_config
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.8.2"
+  pool:
+    dependency: transitive
+    description:
+      name: pool
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.5.1"
+  protobuf:
+    dependency: "direct main"
+    description:
+      name: protobuf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
+  pub_semver:
+    dependency: transitive
+    description:
+      name: pub_semver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.3"
+  pubspec_parse:
+    dependency: transitive
+    description:
+      name: pubspec_parse
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.1"
+  shelf:
+    dependency: transitive
+    description:
+      name: shelf
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.4.0"
+  shelf_web_socket:
+    dependency: transitive
+    description:
+      name: shelf_web_socket
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.99"
+  source_gen:
+    dependency: transitive
+    description:
+      name: source_gen
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.6"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.9.0"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.10.0"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  stream_transform:
+    dependency: transitive
+    description:
+      name: stream_transform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.1"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.2.1"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.4.12"
+  timing:
+    dependency: transitive
+    description:
+      name: timing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.1"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  watcher:
+    dependency: transitive
+    description:
+      name: watcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.2"
+  web_socket_channel:
+    dependency: transitive
+    description:
+      name: web_socket_channel
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.2.0"
+  yaml:
+    dependency: transitive
+    description:
+      name: yaml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.1"
+sdks:
+  dart: ">=2.18.0 <3.0.0"
+  flutter: ">=1.17.0"

+ 3 - 0
frontend/rust-lib/flowy-error/src/code.rs

@@ -157,6 +157,9 @@ pub enum ErrorCode {
 
     #[error("Out of bounds")]
     OutOfBounds = 52,
+
+    #[error("Sort id is empty")]
+    SortIdIsEmpty = 53,
 }
 
 impl ErrorCode {

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

@@ -2,7 +2,7 @@ use crate::entities::parser::NotEmptyStr;
 use crate::entities::{
     AlterFilterParams, AlterFilterPayloadPB, AlterSortParams, AlterSortPayloadPB, DeleteFilterParams,
     DeleteFilterPayloadPB, DeleteGroupParams, DeleteGroupPayloadPB, DeleteSortParams, DeleteSortPayloadPB,
-    InsertGroupParams, InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGroupConfigurationPB,
+    InsertGroupParams, InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGroupConfigurationPB, RepeatedSortPB,
 };
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
@@ -25,6 +25,9 @@ pub struct GridSettingPB {
 
     #[pb(index = 4)]
     pub group_configurations: RepeatedGroupConfigurationPB,
+
+    #[pb(index = 5)]
+    pub sorts: RepeatedSortPB,
 }
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]

+ 42 - 8
frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs

@@ -1,13 +1,14 @@
 use crate::entities::parser::NotEmptyStr;
 use crate::entities::FieldType;
 use crate::services::sort::SortType;
+use std::sync::Arc;
 
 use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
 use flowy_error::ErrorCode;
 use grid_rev_model::{FieldTypeRevision, SortCondition, SortRevision};
 
 #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
-pub struct GridSortPB {
+pub struct SortPB {
     #[pb(index = 1)]
     pub id: String,
 
@@ -21,7 +22,7 @@ pub struct GridSortPB {
     pub condition: GridSortConditionPB,
 }
 
-impl std::convert::From<&SortRevision> for GridSortPB {
+impl std::convert::From<&SortRevision> for SortPB {
     fn from(sort_rev: &SortRevision) -> Self {
         Self {
             id: sort_rev.id.clone(),
@@ -32,6 +33,26 @@ impl std::convert::From<&SortRevision> for GridSortPB {
     }
 }
 
+#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)]
+pub struct RepeatedSortPB {
+    #[pb(index = 1)]
+    pub items: Vec<SortPB>,
+}
+
+impl std::convert::From<Vec<Arc<SortRevision>>> for RepeatedSortPB {
+    fn from(revs: Vec<Arc<SortRevision>>) -> Self {
+        RepeatedSortPB {
+            items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(),
+        }
+    }
+}
+
+impl std::convert::From<Vec<SortPB>> for RepeatedSortPB {
+    fn from(items: Vec<SortPB>) -> Self {
+        Self { items }
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)]
 #[repr(u8)]
 pub enum GridSortConditionPB {
@@ -64,7 +85,7 @@ pub struct AlterSortPayloadPB {
     #[pb(index = 3)]
     pub field_type: FieldType,
 
-    /// Create a new filter if the filter_id is None
+    /// Create a new sort if the sort_id is None
     #[pb(index = 4, one_of)]
     pub sort_id: Option<String>,
 
@@ -83,9 +104,10 @@ impl TryInto<AlterSortParams> for AlterSortPayloadPB {
         let field_id = NotEmptyStr::parse(self.field_id)
             .map_err(|_| ErrorCode::FieldIdIsEmpty)?
             .0;
+
         let sort_id = match self.sort_id {
             None => None,
-            Some(filter_id) => Some(NotEmptyStr::parse(filter_id).map_err(|_| ErrorCode::FilterIdIsEmpty)?.0),
+            Some(sort_id) => Some(NotEmptyStr::parse(sort_id).map_err(|_| ErrorCode::SortIdIsEmpty)?.0),
         };
 
         Ok(AlterSortParams {
@@ -164,16 +186,25 @@ pub struct SortChangesetNotificationPB {
     pub view_id: String,
 
     #[pb(index = 2)]
-    pub insert_sorts: Vec<GridSortPB>,
+    pub insert_sorts: Vec<SortPB>,
 
     #[pb(index = 3)]
-    pub delete_sorts: Vec<GridSortPB>,
+    pub delete_sorts: Vec<SortPB>,
 
     #[pb(index = 4)]
-    pub update_sorts: Vec<GridSortPB>,
+    pub update_sorts: Vec<SortPB>,
 }
 
 impl SortChangesetNotificationPB {
+    pub fn new(view_id: String) -> Self {
+        Self {
+            view_id,
+            insert_sorts: vec![],
+            delete_sorts: vec![],
+            update_sorts: vec![],
+        }
+    }
+
     pub fn extend(&mut self, other: SortChangesetNotificationPB) {
         self.insert_sorts.extend(other.insert_sorts);
         self.delete_sorts.extend(other.delete_sorts);
@@ -194,8 +225,11 @@ pub struct ReorderAllRowsPB {
 #[derive(Debug, Default, ProtoBuf)]
 pub struct ReorderSingleRowPB {
     #[pb(index = 1)]
-    pub old_index: i32,
+    pub row_id: String,
 
     #[pb(index = 2)]
+    pub old_index: i32,
+
+    #[pb(index = 3)]
     pub new_index: i32,
 }

+ 24 - 0
frontend/rust-lib/flowy-grid/src/event_handler.rs

@@ -81,6 +81,30 @@ pub(crate) async fn get_all_filters_handler(
     data_result(filters)
 }
 
+#[tracing::instrument(level = "trace", skip(data, manager), err)]
+pub(crate) async fn get_all_sorts_handler(
+    data: AFPluginData<GridIdPB>,
+    manager: AFPluginState<Arc<GridManager>>,
+) -> DataResult<RepeatedSortPB, FlowyError> {
+    let grid_id: GridIdPB = data.into_inner();
+    let editor = manager.open_grid(grid_id.as_ref()).await?;
+    let sorts = RepeatedSortPB {
+        items: editor.get_all_sorts(grid_id.as_ref()).await?,
+    };
+    data_result(sorts)
+}
+
+#[tracing::instrument(level = "trace", skip(data, manager), err)]
+pub(crate) async fn delete_all_sorts_handler(
+    data: AFPluginData<GridIdPB>,
+    manager: AFPluginState<Arc<GridManager>>,
+) -> Result<(), FlowyError> {
+    let grid_id: GridIdPB = data.into_inner();
+    let editor = manager.open_grid(grid_id.as_ref()).await?;
+    let _ = editor.delete_all_sorts(grid_id.as_ref()).await?;
+    Ok(())
+}
+
 #[tracing::instrument(level = "trace", skip(data, manager), err)]
 pub(crate) async fn get_fields_handler(
     data: AFPluginData<GetFieldPayloadPB>,

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

@@ -13,6 +13,8 @@ pub fn init(grid_manager: Arc<GridManager>) -> AFPlugin {
         .event(GridEvent::GetGridSetting, get_grid_setting_handler)
         .event(GridEvent::UpdateGridSetting, update_grid_setting_handler)
         .event(GridEvent::GetAllFilters, get_all_filters_handler)
+        .event(GridEvent::GetAllSorts, get_all_sorts_handler)
+        .event(GridEvent::DeleteAllSorts, delete_all_sorts_handler)
         // Field
         .event(GridEvent::GetFields, get_fields_handler)
         .event(GridEvent::UpdateField, update_field_handler)
@@ -75,6 +77,12 @@ pub enum GridEvent {
     #[event(input = "GridIdPB", output = "RepeatedFilterPB")]
     GetAllFilters = 4,
 
+    #[event(input = "GridIdPB", output = "RepeatedSortPB")]
+    GetAllSorts = 5,
+
+    #[event(input = "GridIdPB")]
+    DeleteAllSorts = 6,
+
     /// [GetFields] event is used to get the grid's settings.
     ///
     /// The event handler accepts a [GetFieldPayloadPB] and returns a [RepeatedFieldPB]

+ 9 - 5
frontend/rust-lib/flowy-grid/src/manager.rs

@@ -92,7 +92,6 @@ impl GridManager {
         Ok(())
     }
 
-    #[tracing::instrument(level = "debug", skip_all, err)]
     pub async fn open_grid<T: AsRef<str>>(&self, grid_id: T) -> FlowyResult<Arc<GridRevisionEditor>> {
         let grid_id = grid_id.as_ref();
         let _ = self.migration.run_v1_migration(grid_id).await;
@@ -103,16 +102,20 @@ impl GridManager {
     pub async fn close_grid<T: AsRef<str>>(&self, grid_id: T) -> FlowyResult<()> {
         let grid_id = grid_id.as_ref();
         tracing::Span::current().record("grid_id", &grid_id);
-
         self.grid_editors.write().await.remove(grid_id).await;
-        // self.task_scheduler.write().await.unregister_handler(grid_id);
         Ok(())
     }
 
     // #[tracing::instrument(level = "debug", skip(self), err)]
     pub async fn get_grid_editor(&self, grid_id: &str) -> FlowyResult<Arc<GridRevisionEditor>> {
-        match self.grid_editors.read().await.get(grid_id) {
-            None => Err(FlowyError::internal().context("Should call open_grid function first")),
+        let read_guard = self.grid_editors.read().await;
+        let editor = read_guard.get(grid_id);
+        match editor {
+            None => {
+                // Drop the read_guard ASAP in case of the following read/write lock
+                drop(read_guard);
+                self.open_grid(grid_id).await
+            }
             Some(editor) => Ok(editor),
         }
     }
@@ -125,6 +128,7 @@ impl GridManager {
         let mut grid_editors = self.grid_editors.write().await;
         let db_pool = self.grid_user.db_pool()?;
         let editor = self.make_grid_rev_editor(grid_id, db_pool).await?;
+        tracing::trace!("Open grid: {}", grid_id);
         grid_editors.insert(grid_id.to_string(), editor.clone());
         Ok(editor)
     }

+ 10 - 12
frontend/rust-lib/flowy-grid/src/services/field/type_options/type_option_cell.rs

@@ -4,9 +4,9 @@ use crate::services::cell::{
     FromCellChangesetString, FromCellString, TypeCellData,
 };
 use crate::services::field::{
-    default_order, CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, MultiSelectTypeOptionPB,
-    NumberTypeOptionPB, RichTextTypeOptionPB, SingleSelectTypeOptionPB, TypeOption, TypeOptionCellData,
-    TypeOptionCellDataCompare, TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOptionPB,
+    CheckboxTypeOptionPB, ChecklistTypeOptionPB, DateTypeOptionPB, MultiSelectTypeOptionPB, NumberTypeOptionPB,
+    RichTextTypeOptionPB, SingleSelectTypeOptionPB, TypeOption, TypeOptionCellData, TypeOptionCellDataCompare,
+    TypeOptionCellDataFilter, TypeOptionTransform, URLTypeOptionPB,
 };
 use crate::services::filter::FilterType;
 use flowy_error::FlowyResult;
@@ -221,15 +221,13 @@ where
 
     fn handle_cell_compare(&self, left_cell_data: &str, right_cell_data: &str, field_rev: &FieldRevision) -> Ordering {
         let field_type: FieldType = field_rev.ty.into();
-        let left = self.get_decoded_cell_data(left_cell_data.to_owned(), &field_type, field_rev);
-        let right = self.get_decoded_cell_data(right_cell_data.to_owned(), &field_type, field_rev);
-
-        match (left, right) {
-            (Ok(left), Ok(right)) => self.apply_cmp(&left, &right),
-            (Ok(_), Err(_)) => Ordering::Greater,
-            (Err(_), Ok(_)) => Ordering::Less,
-            (Err(_), Err(_)) => default_order(),
-        }
+        let left = self
+            .get_decoded_cell_data(left_cell_data.to_owned(), &field_type, field_rev)
+            .unwrap_or_default();
+        let right = self
+            .get_decoded_cell_data(right_cell_data.to_owned(), &field_type, field_rev)
+            .unwrap_or_default();
+        self.apply_cmp(&left, &right)
     }
 
     fn handle_cell_filter(

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

@@ -115,7 +115,7 @@ impl FilterController {
             .collect::<HashMap<String, Arc<FieldRevision>>>()
     }
 
-    #[tracing::instrument(name = "receive_task_result", level = "trace", skip_all, fields(filter_result), err)]
+    #[tracing::instrument(name = "process_filter_task", level = "trace", skip_all, fields(filter_result), err)]
     pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> {
         let event_type = FilterEvent::from_str(predicate).unwrap();
         match event_type {

+ 22 - 8
frontend/rust-lib/flowy-grid/src/services/grid_editor.rs

@@ -629,14 +629,6 @@ impl GridRevisionEditor {
         self.view_manager.get_filters(&filter_id).await
     }
 
-    pub async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
-        self.view_manager.insert_or_update_group(params).await
-    }
-
-    pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
-        self.view_manager.delete_group(params).await
-    }
-
     pub async fn create_or_update_filter(&self, params: AlterFilterParams) -> FlowyResult<()> {
         self.view_manager.create_or_update_filter(params).await?;
         Ok(())
@@ -647,6 +639,20 @@ impl GridRevisionEditor {
         Ok(())
     }
 
+    pub async fn get_all_sorts(&self, view_id: &str) -> FlowyResult<Vec<SortPB>> {
+        Ok(self
+            .view_manager
+            .get_all_sorts(view_id)
+            .await?
+            .into_iter()
+            .map(|sort| SortPB::from(sort.as_ref()))
+            .collect())
+    }
+
+    pub async fn delete_all_sorts(&self, view_id: &str) -> FlowyResult<()> {
+        self.view_manager.delete_all_sorts(view_id).await
+    }
+
     pub async fn delete_sort(&self, params: DeleteSortParams) -> FlowyResult<()> {
         self.view_manager.delete_sort(params).await?;
         Ok(())
@@ -657,6 +663,14 @@ impl GridRevisionEditor {
         Ok(sort_rev)
     }
 
+    pub async fn insert_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
+        self.view_manager.insert_or_update_group(params).await
+    }
+
+    pub async fn delete_group(&self, params: DeleteGroupParams) -> FlowyResult<()> {
+        self.view_manager.delete_group(params).await
+    }
+
     pub async fn move_row(&self, params: MoveRowParams) -> FlowyResult<()> {
         let MoveRowParams {
             view_id: _,

+ 16 - 9
frontend/rust-lib/flowy-grid/src/services/sort/controller.rs

@@ -39,6 +39,7 @@ impl SortController {
     pub fn new<T>(
         view_id: &str,
         handler_id: &str,
+        sorts: Vec<Arc<SortRevision>>,
         delegate: T,
         task_scheduler: Arc<RwLock<TaskDispatcher>>,
         cell_data_cache: AtomicCellDataCache,
@@ -52,7 +53,7 @@ impl SortController {
             handler_id: handler_id.to_string(),
             delegate: Box::new(delegate),
             task_scheduler,
-            sorts: vec![],
+            sorts,
             cell_data_cache,
             row_index_cache: Default::default(),
             notifier,
@@ -72,7 +73,7 @@ impl SortController {
         self.gen_task(task_type, QualityOfService::Background).await;
     }
 
-    #[tracing::instrument(name = "receive_sort_task_result", level = "trace", skip_all, err)]
+    #[tracing::instrument(name = "process_sort_task", level = "trace", skip_all, err)]
     pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> {
         let event_type = SortEvent::from_str(predicate).unwrap();
         let mut row_revs = self.delegate.get_row_revs().await;
@@ -103,6 +104,7 @@ impl SortController {
                             return Ok(());
                         }
                         let notification = ReorderSingleRowResult {
+                            row_id,
                             view_id: self.view_id.clone(),
                             old_index: old_row_index,
                             new_index: new_row_index,
@@ -120,9 +122,6 @@ impl SortController {
 
     #[tracing::instrument(name = "schedule_sort_task", level = "trace", skip(self))]
     async fn gen_task(&self, task_type: SortEvent, qos: QualityOfService) {
-        if self.sorts.is_empty() {
-            return;
-        }
         let task_id = self.task_scheduler.read().await.next_task_id();
         let task = Task::new(&self.handler_id, task_id, TaskContent::Text(task_type.to_string()), qos);
         self.task_scheduler.write().await.add_task(task);
@@ -142,13 +141,19 @@ impl SortController {
         });
     }
 
+    pub async fn delete_all_sorts(&mut self) {
+        self.sorts.clear();
+        self.gen_task(SortEvent::SortDidChanged, QualityOfService::Background)
+            .await;
+    }
+
     pub async fn did_update_view_field_type_option(&self, _field_rev: &FieldRevision) {
         //
     }
 
     #[tracing::instrument(level = "trace", skip(self))]
     pub async fn did_receive_changes(&mut self, changeset: SortChangeset) -> SortChangesetNotificationPB {
-        let mut notification = SortChangesetNotificationPB::default();
+        let mut notification = SortChangesetNotificationPB::new(self.view_id.clone());
         if let Some(insert_sort) = changeset.insert_sort {
             if let Some(sort) = self.delegate.get_sort_rev(insert_sort).await {
                 notification.insert_sorts.push(sort.as_ref().into());
@@ -172,10 +177,11 @@ impl SortController {
             }
         }
 
-        if !notification.insert_sorts.is_empty() || !notification.delete_sorts.is_empty() {
-            self.gen_task(SortEvent::SortDidChanged, QualityOfService::Background)
+        if !notification.is_empty() {
+            self.gen_task(SortEvent::SortDidChanged, QualityOfService::UserInteractive)
                 .await;
         }
+        tracing::trace!("sort notification: {:?}", notification);
         notification
     }
 }
@@ -200,6 +206,7 @@ fn cmp_row(
         _ => default_order(),
     };
 
+    // The order is calculated by Ascending. So reverse the order if the SortCondition is descending.
     match sort.condition {
         SortCondition::Ascending => order,
         SortCondition::Descending => order.reverse(),
@@ -216,7 +223,7 @@ fn cmp_cell(
     match TypeOptionCellExt::new_with_cell_data_cache(field_rev.as_ref(), Some(cell_data_cache.clone()))
         .get_type_option_cell_data_handler(&field_type)
     {
-        None => Ordering::Less,
+        None => default_order(),
         Some(handler) => {
             let cal_order = || {
                 let left_cell_str = TypeCellData::try_from(left_cell).ok()?.into_inner();

+ 1 - 0
frontend/rust-lib/flowy-grid/src/services/sort/entities.rs

@@ -47,6 +47,7 @@ impl ReorderAllRowsResult {
 #[derive(Clone)]
 pub struct ReorderSingleRowResult {
     pub view_id: String,
+    pub row_id: String,
     pub old_index: usize,
     pub new_index: usize,
 }

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

@@ -54,6 +54,7 @@ impl GridViewChangedReceiverRunner {
                     }
                     GridViewChanged::ReorderSingleRowNotification(notification) => {
                         let reorder_row = ReorderSingleRowPB {
+                            row_id: notification.row_id,
                             old_index: notification.old_index as i32,
                             new_index: notification.new_index as i32,
                         };

+ 54 - 29
frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs

@@ -340,24 +340,6 @@ impl GridViewRevisionEditor {
         self.group_controller.read().await.field_id().to_string()
     }
 
-    pub async fn get_view_setting(&self) -> GridSettingPB {
-        let field_revs = self.delegate.get_field_revs(None).await;
-        make_grid_setting(&*self.pad.read().await, &field_revs)
-    }
-
-    pub async fn get_all_view_filters(&self) -> Vec<Arc<FilterRevision>> {
-        let field_revs = self.delegate.get_field_revs(None).await;
-        self.pad.read().await.get_all_filters(&field_revs)
-    }
-
-    pub async fn get_view_filters(&self, filter_type: &FilterType) -> Vec<Arc<FilterRevision>> {
-        let field_type_rev: FieldTypeRevision = filter_type.field_type.clone().into();
-        self.pad
-            .read()
-            .await
-            .get_filters(&filter_type.field_id, &field_type_rev)
-    }
-
     /// Initialize new group when grouping by a new field
     ///
     pub async fn initialize_new_group(&self, params: InsertGroupParams) -> FlowyResult<()> {
@@ -385,6 +367,18 @@ impl GridViewRevisionEditor {
         .await
     }
 
+    pub async fn get_view_setting(&self) -> GridSettingPB {
+        let field_revs = self.delegate.get_field_revs(None).await;
+        let grid_setting = make_grid_setting(&*self.pad.read().await, &field_revs);
+        grid_setting
+    }
+
+    pub async fn get_all_view_sorts(&self) -> Vec<Arc<SortRevision>> {
+        let field_revs = self.delegate.get_field_revs(None).await;
+        self.pad.read().await.get_all_sorts(&field_revs)
+    }
+
+    #[tracing::instrument(level = "trace", skip(self), err)]
     pub async fn insert_view_sort(&self, params: AlterSortParams) -> FlowyResult<SortRevision> {
         let sort_type = SortType::from(&params);
         let is_exist = params.sort_id.is_some();
@@ -420,14 +414,13 @@ impl GridViewRevisionEditor {
                 .did_receive_changes(SortChangeset::from_insert(sort_type))
                 .await
         };
-        drop(sort_controller);
-
         self.notify_did_update_sort(changeset).await;
+        drop(sort_controller);
         Ok(sort_rev)
     }
 
     pub async fn delete_view_sort(&self, params: DeleteSortParams) -> FlowyResult<()> {
-        let changeset = self
+        let notification = self
             .sort_controller
             .write()
             .await
@@ -441,10 +434,39 @@ impl GridViewRevisionEditor {
         })
         .await?;
 
-        self.notify_did_update_sort(changeset).await;
+        self.notify_did_update_sort(notification).await;
         Ok(())
     }
 
+    pub async fn delete_all_view_sorts(&self) -> FlowyResult<()> {
+        let all_sorts = self.get_all_view_sorts().await;
+        self.sort_controller.write().await.delete_all_sorts().await;
+        let _ = self
+            .modify(|pad| {
+                let changeset = pad.delete_all_sorts()?;
+                Ok(changeset)
+            })
+            .await?;
+
+        let mut notification = SortChangesetNotificationPB::new(self.view_id.clone());
+        notification.delete_sorts = all_sorts.into_iter().map(|sort| SortPB::from(sort.as_ref())).collect();
+        self.notify_did_update_sort(notification).await;
+        Ok(())
+    }
+
+    pub async fn get_all_view_filters(&self) -> Vec<Arc<FilterRevision>> {
+        let field_revs = self.delegate.get_field_revs(None).await;
+        self.pad.read().await.get_all_filters(&field_revs)
+    }
+
+    pub async fn get_view_filters(&self, filter_type: &FilterType) -> Vec<Arc<FilterRevision>> {
+        let field_type_rev: FieldTypeRevision = filter_type.field_type.clone().into();
+        self.pad
+            .read()
+            .await
+            .get_filters(&filter_type.field_id, &field_type_rev)
+    }
+
     #[tracing::instrument(level = "trace", skip(self), err)]
     pub async fn insert_view_filter(&self, params: AlterFilterParams) -> FlowyResult<()> {
         let filter_type = FilterType::from(&params);
@@ -605,16 +627,16 @@ impl GridViewRevisionEditor {
             .send();
     }
 
-    pub async fn notify_did_update_filter(&self, changeset: FilterChangesetNotificationPB) {
-        send_dart_notification(&changeset.view_id, GridDartNotification::DidUpdateFilter)
-            .payload(changeset)
+    pub async fn notify_did_update_filter(&self, notification: FilterChangesetNotificationPB) {
+        send_dart_notification(&notification.view_id, GridDartNotification::DidUpdateFilter)
+            .payload(notification)
             .send();
     }
 
-    pub async fn notify_did_update_sort(&self, changeset: SortChangesetNotificationPB) {
-        if !changeset.is_empty() {
-            send_dart_notification(&changeset.view_id, GridDartNotification::DidUpdateSort)
-                .payload(changeset)
+    pub async fn notify_did_update_sort(&self, notification: SortChangesetNotificationPB) {
+        if !notification.is_empty() {
+            send_dart_notification(&notification.view_id, GridDartNotification::DidUpdateSort)
+                .payload(notification)
                 .send();
         }
     }
@@ -764,6 +786,8 @@ async fn make_sort_controller(
     cell_data_cache: AtomicCellDataCache,
 ) -> Arc<RwLock<SortController>> {
     let handler_id = gen_handler_id();
+    let field_revs = delegate.get_field_revs(None).await;
+    let sorts = pad.read().await.get_all_sorts(&field_revs);
     let sort_delegate = GridViewSortDelegateImpl {
         editor_delegate: delegate.clone(),
         view_revision_pad: pad,
@@ -773,6 +797,7 @@ async fn make_sort_controller(
     let sort_controller = Arc::new(RwLock::new(SortController::new(
         view_id,
         &handler_id,
+        sorts,
         sort_delegate,
         task_scheduler.clone(),
         cell_data_cache,

+ 10 - 0
frontend/rust-lib/flowy-grid/src/services/view_editor/editor_manager.rs

@@ -138,11 +138,21 @@ impl GridViewManager {
         view_editor.delete_view_filter(params).await
     }
 
+    pub async fn get_all_sorts(&self, view_id: &str) -> FlowyResult<Vec<Arc<SortRevision>>> {
+        let view_editor = self.get_view_editor(view_id).await?;
+        Ok(view_editor.get_all_view_sorts().await)
+    }
+
     pub async fn create_or_update_sort(&self, params: AlterSortParams) -> FlowyResult<SortRevision> {
         let view_editor = self.get_view_editor(&params.view_id).await?;
         view_editor.insert_view_sort(params).await
     }
 
+    pub async fn delete_all_sorts(&self, view_id: &str) -> FlowyResult<()> {
+        let view_editor = self.get_view_editor(view_id).await?;
+        view_editor.delete_all_view_sorts().await
+    }
+
     pub async fn delete_sort(&self, params: DeleteSortParams) -> FlowyResult<()> {
         let view_editor = self.get_view_editor(&params.view_id).await?;
         view_editor.delete_view_sort(params).await

+ 4 - 2
frontend/rust-lib/flowy-grid/src/services/view_editor/trait_impl.rs

@@ -119,12 +119,14 @@ pub(crate) async fn apply_change(
 
 pub fn make_grid_setting(view_pad: &GridViewRevisionPad, field_revs: &[Arc<FieldRevision>]) -> GridSettingPB {
     let layout_type: GridLayout = view_pad.layout.clone().into();
-    let filter_configurations = view_pad.get_all_filters(field_revs);
+    let filters = view_pad.get_all_filters(field_revs);
     let group_configurations = view_pad.get_groups_by_field_revs(field_revs);
+    let sorts = view_pad.get_all_sorts(field_revs);
     GridSettingPB {
         layouts: GridLayoutPB::all(),
         layout_type,
-        filters: filter_configurations.into(),
+        filters: filters.into(),
+        sorts: sorts.into(),
         group_configurations: group_configurations.into(),
     }
 }

+ 1 - 3
frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs

@@ -1,12 +1,10 @@
 use flowy_grid::entities::FieldType;
-use std::sync::Arc;
-
 use flowy_grid::services::field::{
     ChecklistTypeOptionPB, DateCellChangeset, MultiSelectTypeOptionPB, SelectOptionPB, SingleSelectTypeOptionPB,
-    URLCellChangeset, URLCellData,
 };
 use flowy_grid::services::row::RowRevisionBuilder;
 use grid_rev_model::{FieldRevision, RowRevision};
+use std::sync::Arc;
 
 use strum::EnumCount;
 

+ 50 - 0
frontend/rust-lib/flowy-grid/tests/grid/sort_test/checkbox_and_text_test.rs

@@ -0,0 +1,50 @@
+use crate::grid::sort_test::script::{GridSortTest, SortScript::*};
+use flowy_grid::entities::FieldType;
+use grid_rev_model::SortCondition;
+
+#[tokio::test]
+async fn sort_checkbox_and_then_text_by_descending_test() {
+    let mut test = GridSortTest::new().await;
+    let checkbox_field = test.get_first_field_rev(FieldType::Checkbox);
+    let text_field = test.get_first_field_rev(FieldType::RichText);
+    let scripts = vec![
+        AssertCellContentOrder {
+            field_id: checkbox_field.id.clone(),
+            orders: vec!["Yes", "Yes", "No", "No", "No", "Yes"],
+        },
+        AssertCellContentOrder {
+            field_id: text_field.id.clone(),
+            orders: vec!["A", "", "C", "DA", "AE", "AE"],
+        },
+        // Insert checkbox sort
+        InsertSort {
+            field_rev: checkbox_field.clone(),
+            condition: SortCondition::Descending,
+        },
+        AssertCellContentOrder {
+            field_id: checkbox_field.id.clone(),
+            orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"],
+        },
+        AssertCellContentOrder {
+            field_id: text_field.id.clone(),
+            orders: vec!["A", "", "AE", "C", "DA", "AE"],
+        },
+        // Insert text sort. After inserting the text sort, the order of the rows
+        // will be changed.
+        // before: ["A", "", "AE", "C", "DA", "AE"]
+        // after: ["", "A", "AE", "AE", "C", "DA"]
+        InsertSort {
+            field_rev: text_field.clone(),
+            condition: SortCondition::Ascending,
+        },
+        AssertCellContentOrder {
+            field_id: checkbox_field.id.clone(),
+            orders: vec!["Yes", "Yes", "Yes", "No", "No", "No"],
+        },
+        AssertCellContentOrder {
+            field_id: text_field.id.clone(),
+            orders: vec!["", "A", "AE", "AE", "C", "DA"],
+        },
+    ];
+    test.run_scripts(scripts).await;
+}

+ 1 - 0
frontend/rust-lib/flowy-grid/tests/grid/sort_test/mod.rs

@@ -1,3 +1,4 @@
+mod checkbox_and_text_test;
 mod multi_sort_test;
 mod script;
 mod single_sort_test;

+ 1 - 1
frontend/rust-lib/flowy-revision/src/rev_manager.rs

@@ -125,7 +125,7 @@ impl<Connection: 'static> RevisionManager<Connection> {
         }
     }
 
-    #[tracing::instrument(level = "debug", skip_all, fields(deserializer, object) err)]
+    #[tracing::instrument(level = "trace", skip_all, fields(deserializer, object) err)]
     pub async fn initialize<B>(&mut self, _cloud: Option<Arc<dyn RevisionCloudService>>) -> FlowyResult<B::Output>
     where
         B: RevisionObjectDeserializer,

+ 11 - 4
frontend/rust-lib/flowy-sync/src/client_grid/view_revision_pad.rs

@@ -127,8 +127,8 @@ impl GridViewRevisionPad {
         })
     }
 
-    pub fn get_all_sorts(&self, field_revs: &[Arc<FieldRevision>]) -> Vec<Arc<SortRevision>> {
-        self.sorts.get_objects_by_field_revs(field_revs)
+    pub fn get_all_sorts(&self, _field_revs: &[Arc<FieldRevision>]) -> Vec<Arc<SortRevision>> {
+        self.sorts.get_all_objects()
     }
 
     /// For the moment, a field type only have one filter.
@@ -148,12 +148,12 @@ impl GridViewRevisionPad {
 
     pub fn insert_sort(
         &mut self,
-        sort_id: &str,
+        field_id: &str,
         sort_rev: SortRevision,
     ) -> CollaborateResult<Option<GridViewRevisionChangeset>> {
         self.modify(|view| {
             let field_type = sort_rev.field_type;
-            view.sorts.add_object(sort_id, &field_type, sort_rev);
+            view.sorts.add_object(field_id, &field_type, sort_rev);
             Ok(Some(()))
         })
     }
@@ -194,6 +194,13 @@ impl GridViewRevisionPad {
         })
     }
 
+    pub fn delete_all_sorts(&mut self) -> CollaborateResult<Option<GridViewRevisionChangeset>> {
+        self.modify(|view| {
+            view.sorts.clear();
+            Ok(Some(()))
+        })
+    }
+
     pub fn get_all_filters(&self, field_revs: &[Arc<FieldRevision>]) -> Vec<Arc<FilterRevision>> {
         self.filters.get_objects_by_field_revs(field_revs)
     }

+ 2 - 0
frontend/rust-lib/flowy-task/src/scheduler.rs

@@ -67,6 +67,7 @@ impl TaskDispatcher {
         let content = task.content.take()?;
         if let Some(handler) = self.handlers.get(&task.handler_id) {
             task.set_state(TaskState::Processing);
+            tracing::trace!("Run {} task with content: {:?}", handler.handler_name(), content);
             match tokio::time::timeout(self.timeout, handler.run(content)).await {
                 Ok(result) => match result {
                     Ok(_) => task.set_state(TaskState::Done),
@@ -91,6 +92,7 @@ impl TaskDispatcher {
     pub fn add_task(&mut self, task: Task) {
         debug_assert!(!task.state().is_done());
         if task.state().is_done() {
+            tracing::warn!("Should not add a task which state is done");
             return;
         }
 

+ 1 - 0
frontend/rust-lib/flowy-task/src/task.rs

@@ -50,6 +50,7 @@ impl Ord for PendingTask {
     }
 }
 
+#[derive(Debug, Clone)]
 pub enum TaskContent {
     Text(String),
     Blob(Vec<u8>),