diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index ed2c1ff2a4..965f1bb0a5 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/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", diff --git a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart index 477909076e..57e5480183 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_data_controller.dart @@ -132,6 +132,7 @@ class BoardDataController { } Future dispose() async { + await _viewCache.dispose(); await _gridFFIService.closeGrid(); await fieldController.dispose(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart index 5cf9dc5531..be0d03e514 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_controller.dart +++ b/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 get filters => _filters; } +class _GridSortNotifier extends ChangeNotifier { + List _sorts = []; + + set sorts(List sorts) { + _sorts = sorts; + notifyListeners(); + } + + void notify() { + notifyListeners(); + } + + List get sorts => _sorts; +} + typedef OnReceiveUpdateFields = void Function(List); typedef OnReceiveFields = void Function(List); typedef OnReceiveFilters = void Function(List); +typedef OnReceiveSorts = void Function(List); 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 _fieldCallbacks = {}; @@ -78,9 +101,16 @@ class GridFieldController { _GridFilterNotifier? _filterNotifier = _GridFilterNotifier(); final Map _filterPBByFieldId = {}; + // Sort callbacks + final Map _sortCallbacks = {}; + _GridSortNotifier? _sortNotifier = _GridSortNotifier(); + final Map _sortPBByFieldId = {}; + // Getters List get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []]; List get filterInfos => [..._filterNotifier?.filters ?? []]; + List 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,68 +178,95 @@ class GridFieldController { void _listenOnFilterChanges() { //Listen on the fitler changes - _filterListener.start(onFilterChanged: (result) { + + deleteFilterFromChangeset( + List 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 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 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 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); - 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); - } + updateFilterFromChangeset(filters, changeset); - // Insert the filter if there is a fitler and its field info is - // not null - if (updatedFilter.hasFilter()) { - final fieldInfo = _findFieldInfoForFilter( - fieldInfos, - updatedFilter.filter, - ); - - if (fieldInfo != null) { - // Insert the filter with the position: filterIndex, otherwise, - // append it to the end of the list. - final filterInfo = - FilterInfo(gridId, updatedFilter.filter, fieldInfo); - if (filterIndex != -1) { - filters.insert(filterIndex, filterInfo); - } else { - filters.add(filterInfo); - } - _filterPBByFieldId[fieldInfo.id] = updatedFilter.filter; - } - } - } _updateFieldInfos(); _filterNotifier?.filters = filters; }, @@ -201,11 +275,99 @@ class GridFieldController { }); } + void _listenOnSortChanged() { + deleteSortFromChangeset( + List 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 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)); + } + } + } + } + + updateSortFromChangeset( + List 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); + } + + 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 newSortInfos = sortInfos; + deleteSortFromChangeset(newSortInfos, changeset); + insertSortFromChangeset(newSortInfos, changeset); + updateSortFromChangeset(newSortInfos, changeset); + + _updateFieldInfos(); + _sortNotifier?.sorts = newSortInfos; + }, + (err) => Log.error(err), + ); + }); + } + void _listenOnSettingChanges() { //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 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> 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 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> _loadSorts() async { + return _sortFFIService.getAllSorts().then((result) { + return result.fold( + (sortPBs) { + final List 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 deletedFields) { @@ -466,11 +693,13 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { } } -FieldInfo? _findFieldInfoForFilter( - List fieldInfos, FilterPB filter) { +FieldInfo? _findFieldInfo({ + required List 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; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_service.dart b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_service.dart index 92a0e6aea9..221ae01c41 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_service.dart +++ b/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 diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_accessory_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_accessory_bloc.dart new file mode 100644 index 0000000000..46b905c25b --- /dev/null +++ b/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 { + final String viewId; + + GridAccessoryMenuBloc({required this.viewId}) + : super(GridAccessoryMenuState.initial( + viewId, + )) { + on( + (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, + ); +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart index f2a24fa2ed..c841faccc3 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_cache.dart @@ -81,6 +81,23 @@ class GridRowCache { _showRows(changeset.visibleRows); } + void reorderAllRows(List 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 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 { diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart index 4c9ff34253..b93f76d19e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart +++ b/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 _rowInfos = []; List get rows => List.from(_rowInfos); @@ -150,6 +149,28 @@ class RowList { return deletedRows; } + void reorderWithRowIds(List 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; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/sort/sort_create_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/sort/sort_create_bloc.dart new file mode 100644 index 0000000000..270a3c5182 --- /dev/null +++ b/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 { + final String viewId; + final SortFFIService _ffiService; + final GridFieldController fieldController; + void Function(List)? _onFieldFn; + CreateSortBloc({required this.viewId, required this.fieldController}) + : _ffiService = SortFFIService(viewId: viewId), + super(CreateSortState.initial(fieldController.fieldInfos)) { + on( + (event, emit) async { + event.when( + initial: () async { + _startListening(); + }, + didReceiveFields: (List 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 _filterFields( + List fields, + String filterText, + ) { + final List 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> _createDefaultSort(FieldInfo field) async { + final result = await _ffiService.insertSort( + fieldId: field.id, + fieldType: field.fieldType, + condition: GridSortConditionPB.Ascending); + + return result; + } + + @override + Future 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 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 creatableFields, + required List allFields, + required bool didCreateSort, + }) = _CreateSortState; + + factory CreateSortState.initial(List fields) { + return CreateSortState( + filterText: "", + creatableFields: getCreatableSorts(fields), + allFields: fields, + didCreateSort: false, + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/sort/sort_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/sort/sort_editor_bloc.dart new file mode 100644 index 0000000000..41a47fa9ba --- /dev/null +++ b/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 { + final String viewId; + final SortFFIService _ffiService; + final GridFieldController fieldController; + void Function(List)? _onFieldFn; + SortEditorBloc({ + required this.viewId, + required this.fieldController, + required List sortInfos, + }) : _ffiService = SortFFIService(viewId: viewId), + super(SortEditorState.initial(sortInfos, fieldController.fieldInfos)) { + on( + (event, emit) async { + event.when( + initial: () async { + _startListening(); + }, + didReceiveFields: (List fields) { + final List allFields = List.from(fields); + final List 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 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 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 fieldInfos) = + _DidReceiveFields; + const factory SortEditorEvent.didReceiveSorts(List 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 sortInfos, + required List creatableFields, + required List allFields, + }) = _SortEditorState; + + factory SortEditorState.initial( + List sortInfos, + List fields, + ) { + return SortEditorState( + creatableFields: getCreatableSorts(fields), + allFields: fields, + sortInfos: sortInfos, + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/sort/sort_listener.dart b/frontend/app_flowy/lib/plugins/grid/application/sort/sort_listener.dart new file mode 100644 index 0000000000..a45cb6b6e2 --- /dev/null +++ b/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; + +class SortsListener { + final String viewId; + PublishNotifier? _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 result, + ) { + switch (ty) { + case GridDartNotification.DidUpdateSort: + result.fold( + (payload) => _notifier?.value = + left(SortChangesetNotificationPB.fromBuffer(payload)), + (error) => _notifier?.value = right(error), + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _notifier?.dispose(); + _notifier = null; + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/sort/sort_menu_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/sort/sort_menu_bloc.dart new file mode 100644 index 0000000000..ea0215ac30 --- /dev/null +++ b/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 { + final String viewId; + final GridFieldController fieldController; + void Function(List)? _onSortChangeFn; + void Function(List)? _onFieldFn; + + SortMenuBloc({required this.viewId, required this.fieldController}) + : super(SortMenuState.initial( + viewId, + fieldController.sortInfos, + fieldController.fieldInfos, + )) { + on( + (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 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 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 sortInfos) = + _DidReceiveSortInfos; + const factory SortMenuEvent.didReceiveFields(List fields) = + _DidReceiveFields; + const factory SortMenuEvent.toggleMenu() = _SetMenuVisibility; +} + +@freezed +class SortMenuState with _$SortMenuState { + const factory SortMenuState({ + required String viewId, + required List sortInfos, + required List fields, + required List creatableFields, + required bool isVisible, + }) = _SortMenuState; + + factory SortMenuState.initial( + String viewId, + List sortInfos, + List fields, + ) => + SortMenuState( + viewId: viewId, + sortInfos: sortInfos, + fields: fields, + creatableFields: getCreatableSorts(fields), + isVisible: false, + ); +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/sort/sort_service.dart b/frontend/app_flowy/lib/plugins/grid/application/sort/sort_service.dart new file mode 100644 index 0000000000..6f0ed21514 --- /dev/null +++ b/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, FlowyError>> getAllSorts() { + final payload = GridIdPB()..value = viewId; + + return GridEventGetAllSorts(payload).send().then((result) { + return result.fold( + (repeated) => left(repeated.items), + (r) => right(r), + ); + }); + } + + Future> 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> 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> 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> 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); + }, + ); + }); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/sort/util.dart b/frontend/app_flowy/lib/plugins/grid/application/sort/util.dart new file mode 100644 index 0000000000..3dc42909cb --- /dev/null +++ b/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 getCreatableSorts(List fieldInfos) { + final List creatableFields = List.from(fieldInfos); + creatableFields.retainWhere((element) => element.canCreateSort); + return creatableFields; +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart b/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart index ef3082d3cf..eb83500d7d 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_cache.dart +++ b/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), + ); + }, ); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_listener.dart b/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_listener.dart index eae4b02e7d..9d498da631 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/view/grid_view_listener.dart +++ b/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; typedef GridViewRowsNotifierValue = Either; +typedef GridViewReorderAllRowsNotifierValue = Either, FlowyError>; +typedef GridViewSingleRowNotifierValue = Either; class GridViewListener { final String viewId; PublishNotifier? _rowsNotifier = PublishNotifier(); - PublishNotifier? _rowsVisibilityNotifier = + PublishNotifier? _reorderAllRows = + PublishNotifier(); + PublishNotifier? _reorderSingleRow = + PublishNotifier(); + PublishNotifier? _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 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 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; } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart index 568f02adb0..177e2a2f2a 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/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 { fieldController: widget.gridController.fieldController, )..add(const GridFilterMenuEvent.initial()), ), + BlocProvider( + create: (context) => SortMenuBloc( + viewId: widget.view.id, + fieldController: widget.gridController.fieldController, + )..add(const SortMenuEvent.initial()), + ), BlocProvider( create: (context) => GridSettingBloc(gridId: widget.view.id), ), @@ -139,7 +146,7 @@ class _FlowyGridState extends State { 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, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/layout/sizes.dart b/frontend/app_flowy/lib/plugins/grid/presentation/layout/sizes.dart index 0e69a755ef..9343c0af1f 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/layout/sizes.dart +++ b/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( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/accessory_menu.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/accessory_menu.dart new file mode 100644 index 0000000000..4967354f82 --- /dev/null +++ b/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( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => context + .read() + .add(const GridAccessoryMenuEvent.toggleMenu())), + BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => context + .read() + .add(const GridAccessoryMenuEvent.toggleMenu()), + ), + ], + child: BlocBuilder( + 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( + 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, + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_cell_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_cell_editor.dart index f68ac9e687..3c0810f3b5 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_cell_editor.dart +++ b/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( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart index c0cbe1a261..d294d11518 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/date_cell/date_editor.dart +++ b/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, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart index 50461aa3f2..68ab937881 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/select_option_editor.dart +++ b/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) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart index ddd9fa688c..579c3debf8 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart +++ b/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) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart index 20fd1b4cca..4c6856c7ab 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart @@ -57,7 +57,7 @@ class _GridCreateFilterListState extends State { 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), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_menu.dart similarity index 63% rename from frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart rename to frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_menu.dart index ca72bd52e8..c7561380a2 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart +++ b/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( builder: (context, state) { - if (state.isVisible) { - return _wrapPadding(Column( - children: [ - buildDivider(context), - const VSpace(6), - buildFilterItems(state.viewId, state), - ], - )); - } else { - return const SizedBox(); + final List children = []; + children.addAll( + state.filters + .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo)) + .toList(), + ); + + if (state.creatableFields.isNotEmpty) { + children.add(AddFilterButton(viewId: state.viewId)); } - }, - ); - } - 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 children = []; - children.addAll( - state.filters - .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo)) - .toList(), - ); - - if (state.creatableFields.isNotEmpty) { - children.add(AddFilterButton(viewId: 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, + ), + ), + ], ), - ), - ], + ); + }, ); } } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu_item.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_menu_item.dart similarity index 100% rename from frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu_item.dart rename to frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_menu_item.dart diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart index 5fa729edd1..57f9f54e4d 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell_action_sheet.dart @@ -77,7 +77,7 @@ class _EditFieldButton extends StatelessWidget { return BlocBuilder( 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, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart index 03dc44760a..dda344b8eb 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_editor.dart +++ b/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), ); }, ); diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart index cda88a3f45..c5719327f1 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_list.dart +++ b/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), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart index c9eb261e3e..afb1e53542 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_type_option_editor.dart +++ b/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, ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart index 60a7be4876..ae4db4b91b 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/date.dart +++ b/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, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart index dc3788192c..ab57580e6b 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/number.dart +++ b/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( 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), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart index a7e18fdddd..297a91e359 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option.dart +++ b/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: () { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart index e110fe7b3e..97edfbf3c8 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart +++ b/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, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart index c04145a88c..3b309c7275 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_action_sheet.dart +++ b/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(), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/create_sort_list.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/create_sort_list.dart new file mode 100644 index 0000000000..6ed598d2ba --- /dev/null +++ b/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 createState() => _GridCreateSortListState(); +} + +class _GridCreateSortListState extends State { + 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( + listener: (context, state) { + if (state.didCreateSort) { + widget.onClosed(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cells = state.creatableFields.map((fieldInfo) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: _SortPropertyCell( + fieldInfo: fieldInfo, + onTap: (fieldInfo) => createSort(fieldInfo), + ), + ); + }).toList(); + + List 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 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() + .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, + ), + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/order_panel.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/order_panel.dart new file mode 100644 index 0000000000..5adb6313b3 --- /dev/null +++ b/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 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 ""; + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_choice_button.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_choice_button.dart new file mode 100644 index 0000000000..29e705f9d0 --- /dev/null +++ b/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, + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_editor.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_editor.dart new file mode 100644 index 0000000000..6b23dac49b --- /dev/null +++ b/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 sortInfos; + final GridFieldController fieldController; + const SortEditor({ + required this.viewId, + required this.fieldController, + required this.sortInfos, + Key? key, + }) : super(key: key); + + @override + State createState() => _SortEditorState(); +} + +class _SortEditorState extends State { + 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( + 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( + builder: (context, state) { + final List 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() + .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( + builder: (context, state) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(LocaleKeys.grid_sort_deleteSort.tr()), + onTap: () { + context + .read() + .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() + .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 ""; + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_info.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_info.dart new file mode 100644 index 0000000000..b6aa02987f --- /dev/null +++ b/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; +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_menu.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/sort/sort_menu.dart new file mode 100644 index 0000000000..2f4a04c4b9 --- /dev/null +++ b/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( + 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().fieldController, + sortInfos: state.sortInfos, + ); + }, + child: SortChoiceChip(sortInfos: state.sortInfos), + ); + } else { + return const SizedBox(); + } + }, + ); + } +} + +class SortChoiceChip extends StatelessWidget { + final List 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, + ), + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart index d72e31c217..0292f09645 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart @@ -54,7 +54,7 @@ class _FilterButtonState extends State { 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, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart index a8d9d09abb..7cffb6ac62 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_group.dart +++ b/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( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart index a6cb9914ec..b4b36b9f76 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_property.dart +++ b/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), ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart index 08f7dd552e..1c8529cf94 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_setting.dart +++ b/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(), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart index 7d65c86b8a..f73fd9d1aa 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/grid_toolbar.dart +++ b/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(), ], ), diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart index 64e208437d..75224922dc 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart @@ -45,7 +45,7 @@ class _SettingButtonState extends State { 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, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/sort_button.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/sort_button.dart new file mode 100644 index 0000000000..efe9d47cc3 --- /dev/null +++ b/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 createState() => _SortButtonState(); +} + +class _SortButtonState extends State { + final _popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + 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(); + 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(); + return GridCreateSortList( + viewId: bloc.viewId, + fieldController: bloc.fieldController, + onClosed: () => _popoverController.close(), + onCreateSort: () { + if (!bloc.state.isVisible) { + bloc.add(const SortMenuEvent.toggleMenu()); + } + }, + ); + }, + ); + } +} diff --git a/frontend/app_flowy/packages/flowy_infra/pubspec.lock b/frontend/app_flowy/packages/flowy_infra/pubspec.lock index 23fd739d68..e97f235a59 100644 --- a/frontend/app_flowy/packages/flowy_infra/pubspec.lock +++ b/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" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock index 398e3d0c89..76a1015b78 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/example/pubspec.lock +++ b/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" diff --git a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock index f736121e48..825e6fbbfb 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.lock +++ b/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: diff --git a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock index e05d8bd65c..cfed8a47b4 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.lock +++ b/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: diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 9a978f8b2a..d9afaf03f2 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/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, diff --git a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart index 4b9b5002a8..73e8800979 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/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!); } diff --git a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock b/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock index 04805bd707..c876268afb 100644 --- a/frontend/app_flowy/packages/flowy_infra_ui/pubspec.lock +++ b/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" diff --git a/frontend/app_flowy/packages/flowy_sdk/pubspec.lock b/frontend/app_flowy/packages/flowy_sdk/pubspec.lock new file mode 100644 index 0000000000..9066a7950c --- /dev/null +++ b/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" diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index b66675a6ab..f3e07b2948 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/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 { diff --git a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs index ed0222b11e..f2a9f92218 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/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)] diff --git a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs index b65aa79ddd..9a9dc33251 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/sort_entities.rs +++ b/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, +} + +impl std::convert::From>> for RepeatedSortPB { + fn from(revs: Vec>) -> Self { + RepeatedSortPB { + items: revs.into_iter().map(|rev| rev.as_ref().into()).collect(), + } + } +} + +impl std::convert::From> for RepeatedSortPB { + fn from(items: Vec) -> 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, @@ -83,9 +104,10 @@ impl TryInto 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, + pub insert_sorts: Vec, #[pb(index = 3)] - pub delete_sorts: Vec, + pub delete_sorts: Vec, #[pb(index = 4)] - pub update_sorts: Vec, + pub update_sorts: Vec, } 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, } diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 49b72b7be6..f62d25bf3c 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/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, + manager: AFPluginState>, +) -> DataResult { + 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, + manager: AFPluginState>, +) -> 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, diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 654580766b..e760fdd26d 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -13,6 +13,8 @@ pub fn init(grid_manager: Arc) -> 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] diff --git a/frontend/rust-lib/flowy-grid/src/manager.rs b/frontend/rust-lib/flowy-grid/src/manager.rs index d3608a6f08..c73d8b28e8 100644 --- a/frontend/rust-lib/flowy-grid/src/manager.rs +++ b/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>(&self, grid_id: T) -> FlowyResult> { 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>(&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> { - 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) } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/type_option_cell.rs index 35f25a1219..f68a217d47 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/type_option_cell.rs +++ b/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( diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/controller.rs b/frontend/rust-lib/flowy-grid/src/services/filter/controller.rs index cdcc07c49e..5639aaef0c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/controller.rs @@ -115,7 +115,7 @@ impl FilterController { .collect::>>() } - #[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 { diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs index f0d805b3af..e4a23a607e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/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> { + 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: _, diff --git a/frontend/rust-lib/flowy-grid/src/services/sort/controller.rs b/frontend/rust-lib/flowy-grid/src/services/sort/controller.rs index 6eb82caca0..135335a444 100644 --- a/frontend/rust-lib/flowy-grid/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/sort/controller.rs @@ -39,6 +39,7 @@ impl SortController { pub fn new( view_id: &str, handler_id: &str, + sorts: Vec>, delegate: T, task_scheduler: Arc>, 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(); diff --git a/frontend/rust-lib/flowy-grid/src/services/sort/entities.rs b/frontend/rust-lib/flowy-grid/src/services/sort/entities.rs index bdbc8eb17c..e5a894f43e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/sort/entities.rs +++ b/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, } diff --git a/frontend/rust-lib/flowy-grid/src/services/view_editor/changed_notifier.rs b/frontend/rust-lib/flowy-grid/src/services/view_editor/changed_notifier.rs index 16e05ac1dc..6e22489d44 100644 --- a/frontend/rust-lib/flowy-grid/src/services/view_editor/changed_notifier.rs +++ b/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, }; diff --git a/frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs b/frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs index f1f7158571..97a57f5b5a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/view_editor/editor.rs +++ b/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> { - 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> { - 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> { + 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 { let sort_type = SortType::from(¶ms); 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> { + 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> { + 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(¶ms); @@ -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(¬ification.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(¬ification.view_id, GridDartNotification::DidUpdateSort) + .payload(notification) .send(); } } @@ -764,6 +786,8 @@ async fn make_sort_controller( cell_data_cache: AtomicCellDataCache, ) -> Arc> { 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, diff --git a/frontend/rust-lib/flowy-grid/src/services/view_editor/editor_manager.rs b/frontend/rust-lib/flowy-grid/src/services/view_editor/editor_manager.rs index 0febd05b28..25effc8a7e 100644 --- a/frontend/rust-lib/flowy-grid/src/services/view_editor/editor_manager.rs +++ b/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>> { + 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 { let view_editor = self.get_view_editor(¶ms.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(¶ms.view_id).await?; view_editor.delete_view_sort(params).await diff --git a/frontend/rust-lib/flowy-grid/src/services/view_editor/trait_impl.rs b/frontend/rust-lib/flowy-grid/src/services/view_editor/trait_impl.rs index 87c96b79fe..14bf0f0558 100644 --- a/frontend/rust-lib/flowy-grid/src/services/view_editor/trait_impl.rs +++ b/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]) -> 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(), } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs b/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs index e0bf9dc0a7..13c38bc42b 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/block_test/util.rs +++ b/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; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/sort_test/checkbox_and_text_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/sort_test/checkbox_and_text_test.rs new file mode 100644 index 0000000000..315de599c3 --- /dev/null +++ b/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; +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/sort_test/mod.rs b/frontend/rust-lib/flowy-grid/tests/grid/sort_test/mod.rs index d84b43ca7b..69e0a622f8 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/sort_test/mod.rs +++ b/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; diff --git a/frontend/rust-lib/flowy-revision/src/rev_manager.rs b/frontend/rust-lib/flowy-revision/src/rev_manager.rs index d06b8187fa..3be04ae001 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_manager.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_manager.rs @@ -125,7 +125,7 @@ impl RevisionManager { } } - #[tracing::instrument(level = "debug", skip_all, fields(deserializer, object) err)] + #[tracing::instrument(level = "trace", skip_all, fields(deserializer, object) err)] pub async fn initialize(&mut self, _cloud: Option>) -> FlowyResult where B: RevisionObjectDeserializer, diff --git a/frontend/rust-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/frontend/rust-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 93e3c739dd..f672da6f93 100644 --- a/frontend/rust-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/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]) -> Vec> { - self.sorts.get_objects_by_field_revs(field_revs) + pub fn get_all_sorts(&self, _field_revs: &[Arc]) -> Vec> { + 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> { 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> { + self.modify(|view| { + view.sorts.clear(); + Ok(Some(())) + }) + } + pub fn get_all_filters(&self, field_revs: &[Arc]) -> Vec> { self.filters.get_objects_by_field_revs(field_revs) } diff --git a/frontend/rust-lib/flowy-task/src/scheduler.rs b/frontend/rust-lib/flowy-task/src/scheduler.rs index 5e9d599241..2cd70f7a08 100644 --- a/frontend/rust-lib/flowy-task/src/scheduler.rs +++ b/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; } diff --git a/frontend/rust-lib/flowy-task/src/task.rs b/frontend/rust-lib/flowy-task/src/task.rs index 02f2322898..43d32813ab 100644 --- a/frontend/rust-lib/flowy-task/src/task.rs +++ b/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),