diff --git a/frontend/app_flowy/assets/translations/en.json b/frontend/app_flowy/assets/translations/en.json index 70ebe3245f..78055d98df 100644 --- a/frontend/app_flowy/assets/translations/en.json +++ b/frontend/app_flowy/assets/translations/en.json @@ -161,7 +161,21 @@ "filter": "Filter", "sortBy": "Sort by", "Properties": "Properties", - "group": "Group" + "group": "Group", + "addFilter": "Add Filter", + "deleteFilter": "Delete filter", + "filterBy": "Filter by...", + "typeAValue": "Type a value..." + }, + "textFilter": { + "contains": "Contains", + "doesNotContain": "Does not contain", + "endsWith": "Ends with", + "startWith": "Starts with", + "is": "Is", + "isNot": "Is not", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty" }, "field": { "hide": "Hide", diff --git a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart index 027fa94316..de177e47a4 100644 --- a/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/board_bloc.dart @@ -136,9 +136,9 @@ class BoardBloc extends Bloc { } void _groupItemStartEditing(GroupPB group, RowPB row, bool isEdit) { - final fieldContext = fieldController.getField(group.fieldId); - if (fieldContext == null) { - Log.warn("FieldContext should not be null"); + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); return; } @@ -147,7 +147,7 @@ class BoardBloc extends Bloc { // group.groupId, // GroupItem( // row: row, - // fieldContext: fieldContext, + // fieldInfo: fieldInfo, // isDraggable: !isEdit, // ), // ); @@ -204,7 +204,7 @@ class BoardBloc extends Bloc { items: _buildGroupItems(group), customData: GroupData( group: group, - fieldContext: fieldController.getField(group.fieldId)!, + fieldInfo: fieldController.getField(group.fieldId)!, ), ); }).toList(); @@ -275,10 +275,10 @@ class BoardBloc extends Bloc { List _buildGroupItems(GroupPB group) { final items = group.rows.map((row) { - final fieldContext = fieldController.getField(group.fieldId); + final fieldInfo = fieldController.getField(group.fieldId); return GroupItem( row: row, - fieldContext: fieldContext!, + fieldInfo: fieldInfo!, ); }).toList(); @@ -374,11 +374,11 @@ class GridFieldEquatable extends Equatable { class GroupItem extends AppFlowyGroupItem { final RowPB row; - final GridFieldContext fieldContext; + final FieldInfo fieldInfo; GroupItem({ required this.row, - required this.fieldContext, + required this.fieldInfo, bool draggable = true, }) { super.draggable = draggable; @@ -401,22 +401,22 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void insertRow(GroupPB group, RowPB row, int? index) { - final fieldContext = fieldController.getField(group.fieldId); - if (fieldContext == null) { - Log.warn("FieldContext should not be null"); + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); return; } if (index != null) { final item = GroupItem( row: row, - fieldContext: fieldContext, + fieldInfo: fieldInfo, ); controller.insertGroupItem(group.groupId, index, item); } else { final item = GroupItem( row: row, - fieldContext: fieldContext, + fieldInfo: fieldInfo, ); controller.addGroupItem(group.groupId, item); } @@ -429,30 +429,30 @@ class GroupControllerDelegateImpl extends GroupControllerDelegate { @override void updateRow(GroupPB group, RowPB row) { - final fieldContext = fieldController.getField(group.fieldId); - if (fieldContext == null) { - Log.warn("FieldContext should not be null"); + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); return; } controller.updateGroupItem( group.groupId, GroupItem( row: row, - fieldContext: fieldContext, + fieldInfo: fieldInfo, ), ); } @override void addNewRow(GroupPB group, RowPB row, int? index) { - final fieldContext = fieldController.getField(group.fieldId); - if (fieldContext == null) { - Log.warn("FieldContext should not be null"); + final fieldInfo = fieldController.getField(group.fieldId); + if (fieldInfo == null) { + Log.warn("fieldInfo should not be null"); return; } final item = GroupItem( row: row, - fieldContext: fieldContext, + fieldInfo: fieldInfo, draggable: false, ); @@ -479,10 +479,10 @@ class BoardEditingRow { class GroupData { final GroupPB group; - final GridFieldContext fieldContext; + final FieldInfo fieldInfo; GroupData({ required this.group, - required this.fieldContext, + required this.fieldInfo, }); CheckboxGroup? asCheckboxGroup() { @@ -490,7 +490,7 @@ class GroupData { return CheckboxGroup(group); } - FieldType get fieldType => fieldContext.fieldType; + FieldType get fieldType => fieldInfo.fieldType; } class CheckboxGroup { 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 7ca035d375..c3669948b9 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 @@ -12,7 +12,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart'; import 'board_listener.dart'; -typedef OnFieldsChanged = void Function(UnmodifiableListView); +typedef OnFieldsChanged = void Function(UnmodifiableListView); typedef OnGridChanged = void Function(GridPB); typedef DidLoadGroups = void Function(List); typedef OnUpdatedGroup = void Function(List); diff --git a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart index 0512fb8bb9..5ed8e76c49 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/board_date_cell_bloc.dart @@ -58,14 +58,14 @@ class BoardDateCellState with _$BoardDateCellState { const factory BoardDateCellState({ required DateCellDataPB? data, required String dateStr, - required GridFieldContext fieldContext, + required FieldInfo fieldInfo, }) = _BoardDateCellState; factory BoardDateCellState.initial(GridDateCellController context) { final cellData = context.getCellData(); return BoardDateCellState( - fieldContext: context.fieldContext, + fieldInfo: context.fieldInfo, data: cellData, dateStr: _dateStrFromCellData(cellData), ); diff --git a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart index 92461772e2..0f383432bd 100644 --- a/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart +++ b/frontend/app_flowy/lib/plugins/board/application/card/card_bloc.dart @@ -63,7 +63,7 @@ class BoardCardBloc extends Bloc { return RowInfo( gridId: _rowService.gridId, fields: UnmodifiableListView( - state.cells.map((cell) => cell.identifier.fieldContext).toList(), + state.cells.map((cell) => cell.identifier.fieldInfo).toList(), ), rowPB: state.rowPB, visible: true, @@ -133,10 +133,10 @@ class BoardCellEquatable extends Equatable { @override List get props { return [ - identifier.fieldContext.id, - identifier.fieldContext.fieldType, - identifier.fieldContext.visibility, - identifier.fieldContext.width, + identifier.fieldInfo.id, + identifier.fieldInfo.fieldType, + identifier.fieldInfo.visibility, + identifier.fieldInfo.width, ]; } } diff --git a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart index ac82d7de29..816feb76de 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/board_page.dart @@ -236,7 +236,7 @@ class _BoardContentState extends State { child: BoardCard( gridId: gridId, groupId: groupData.group.groupId, - fieldId: groupItem.fieldContext.id, + fieldId: groupItem.fieldInfo.id, isEditing: isEditing, cellBuilder: cellBuilder, dataController: cardController, @@ -285,7 +285,7 @@ class _BoardContentState extends State { ) { final rowInfo = RowInfo( gridId: gridId, - fields: UnmodifiableListView(fieldController.fieldContexts), + fields: UnmodifiableListView(fieldController.fieldInfos), rowPB: rowPB, visible: true, ); diff --git a/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart b/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart index fd00193f19..7f4b733b89 100644 --- a/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart +++ b/frontend/app_flowy/lib/plugins/board/presentation/toolbar/board_toolbar.dart @@ -1,10 +1,12 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:flowy_infra/image.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/color_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flutter/material.dart'; +import '../../../../generated/locale_keys.g.dart'; import 'board_setting.dart'; class BoardToolbarContext { @@ -30,6 +32,7 @@ class BoardToolbar extends StatelessWidget { height: 40, child: Row( children: [ + const Spacer(), _SettingButton( settingContext: BoardSettingContext.from(toolbarContext), ), @@ -61,16 +64,18 @@ class _SettingButtonState extends State<_SettingButton> { Widget build(BuildContext context) { return AppFlowyPopover( controller: popoverController, + direction: PopoverDirection.leftWithTopAligned, + triggerActions: PopoverTriggerFlags.none, constraints: BoxConstraints.loose(const Size(260, 400)), - child: FlowyIconButton( - width: 22, - icon: Padding( - padding: const EdgeInsets.symmetric(vertical: 3.0, horizontal: 3.0), - child: svgWidget( - "grid/setting/setting", - color: Theme.of(context).colorScheme.onSurface, - ), - ), + child: FlowyTextButton( + LocaleKeys.settings_title.tr(), + fontSize: 14, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), + onPressed: () { + popoverController.show(); + }, ), popupBuilder: (BuildContext popoverContext) { return BoardSettingListPopover( diff --git a/frontend/app_flowy/lib/plugins/document/document.dart b/frontend/app_flowy/lib/plugins/document/document.dart index 2808b032d8..5d695ff83e 100644 --- a/frontend/app_flowy/lib/plugins/document/document.dart +++ b/frontend/app_flowy/lib/plugins/document/document.dart @@ -213,7 +213,7 @@ class ShareActionWrapper extends ActionCell { ShareActionWrapper(this.inner); @override - Widget? icon(Color iconColor) => null; + Widget? leftIcon(Color iconColor) => null; @override String get name => inner.name; diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart index cdf695dc7f..df97a9567a 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_controller.dart @@ -150,22 +150,22 @@ class IGridCellController extends Equatable { _fieldNotifier = fieldNotifier, _fieldService = FieldService( gridId: cellId.gridId, - fieldId: cellId.fieldContext.id, + fieldId: cellId.fieldInfo.id, ), _cacheKey = GridCellCacheKey( rowId: cellId.rowId, - fieldId: cellId.fieldContext.id, + fieldId: cellId.fieldInfo.id, ); String get gridId => cellId.gridId; String get rowId => cellId.rowId; - String get fieldId => cellId.fieldContext.id; + String get fieldId => cellId.fieldInfo.id; - GridFieldContext get fieldContext => cellId.fieldContext; + FieldInfo get fieldInfo => cellId.fieldInfo; - FieldType get fieldType => cellId.fieldContext.fieldType; + FieldType get fieldType => cellId.fieldInfo.fieldType; VoidCallback? startListening({ required void Function(T?) onCellChanged, @@ -179,7 +179,7 @@ class IGridCellController extends Equatable { _cellDataNotifier = ValueNotifier(_cellsCache.get(_cacheKey)); _cellListener = - CellListener(rowId: cellId.rowId, fieldId: cellId.fieldContext.id); + CellListener(rowId: cellId.rowId, fieldId: cellId.fieldInfo.id); /// 1.Listen on user edit event and load the new cell data if needed. /// For example: @@ -310,30 +310,33 @@ class IGridCellController extends Equatable { @override List get props => - [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldContext.id]; + [_cellsCache.get(_cacheKey) ?? "", cellId.rowId + cellId.fieldInfo.id]; } class GridCellFieldNotifierImpl extends IGridCellFieldNotifier { - final GridFieldController _cache; - OnChangeset? _onChangesetFn; + final GridFieldController _fieldController; + OnReceiveUpdateFields? _onChangesetFn; - GridCellFieldNotifierImpl(GridFieldController cache) : _cache = cache; + GridCellFieldNotifierImpl(GridFieldController cache) + : _fieldController = cache; @override void onCellDispose() { if (_onChangesetFn != null) { - _cache.removeListener(onChangesetListener: _onChangesetFn!); + _fieldController.removeListener(onChangesetListener: _onChangesetFn!); _onChangesetFn = null; } } @override - void onCellFieldChanged(void Function(FieldPB p1) callback) { - _onChangesetFn = (GridFieldChangesetPB changeset) { - for (final updatedField in changeset.updatedFields) { - callback(updatedField); + void onCellFieldChanged(void Function(FieldInfo) callback) { + _onChangesetFn = (List filedInfos) { + for (final field in filedInfos) { + callback(field); } }; - _cache.addListener(onChangeset: _onChangesetFn); + _fieldController.addListener( + onFieldsUpdated: _onChangesetFn, + ); } } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart index d4cca373bc..5e2a2b3838 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_field_notifier.dart @@ -1,10 +1,10 @@ -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; import 'package:flutter/foundation.dart'; import 'cell_service.dart'; abstract class IGridCellFieldNotifier { - void onCellFieldChanged(void Function(FieldPB) callback); + void onCellFieldChanged(void Function(FieldInfo) callback); void onCellDispose(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart index 1e6cbaab11..b28b92a7b7 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/cell_service/cell_service.dart @@ -60,17 +60,17 @@ class GridCellIdentifier with _$GridCellIdentifier { const factory GridCellIdentifier({ required String gridId, required String rowId, - required GridFieldContext fieldContext, + required FieldInfo fieldInfo, }) = _GridCellIdentifier; // ignore: unused_element const GridCellIdentifier._(); - String get fieldId => fieldContext.id; + String get fieldId => fieldInfo.id; - FieldType get fieldType => fieldContext.fieldType; + FieldType get fieldType => fieldInfo.fieldType; ValueKey key() { - return ValueKey("$rowId$fieldId${fieldContext.fieldType}"); + return ValueKey("$rowId$fieldId${fieldInfo.fieldType}"); } } diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart index 61de91e21b..6dc5df61c1 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cal_bloc.dart @@ -176,7 +176,7 @@ class DateCalBloc extends Bloc { final result = await FieldService.updateFieldTypeOption( gridId: cellController.gridId, - fieldId: cellController.fieldContext.id, + fieldId: cellController.fieldInfo.id, typeOptionData: newDateTypeOption.writeToBuffer(), ); diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart index 38f51e710e..6256dc1f40 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/date_cell_bloc.dart @@ -58,14 +58,14 @@ class DateCellState with _$DateCellState { const factory DateCellState({ required DateCellDataPB? data, required String dateStr, - required GridFieldContext fieldContext, + required FieldInfo fieldInfo, }) = _DateCellState; factory DateCellState.initial(GridDateCellController context) { final cellData = context.getCellData(); return DateCellState( - fieldContext: context.fieldContext, + fieldInfo: context.fieldInfo, data: cellData, dateStr: _dateStrFromCellData(cellData), ); diff --git a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart index 3bf8950b6c..a179ccab30 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/cell/select_option_service.dart @@ -11,7 +11,7 @@ class SelectOptionService { SelectOptionService({required this.cellId}); String get gridId => cellId.gridId; - String get fieldId => cellId.fieldContext.id; + String get fieldId => cellId.fieldInfo.id; String get rowId => cellId.rowId; Future> create({required String name}) { 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 b5cb8cd9bf..0c084adf26 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 @@ -1,22 +1,26 @@ import 'dart:collection'; import 'package:app_flowy/plugins/grid/application/field/grid_listener.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart'; import 'package:app_flowy/plugins/grid/application/grid_service.dart'; import 'package:app_flowy/plugins/grid/application/setting/setting_listener.dart'; import 'package:app_flowy/plugins/grid/application/setting/setting_service.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/setting_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart'; import 'package:flutter/foundation.dart'; import '../row/row_cache.dart'; class _GridFieldNotifier extends ChangeNotifier { - List _fieldContexts = []; + List _fieldInfos = []; - set fieldContexts(List fieldContexts) { - _fieldContexts = fieldContexts; + set fieldInfos(List fieldInfos) { + _fieldInfos = fieldInfos; notifyListeners(); } @@ -24,91 +28,222 @@ class _GridFieldNotifier extends ChangeNotifier { notifyListeners(); } - List get fieldContexts => _fieldContexts; + List get fieldInfos => _fieldInfos; } -typedef OnChangeset = void Function(GridFieldChangesetPB); -typedef OnReceiveFields = void Function(List); +class _GridFilterNotifier extends ChangeNotifier { + List _filters = []; + + set filters(List filters) { + _filters = filters; + notifyListeners(); + } + + void notify() { + notifyListeners(); + } + + List get filters => _filters; +} + +typedef OnReceiveUpdateFields = void Function(List); +typedef OnReceiveFields = void Function(List); +typedef OnReceiveFilters = void Function(List); class GridFieldController { final String gridId; + // Listeners final GridFieldsListener _fieldListener; final SettingListener _settingListener; - final Map _fieldCallbackMap = {}; - final Map _changesetCallbackMap = {}; + final FiltersListener _filterListener; + + // FFI services final GridFFIService _gridFFIService; final SettingFFIService _settingFFIService; + final FilterFFIService _filterFFIService; + // Field callbacks + final Map _fieldCallbacks = {}; _GridFieldNotifier? _fieldNotifier = _GridFieldNotifier(); - final Map _configurationByFieldId = {}; - List get fieldContexts => - [..._fieldNotifier?.fieldContexts ?? []]; + // Field updated callbacks + final Map)> + _updatedFieldCallbacks = {}; + + // Group callbacks + final Map _groupConfigurationByFieldId = {}; + + // Filter callbacks + final Map _filterCallbacks = {}; + _GridFilterNotifier? _filterNotifier = _GridFilterNotifier(); + final Map _filterPBByFieldId = {}; + + // Getters + List get fieldInfos => [..._fieldNotifier?.fieldInfos ?? []]; + List get filterInfos => [..._filterNotifier?.filters ?? []]; + FieldInfo? getField(String fieldId) { + final fields = _fieldNotifier?.fieldInfos + .where((element) => element.id == fieldId) + .toList() ?? + []; + if (fields.isEmpty) { + return null; + } + assert(fields.length == 1); + return fields.first; + } + + FilterInfo? getFilter(String filterId) { + final filters = _filterNotifier?.filters + .where((element) => element.filter.id == filterId) + .toList() ?? + []; + if (filters.isEmpty) { + return null; + } + assert(filters.length == 1); + return filters.first; + } GridFieldController({required this.gridId}) : _fieldListener = GridFieldsListener(gridId: gridId), + _settingListener = SettingListener(gridId: gridId), + _filterListener = FiltersListener(viewId: gridId), _gridFFIService = GridFFIService(gridId: gridId), - _settingFFIService = SettingFFIService(viewId: gridId), - _settingListener = SettingListener(gridId: gridId) { + _filterFFIService = FilterFFIService(viewId: gridId), + _settingFFIService = SettingFFIService(viewId: gridId) { + //Listen on field's changes + _listenOnFieldChanges(); + + //Listen on setting changes + _listenOnSettingChanges(); + + //Listen on the fitler changes + _listenOnFilterChanges(); + + _settingFFIService.getSetting().then((result) { + result.fold( + (setting) => _updateSettingConfiguration(setting), + (err) => Log.error(err), + ); + }); + } + + void _listenOnFilterChanges() { + //Listen on the fitler changes + _filterListener.start(onFilterChanged: (result) { + result.fold( + (changeset) { + 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), + ); + } + + // Inserts the new filter if it's not exist + for (final newFilter in changeset.insertFilters) { + final filterIndex = filters + .indexWhere((element) => element.filter.id == newFilter.id); + if (filterIndex == -1) { + final fieldInfo = _findFieldInfoForFilter(fieldInfos, newFilter); + if (fieldInfo != null) { + filters.add(FilterInfo(gridId, newFilter, fieldInfo)); + } + } + } + + for (final updatedFilter in changeset.updateFilters) { + final filterIndex = filters.indexWhere( + (element) => element.filter.id == updatedFilter.filterId, + ); + // Remove the old filter + if (filterIndex != -1) { + filters.removeAt(filterIndex); + _filterPBByFieldId.removeWhere( + (key, value) => value.id == updatedFilter.filterId); + } + + // Insert the filter if there is a fitler and its field info is + // not null + if (updatedFilter.hasFilter()) { + final fieldInfo = _findFieldInfoForFilter( + fieldInfos, + updatedFilter.filter, + ); + + if (fieldInfo != null) { + // Insert the filter with the position: filterIndex, otherwise, + // append it to the end of the list. + final filterInfo = + FilterInfo(gridId, updatedFilter.filter, fieldInfo); + if (filterIndex != -1) { + filters.insert(filterIndex, filterInfo); + } else { + filters.add(filterInfo); + } + _filterPBByFieldId[fieldInfo.id] = updatedFilter.filter; + } + + _updateFieldInfos(); + } + } + _filterNotifier?.filters = filters; + }, + (err) => Log.error(err), + ); + }); + } + + void _listenOnSettingChanges() { + //Listen on setting changes + _settingListener.start(onSettingUpdated: (result) { + result.fold( + (setting) => _updateSettingConfiguration(setting), + (r) => Log.error(r), + ); + }); + } + + void _listenOnFieldChanges() { //Listen on field's changes _fieldListener.start(onFieldsChanged: (result) { result.fold( (changeset) { _deleteFields(changeset.deletedFields); _insertFields(changeset.insertedFields); - _updateFields(changeset.updatedFields); - for (final listener in _changesetCallbackMap.values) { - listener(changeset); + + final updateFields = _updateFields(changeset.updatedFields); + for (final listener in _updatedFieldCallbacks.values) { + listener(updateFields); } }, (err) => Log.error(err), ); }); - - //Listen on setting changes - _settingListener.start(onSettingUpdated: (result) { - result.fold( - (setting) => _updateGroupConfiguration(setting), - (r) => Log.error(r), - ); - }); - - _settingFFIService.getSetting().then((result) { - result.fold( - (setting) => _updateGroupConfiguration(setting), - (err) => Log.error(err), - ); - }); } - GridFieldContext? getField(String fieldId) { - final fields = _fieldNotifier?.fieldContexts - .where( - (element) => element.id == fieldId, - ) - .toList(); - if (fields?.isEmpty ?? true) { - return null; - } - return fields!.first; - } - - void _updateGroupConfiguration(GridSettingPB setting) { - _configurationByFieldId.clear(); + void _updateSettingConfiguration(GridSettingPB setting) { + _groupConfigurationByFieldId.clear(); for (final configuration in setting.groupConfigurations.items) { - _configurationByFieldId[configuration.fieldId] = configuration; + _groupConfigurationByFieldId[configuration.fieldId] = configuration; } - _updateFieldContexts(); + + for (final configuration in setting.filters.items) { + _filterPBByFieldId[configuration.fieldId] = configuration; + } + + _updateFieldInfos(); } - void _updateFieldContexts() { + void _updateFieldInfos() { if (_fieldNotifier != null) { - for (var field in _fieldNotifier!.fieldContexts) { - if (_configurationByFieldId[field.id] != null) { - field._isGroupField = true; - } else { - field._isGroupField = false; - } + for (var field in _fieldNotifier!.fieldInfos) { + field._isGroupField = _groupConfigurationByFieldId[field.id] != null; + field._hasFilter = _filterPBByFieldId[field.id] != null; } _fieldNotifier?.notify(); } @@ -116,20 +251,33 @@ class GridFieldController { Future dispose() async { await _fieldListener.stop(); + await _filterListener.stop(); + await _settingListener.stop(); + + for (final callback in _fieldCallbacks.values) { + _fieldNotifier?.removeListener(callback); + } _fieldNotifier?.dispose(); _fieldNotifier = null; + + for (final callback in _filterCallbacks.values) { + _filterNotifier?.removeListener(callback); + } + _filterNotifier?.dispose(); + _filterNotifier = null; } - Future> loadFields( - {required List fieldIds}) async { + Future> loadFields({ + required List fieldIds, + }) async { final result = await _gridFFIService.getFields(fieldIds: fieldIds); return Future( () => result.fold( (newFields) { - _fieldNotifier?.fieldContexts = newFields.items - .map((field) => GridFieldContext(field: field)) - .toList(); - _updateFieldContexts(); + _fieldNotifier?.fieldInfos = + newFields.map((field) => FieldInfo(field: field)).toList(); + _loadFilters(); + _updateFieldInfos(); return left(unit); }, (err) => right(err), @@ -137,20 +285,43 @@ class GridFieldController { ); } + Future> _loadFilters() async { + return _filterFFIService.getAllFilters().then((result) { + return result.fold( + (filterPBs) { + final List filters = []; + for (final filterPB in filterPBs) { + final fieldInfo = _findFieldInfoForFilter(fieldInfos, filterPB); + if (fieldInfo != null) { + final filterInfo = FilterInfo(gridId, filterPB, fieldInfo); + filters.add(filterInfo); + } + } + + _updateFieldInfos(); + _filterNotifier?.filters = filters; + return left(unit); + }, + (err) => right(err), + ); + }); + } + void addListener({ OnReceiveFields? onFields, - OnChangeset? onChangeset, + OnReceiveUpdateFields? onFieldsUpdated, + OnReceiveFilters? onFilters, bool Function()? listenWhen, }) { - if (onChangeset != null) { - callback(c) { + if (onFieldsUpdated != null) { + callback(List updateFields) { if (listenWhen != null && listenWhen() == false) { return; } - onChangeset(c); + onFieldsUpdated(updateFields); } - _changesetCallbackMap[onChangeset] = callback; + _updatedFieldCallbacks[onFieldsUpdated] = callback; } if (onFields != null) { @@ -158,27 +329,43 @@ class GridFieldController { if (listenWhen != null && listenWhen() == false) { return; } - onFields(fieldContexts); + onFields(fieldInfos); } - _fieldCallbackMap[onFields] = callback; + _fieldCallbacks[onFields] = callback; _fieldNotifier?.addListener(callback); } + + if (onFilters != null) { + callback() { + if (listenWhen != null && listenWhen() == false) { + return; + } + onFilters(filterInfos); + } + + _filterCallbacks[onFilters] = callback; + callback(); + _filterNotifier?.addListener(callback); + } } void removeListener({ OnReceiveFields? onFieldsListener, - OnChangeset? onChangesetListener, + OnReceiveFilters? onFiltersListener, + OnReceiveUpdateFields? onChangesetListener, }) { if (onFieldsListener != null) { - final callback = _fieldCallbackMap.remove(onFieldsListener); + final callback = _fieldCallbacks.remove(onFieldsListener); if (callback != null) { _fieldNotifier?.removeListener(callback); } } - - if (onChangesetListener != null) { - _changesetCallbackMap.remove(onChangesetListener); + if (onFiltersListener != null) { + final callback = _filterCallbacks.remove(onFiltersListener); + if (callback != null) { + _filterNotifier?.removeListener(callback); + } } } @@ -186,58 +373,65 @@ class GridFieldController { if (deletedFields.isEmpty) { return; } - final List newFields = fieldContexts; + final List newFields = fieldInfos; final Map deletedFieldMap = { for (var fieldOrder in deletedFields) fieldOrder.fieldId: fieldOrder }; newFields.retainWhere((field) => (deletedFieldMap[field.id] == null)); - _fieldNotifier?.fieldContexts = newFields; + _fieldNotifier?.fieldInfos = newFields; } void _insertFields(List insertedFields) { if (insertedFields.isEmpty) { return; } - final List newFields = fieldContexts; + final List newFields = fieldInfos; for (final indexField in insertedFields) { - final gridField = GridFieldContext(field: indexField.field_1); + final gridField = FieldInfo(field: indexField.field_1); if (newFields.length > indexField.index) { newFields.insert(indexField.index, gridField); } else { newFields.add(gridField); } } - _fieldNotifier?.fieldContexts = newFields; + _fieldNotifier?.fieldInfos = newFields; } - void _updateFields(List updatedFields) { - if (updatedFields.isEmpty) { - return; + List _updateFields(List updatedFieldPBs) { + if (updatedFieldPBs.isEmpty) { + return []; } - final List newFields = fieldContexts; - for (final updatedField in updatedFields) { + + final List newFields = fieldInfos; + final List updatedFields = []; + for (final updatedFieldPB in updatedFieldPBs) { final index = - newFields.indexWhere((field) => field.id == updatedField.id); + newFields.indexWhere((field) => field.id == updatedFieldPB.id); if (index != -1) { newFields.removeAt(index); - final gridField = GridFieldContext(field: updatedField); - newFields.insert(index, gridField); + final fieldInfo = FieldInfo(field: updatedFieldPB); + newFields.insert(index, fieldInfo); + updatedFields.add(fieldInfo); } } - _fieldNotifier?.fieldContexts = newFields; + + if (updatedFields.isNotEmpty) { + _fieldNotifier?.fieldInfos = newFields; + } + return updatedFields; } } class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { final GridFieldController _cache; - OnChangeset? _onChangesetFn; + OnReceiveUpdateFields? _onChangesetFn; OnReceiveFields? _onFieldFn; GridRowFieldNotifierImpl(GridFieldController cache) : _cache = cache; @override - UnmodifiableListView get fields => - UnmodifiableListView(_cache.fieldContexts); + UnmodifiableListView get fields => + UnmodifiableListView(_cache.fieldInfos); @override void onRowFieldsChanged(VoidCallback callback) { @@ -246,14 +440,14 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { } @override - void onRowFieldChanged(void Function(FieldPB) callback) { - _onChangesetFn = (GridFieldChangesetPB changeset) { - for (final updatedField in changeset.updatedFields) { + void onRowFieldChanged(void Function(FieldInfo) callback) { + _onChangesetFn = (List fieldInfos) { + for (final updatedField in fieldInfos) { callback(updatedField); } }; - _cache.addListener(onChangeset: _onChangesetFn); + _cache.addListener(onFieldsUpdated: _onChangesetFn); } @override @@ -270,10 +464,25 @@ class GridRowFieldNotifierImpl extends IGridRowFieldNotifier { } } -class GridFieldContext { +FieldInfo? _findFieldInfoForFilter( + List fieldInfos, FilterPB filter) { + final fieldIndex = fieldInfos.indexWhere((element) { + return element.id == filter.fieldId && + element.fieldType == filter.fieldType; + }); + if (fieldIndex != -1) { + return fieldInfos[fieldIndex]; + } else { + return null; + } +} + +class FieldInfo { final FieldPB _field; bool _isGroupField = false; + bool _hasFilter = false; + String get id => _field.id; FieldType get fieldType => _field.fieldType; @@ -290,6 +499,8 @@ class GridFieldContext { bool get isGroupField => _isGroupField; + bool get hasFilter => _hasFilter; + bool get canGroup { switch (_field.fieldType) { case FieldType.Checkbox: @@ -311,5 +522,13 @@ class GridFieldContext { return false; } - GridFieldContext({required FieldPB field}) : _field = field; + bool get canCreateFilter { + if (hasFilter) return false; + + if (_field.fieldType != FieldType.RichText) return false; + + return true; + } + + FieldInfo({required FieldPB field}) : _field = field; } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart index f29186e55a..0f3b5b66c6 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/field_service.dart @@ -34,7 +34,6 @@ class FieldService { bool? frozen, bool? visibility, double? width, - List? typeOptionData, }) { var payload = FieldChangesetPB.create() ..gridId = gridId @@ -60,10 +59,6 @@ class FieldService { payload.width = width.toInt(); } - if (typeOptionData != null) { - payload.typeOptionData = typeOptionData; - } - return GridEventUpdateField(payload).send(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart index 621114045c..159de97d74 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/field/type_option/type_option_data_controller.dart @@ -4,7 +4,7 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:app_flowy/plugins/grid/application/field/field_service.dart'; import 'package:dartz/dartz.dart'; -import 'package:protobuf/protobuf.dart'; +import 'package:protobuf/protobuf.dart' hide FieldInfo; import 'package:flowy_sdk/log.dart'; import 'type_option_context.dart'; @@ -18,18 +18,18 @@ class TypeOptionDataController { /// Returns a [TypeOptionDataController] used to modify the specified /// [FieldPB]'s data /// - /// Should call [loadTypeOptionData] if the passed-in [GridFieldContext] + /// Should call [loadTypeOptionData] if the passed-in [FieldInfo] /// is null /// TypeOptionDataController({ required this.gridId, required this.loader, - GridFieldContext? fieldContext, + FieldInfo? fieldInfo, }) { - if (fieldContext != null) { + if (fieldInfo != null) { _data = TypeOptionPB.create() ..gridId = gridId - ..field_2 = fieldContext.field; + ..field_2 = fieldInfo.field; } } diff --git a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_bloc.dart deleted file mode 100644 index a98e40a3e0..0000000000 --- a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_bloc.dart +++ /dev/null @@ -1,206 +0,0 @@ -import 'package:app_flowy/plugins/grid/application/filter/filter_listener.dart'; -import 'package:flowy_sdk/log.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'dart:async'; -import 'filter_service.dart'; - -part 'filter_bloc.freezed.dart'; - -class GridFilterBloc extends Bloc { - final String viewId; - final FilterFFIService _ffiService; - final FilterListener _listener; - GridFilterBloc({required this.viewId}) - : _ffiService = FilterFFIService(viewId: viewId), - _listener = FilterListener(viewId: viewId), - super(GridFilterState.initial()) { - on( - (event, emit) async { - event.when( - initial: () async { - _startListening(); - await _loadFilters(); - }, - deleteFilter: ( - String fieldId, - String filterId, - FieldType fieldType, - ) { - _ffiService.deleteFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: fieldType, - ); - }, - didReceiveFilters: (filters) { - emit(state.copyWith(filters: filters)); - }, - createCheckboxFilter: ( - String fieldId, - CheckboxFilterCondition condition, - ) { - _ffiService.createCheckboxFilter( - fieldId: fieldId, - condition: condition, - ); - }, - createNumberFilter: ( - String fieldId, - NumberFilterCondition condition, - String content, - ) { - _ffiService.createNumberFilter( - fieldId: fieldId, - condition: condition, - content: content, - ); - }, - createTextFilter: ( - String fieldId, - TextFilterCondition condition, - String content, - ) { - _ffiService.createTextFilter( - fieldId: fieldId, - condition: condition, - ); - }, - createDateFilter: ( - String fieldId, - DateFilterCondition condition, - int timestamp, - ) { - _ffiService.createDateFilter( - fieldId: fieldId, - condition: condition, - timestamp: timestamp, - ); - }, - createDateFilterInRange: ( - String fieldId, - DateFilterCondition condition, - int start, - int end, - ) { - _ffiService.createDateFilter( - fieldId: fieldId, - condition: condition, - start: start, - end: end, - ); - }, - ); - }, - ); - } - - void _startListening() { - _listener.start(onFilterChanged: (result) { - result.fold( - (changeset) { - final List filters = List.from(state.filters); - - // Deletes the filters - final deleteFilterIds = - changeset.deleteFilters.map((e) => e.id).toList(); - filters.retainWhere( - (element) => !deleteFilterIds.contains(element.id), - ); - - // Inserts the new filter if it's not exist - for (final newFilter in changeset.insertFilters) { - final index = - filters.indexWhere((element) => element.id == newFilter.id); - if (index == -1) { - filters.add(newFilter); - } - } - - if (!isClosed) { - add(GridFilterEvent.didReceiveFilters(filters)); - } - }, - (err) => Log.error(err), - ); - }); - } - - Future _loadFilters() async { - final result = await _ffiService.getAllFilters(); - result.fold( - (filters) { - if (!isClosed) { - add(GridFilterEvent.didReceiveFilters(filters)); - } - }, - (err) => Log.error(err), - ); - } - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } -} - -@freezed -class GridFilterEvent with _$GridFilterEvent { - const factory GridFilterEvent.initial() = _Initial; - const factory GridFilterEvent.didReceiveFilters(List filters) = - _DidReceiveFilters; - - const factory GridFilterEvent.deleteFilter({ - required String fieldId, - required String filterId, - required FieldType fieldType, - }) = _DeleteFilter; - - const factory GridFilterEvent.createTextFilter({ - required String fieldId, - required TextFilterCondition condition, - required String content, - }) = _CreateTextFilter; - - const factory GridFilterEvent.createCheckboxFilter({ - required String fieldId, - required CheckboxFilterCondition condition, - }) = _CreateCheckboxFilter; - - const factory GridFilterEvent.createNumberFilter({ - required String fieldId, - required NumberFilterCondition condition, - required String content, - }) = _CreateCheckboxFitler; - - const factory GridFilterEvent.createDateFilter({ - required String fieldId, - required DateFilterCondition condition, - required int start, - }) = _CreateDateFitler; - - const factory GridFilterEvent.createDateFilterInRange({ - required String fieldId, - required DateFilterCondition condition, - required int start, - required int end, - }) = _CreateDateFitlerInRange; -} - -@freezed -class GridFilterState with _$GridFilterState { - const factory GridFilterState({ - required List filters, - }) = _GridFilterState; - - factory GridFilterState.initial() => const GridFilterState( - filters: [], - ); -} diff --git a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_create_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_create_bloc.dart new file mode 100644 index 0000000000..e62e36ae1c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_create_bloc.dart @@ -0,0 +1,179 @@ +import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbenum.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'filter_service.dart'; + +part 'filter_create_bloc.freezed.dart'; + +class GridCreateFilterBloc + extends Bloc { + final String viewId; + final FilterFFIService _ffiService; + final GridFieldController fieldController; + void Function(List)? _onFieldFn; + GridCreateFilterBloc({required this.viewId, required this.fieldController}) + : _ffiService = FilterFFIService(viewId: viewId), + super(GridCreateFilterState.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), + ), + ); + }, + createDefaultFilter: (FieldInfo field) { + emit(state.copyWith(didCreateFilter: true)); + _createDefaultFilter(field); + }, + ); + }, + ); + } + + List _filterFields( + List fields, + String filterText, + ) { + final List allFields = List.from(fields); + final keyword = filterText.toLowerCase(); + allFields.retainWhere((field) { + if (field.canCreateFilter) { + return false; + } + + if (filterText.isNotEmpty) { + return field.name.toLowerCase().contains(keyword); + } + + return true; + }); + + return allFields; + } + + void _startListening() { + _onFieldFn = (fields) { + fields.retainWhere((field) => field.hasFilter == false); + add(GridCreateFilterEvent.didReceiveFields(fields)); + }; + fieldController.addListener(onFields: _onFieldFn); + } + + Future> _createDefaultFilter(FieldInfo field) async { + final fieldId = field.id; + switch (field.fieldType) { + case FieldType.Checkbox: + return _ffiService.insertCheckboxFilter( + fieldId: fieldId, + condition: CheckboxFilterCondition.IsChecked, + ); + case FieldType.DateTime: + final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000; + return _ffiService.insertDateFilter( + fieldId: fieldId, + condition: DateFilterCondition.DateIs, + timestamp: timestamp, + ); + case FieldType.MultiSelect: + return _ffiService.insertSingleSelectFilter( + fieldId: fieldId, + condition: SelectOptionCondition.OptionIs, + ); + case FieldType.Number: + return _ffiService.insertNumberFilter( + fieldId: fieldId, + condition: NumberFilterCondition.Equal, + content: "", + ); + case FieldType.RichText: + return _ffiService.insertTextFilter( + fieldId: fieldId, + condition: TextFilterCondition.Contains, + content: '', + ); + case FieldType.SingleSelect: + return _ffiService.insertSingleSelectFilter( + fieldId: fieldId, + condition: SelectOptionCondition.OptionIs, + ); + case FieldType.URL: + return _ffiService.insertURLFilter( + fieldId: fieldId, + condition: TextFilterCondition.Contains, + ); + } + + return left(unit); + } + + @override + Future close() async { + if (_onFieldFn != null) { + fieldController.removeListener(onFieldsListener: _onFieldFn); + _onFieldFn = null; + } + return super.close(); + } +} + +@freezed +class GridCreateFilterEvent with _$GridCreateFilterEvent { + const factory GridCreateFilterEvent.initial() = _Initial; + const factory GridCreateFilterEvent.didReceiveFields(List fields) = + _DidReceiveFields; + + const factory GridCreateFilterEvent.createDefaultFilter(FieldInfo field) = + _CreateDefaultFilter; + + const factory GridCreateFilterEvent.didReceiveFilterText(String text) = + _DidReceiveFilterText; +} + +@freezed +class GridCreateFilterState with _$GridCreateFilterState { + const factory GridCreateFilterState({ + required String filterText, + required List creatableFields, + required List allFields, + required bool didCreateFilter, + }) = _GridFilterState; + + factory GridCreateFilterState.initial(List fields) { + return GridCreateFilterState( + filterText: "", + creatableFields: getCreatableFilter(fields), + allFields: fields, + didCreateFilter: false, + ); + } +} + +List getCreatableFilter(List fieldInfos) { + final List creatableFields = List.from(fieldInfos); + creatableFields.retainWhere((element) => element.canCreateFilter); + return creatableFields; +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart index 94fd5942d7..79d4c663e3 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_listener.dart @@ -6,17 +6,18 @@ import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/filter_changeset.pb.dart'; import 'package:dartz/dartz.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart'; typedef UpdateFilterNotifiedValue = Either; -class FilterListener { +class FiltersListener { final String viewId; PublishNotifier? _filterNotifier = PublishNotifier(); GridNotificationListener? _listener; - FilterListener({required this.viewId}); + FiltersListener({required this.viewId}); void start({ required void Function(UpdateFilterNotifiedValue) onFilterChanged, @@ -51,3 +52,76 @@ class FilterListener { _filterNotifier = null; } } + +class FilterListener { + final String viewId; + final String filterId; + + PublishNotifier? _onDeleteNotifier = PublishNotifier(); + PublishNotifier? _onUpdateNotifier = PublishNotifier(); + + GridNotificationListener? _listener; + FilterListener({required this.viewId, required this.filterId}); + + void start({ + void Function()? onDeleted, + void Function(FilterPB)? onUpdated, + }) { + _onDeleteNotifier?.addPublishListener((_) { + onDeleted?.call(); + }); + + _onUpdateNotifier?.addPublishListener((filter) { + onUpdated?.call(filter); + }); + + _listener = GridNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void handleChangeset(FilterChangesetNotificationPB changeset) { + // check the delete filter + final deletedIndex = changeset.deleteFilters.indexWhere( + (element) => element.id == filterId, + ); + if (deletedIndex != -1) { + _onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex]; + } + + // check the updated filter + final updatedIndex = changeset.updateFilters.indexWhere( + (element) => element.filter.id == filterId, + ); + if (updatedIndex != -1) { + _onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter; + } + } + + void _handler( + GridDartNotification ty, + Either result, + ) { + switch (ty) { + case GridDartNotification.DidUpdateFilter: + result.fold( + (payload) => handleChangeset( + FilterChangesetNotificationPB.fromBuffer(payload)), + (error) {}, + ); + break; + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _onDeleteNotifier?.dispose(); + _onDeleteNotifier = null; + + _onUpdateNotifier?.dispose(); + _onUpdateNotifier = null; + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/filter/filter_menu_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_menu_bloc.dart new file mode 100644 index 0000000000..f1b6b80e54 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/filter/filter_menu_bloc.dart @@ -0,0 +1,92 @@ +import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; + +part 'filter_menu_bloc.freezed.dart'; + +class GridFilterMenuBloc + extends Bloc { + final String viewId; + final GridFieldController fieldController; + void Function(List)? _onFilterFn; + + GridFilterMenuBloc({required this.viewId, required this.fieldController}) + : super(GridFilterMenuState.initial( + viewId, + fieldController.filterInfos, + fieldController.fieldInfos, + )) { + on( + (event, emit) async { + event.when( + initial: () { + _startListening(); + }, + didReceiveFilters: (filters) { + emit(state.copyWith(filters: filters)); + }, + toggleMenu: () { + final isVisible = !state.isVisible; + emit(state.copyWith(isVisible: isVisible)); + }, + didReceiveFields: (List fields) { + emit(state.copyWith(fields: fields)); + }, + ); + }, + ); + } + + void _startListening() { + _onFilterFn = (filters) { + add(GridFilterMenuEvent.didReceiveFilters(filters)); + }; + + fieldController.addListener(onFilters: (filters) { + _onFilterFn?.call(filters); + }); + } + + @override + Future close() { + if (_onFilterFn != null) { + fieldController.removeListener(onFiltersListener: _onFilterFn!); + _onFilterFn = null; + } + return super.close(); + } +} + +@freezed +class GridFilterMenuEvent with _$GridFilterMenuEvent { + const factory GridFilterMenuEvent.initial() = _Initial; + const factory GridFilterMenuEvent.didReceiveFilters( + List filters) = _DidReceiveFilters; + const factory GridFilterMenuEvent.didReceiveFields(List fields) = + _DidReceiveFields; + const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility; +} + +@freezed +class GridFilterMenuState with _$GridFilterMenuState { + const factory GridFilterMenuState({ + required String viewId, + required List filters, + required List fields, + required bool isVisible, + }) = _GridFilterMenuState; + + factory GridFilterMenuState.initial( + String viewId, + List filterInfos, + List fields, + ) => + GridFilterMenuState( + viewId: viewId, + filters: filterInfos, + fields: fields, + isVisible: false, + ); +} 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 4cb703bcd3..67348ae2c7 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 @@ -28,37 +28,42 @@ class FilterFFIService { }); } - Future> createTextFilter({ + Future> insertTextFilter({ required String fieldId, + String? filterId, required TextFilterCondition condition, - String content = "", + required String content, }) { final filter = TextFilterPB() ..condition = condition ..content = content; - return createFilter( + return insertFilter( fieldId: fieldId, + filterId: filterId, fieldType: FieldType.RichText, data: filter.writeToBuffer(), ); } - Future> createCheckboxFilter({ + Future> insertCheckboxFilter({ required String fieldId, + String? filterId, required CheckboxFilterCondition condition, }) { final filter = CheckboxFilterPB()..condition = condition; - return createFilter( + return insertFilter( fieldId: fieldId, + filterId: filterId, fieldType: FieldType.Checkbox, data: filter.writeToBuffer(), ); } - Future> createNumberFilter({ + Future> insertNumberFilter({ required String fieldId, + String? filterId, required NumberFilterCondition condition, String content = "", }) { @@ -66,15 +71,17 @@ class FilterFFIService { ..condition = condition ..content = content; - return createFilter( + return insertFilter( fieldId: fieldId, + filterId: filterId, fieldType: FieldType.Number, data: filter.writeToBuffer(), ); } - Future> createDateFilter({ + Future> insertDateFilter({ required String fieldId, + String? filterId, required DateFilterCondition condition, int? start, int? end, @@ -93,15 +100,17 @@ class FilterFFIService { } } - return createFilter( + return insertFilter( fieldId: fieldId, + filterId: filterId, fieldType: FieldType.DateTime, data: filter.writeToBuffer(), ); } - Future> createURLFilter({ + Future> insertURLFilter({ required String fieldId, + String? filterId, required TextFilterCondition condition, String content = "", }) { @@ -109,15 +118,17 @@ class FilterFFIService { ..condition = condition ..content = content; - return createFilter( + return insertFilter( fieldId: fieldId, + filterId: filterId, fieldType: FieldType.URL, data: filter.writeToBuffer(), ); } - Future> createSingleSelectFilter({ + Future> insertSingleSelectFilter({ required String fieldId, + String? filterId, required SelectOptionCondition condition, List optionIds = const [], }) { @@ -125,15 +136,17 @@ class FilterFFIService { ..condition = condition ..optionIds.addAll(optionIds); - return createFilter( + return insertFilter( fieldId: fieldId, + filterId: filterId, fieldType: FieldType.SingleSelect, data: filter.writeToBuffer(), ); } - Future> createMultiSelectFilter({ + Future> insertMultiSelectFilter({ required String fieldId, + String? filterId, required SelectOptionCondition condition, List optionIds = const [], }) { @@ -141,25 +154,31 @@ class FilterFFIService { ..condition = condition ..optionIds.addAll(optionIds); - return createFilter( + return insertFilter( fieldId: fieldId, + filterId: filterId, fieldType: FieldType.MultiSelect, data: filter.writeToBuffer(), ); } - Future> createFilter({ + Future> insertFilter({ required String fieldId, + String? filterId, required FieldType fieldType, required List data, }) { TextFilterCondition.DoesNotContain.value; - final insertFilterPayload = CreateFilterPayloadPB.create() + var insertFilterPayload = AlterFilterPayloadPB.create() ..fieldId = fieldId ..fieldType = fieldType ..data = data; + if (filterId != null) { + insertFilterPayload.filterId = filterId; + } + final payload = GridSettingChangesetPB.create() ..gridId = viewId ..insertFilter = insertFilterPayload; @@ -189,6 +208,7 @@ class FilterFFIService { final payload = GridSettingChangesetPB.create() ..gridId = viewId ..deleteFilter = deleteFilterPayload; + return GridEventUpdateGridSetting(payload).send().then((result) { return result.fold( (l) => left(l), diff --git a/frontend/app_flowy/lib/plugins/grid/application/filter/text_filter_editor_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/filter/text_filter_editor_bloc.dart new file mode 100644 index 0000000000..86b9dc623b --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/filter/text_filter_editor_bloc.dart @@ -0,0 +1,110 @@ +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flowy_sdk/log.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pbserver.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'dart:async'; +import 'filter_listener.dart'; +import 'filter_service.dart'; + +part 'text_filter_editor_bloc.freezed.dart'; + +class TextFilterEditorBloc + extends Bloc { + final FilterInfo filterInfo; + final FilterFFIService _ffiService; + final FilterListener _listener; + + TextFilterEditorBloc({required this.filterInfo}) + : _ffiService = FilterFFIService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(TextFilterEditorState.initial(filterInfo)) { + on( + (event, emit) async { + event.when( + initial: () async { + _startListening(); + }, + updateCondition: (TextFilterCondition condition) { + final textFilter = filterInfo.textFilter()!; + _ffiService.insertTextFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.field.id, + condition: condition, + content: textFilter.content, + ); + }, + updateContent: (content) { + final textFilter = filterInfo.textFilter(); + if (textFilter != null) { + _ffiService.insertTextFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.field.id, + condition: textFilter.condition, + content: content, + ); + } else { + Log.error("Invalid text filter"); + } + }, + delete: () { + _ffiService.deleteFilter( + fieldId: filterInfo.field.id, + filterId: filterInfo.filter.id, + fieldType: filterInfo.field.fieldType, + ); + }, + didReceiveFilter: (FilterPB filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + emit(state.copyWith(filterInfo: filterInfo)); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onDeleted: () { + if (!isClosed) add(const TextFilterEditorEvent.delete()); + }, + onUpdated: (filter) { + if (!isClosed) add(TextFilterEditorEvent.didReceiveFilter(filter)); + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class TextFilterEditorEvent with _$TextFilterEditorEvent { + const factory TextFilterEditorEvent.initial() = _Initial; + const factory TextFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory TextFilterEditorEvent.updateCondition( + TextFilterCondition condition) = _UpdateCondition; + const factory TextFilterEditorEvent.updateContent(String content) = + _UpdateContent; + const factory TextFilterEditorEvent.delete() = _Delete; +} + +@freezed +class TextFilterEditorState with _$TextFilterEditorState { + const factory TextFilterEditorState({required FilterInfo filterInfo}) = + _GridFilterState; + + factory TextFilterEditorState.initial(FilterInfo filterInfo) { + return TextFilterEditorState( + filterInfo: filterInfo, + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart index fb3a9cc518..ec35a05eef 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_bloc.dart @@ -16,12 +16,11 @@ import 'row/row_service.dart'; part 'grid_bloc.freezed.dart'; class GridBloc extends Bloc { - final GridDataController dataController; + final GridController gridController; void Function()? _createRowOperation; - GridBloc({required ViewPB view}) - : dataController = GridDataController(view: view), - super(GridState.initial(view.id)) { + GridBloc({required ViewPB view, required this.gridController}) + : super(GridState.initial(view.id)) { on( (event, emit) async { await event.when( @@ -32,9 +31,9 @@ class GridBloc extends Bloc { createRow: () { state.loadingState.when( loading: () { - _createRowOperation = () => dataController.createRow(); + _createRowOperation = () => gridController.createRow(); }, - finish: (_) => dataController.createRow(), + finish: (_) => gridController.createRow(), ); }, deleteRow: (rowInfo) async { @@ -66,17 +65,17 @@ class GridBloc extends Bloc { @override Future close() async { - await dataController.dispose(); + await gridController.dispose(); return super.close(); } GridRowCache? getRowCache(String blockId, String rowId) { - final GridBlockCache? blockCache = dataController.blocks[blockId]; + final GridBlockCache? blockCache = gridController.blocks[blockId]; return blockCache?.rowCache; } void _startListening() { - dataController.addListener( + gridController.addListener( onGridChanged: (grid) { if (!isClosed) { add(GridEvent.didReceiveGridUpdate(grid)); @@ -96,7 +95,7 @@ class GridBloc extends Bloc { } Future _openGrid(Emitter emit) async { - final result = await dataController.openGrid(); + final result = await gridController.openGrid(); result.fold( (grid) { if (_createRowOperation != null) { @@ -124,7 +123,7 @@ class GridEvent with _$GridEvent { RowsChangedReason listState, ) = _DidReceiveRowUpdate; const factory GridEvent.didReceiveFieldUpdate( - UnmodifiableListView fields, + List fields, ) = _DidReceiveFieldUpdate; const factory GridEvent.didReceiveGridUpdate( @@ -163,9 +162,9 @@ class GridLoadingState with _$GridLoadingState { } class GridFieldEquatable extends Equatable { - final UnmodifiableListView _fields; + final List _fields; const GridFieldEquatable( - UnmodifiableListView fields, + List fields, ) : _fields = fields; @override @@ -182,6 +181,5 @@ class GridFieldEquatable extends Equatable { ]; } - UnmodifiableListView get value => - UnmodifiableListView(_fields); + UnmodifiableListView get value => UnmodifiableListView(_fields); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart index 08964de4f6..af25cc1007 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_data_controller.dart @@ -1,5 +1,6 @@ import 'dart:collection'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart'; @@ -12,7 +13,8 @@ import 'field/field_controller.dart'; import 'prelude.dart'; import 'row/row_cache.dart'; -typedef OnFieldsChanged = void Function(UnmodifiableListView); +typedef OnFieldsChanged = void Function(List); +typedef OnFiltersChanged = void Function(List); typedef OnGridChanged = void Function(GridPB); typedef OnRowsChanged = void Function( @@ -21,20 +23,19 @@ typedef OnRowsChanged = void Function( ); typedef ListenOnRowChangedCondition = bool Function(); -class GridDataController { +class GridController { final String gridId; final GridFFIService _gridFFIService; final GridFieldController fieldController; + OnRowsChanged? _onRowChanged; + OnGridChanged? _onGridChanged; + // Getters // key: the block id final LinkedHashMap _blocks; UnmodifiableMapView get blocks => UnmodifiableMapView(_blocks); - OnRowsChanged? _onRowChanged; - OnFieldsChanged? _onFieldsChanged; - OnGridChanged? _onGridChanged; - List get rowInfos { final List rows = []; for (var block in _blocks.values) { @@ -43,7 +44,7 @@ class GridDataController { return rows; } - GridDataController({required ViewPB view}) + GridController({required ViewPB view}) : gridId = view.id, // ignore: prefer_collection_literals _blocks = LinkedHashMap(), @@ -51,32 +52,36 @@ class GridDataController { fieldController = GridFieldController(gridId: view.id); void addListener({ - required OnGridChanged onGridChanged, - required OnRowsChanged onRowsChanged, - required OnFieldsChanged onFieldsChanged, + OnGridChanged? onGridChanged, + OnRowsChanged? onRowsChanged, + OnFieldsChanged? onFieldsChanged, + OnFiltersChanged? onFiltersChanged, }) { _onGridChanged = onGridChanged; _onRowChanged = onRowsChanged; - _onFieldsChanged = onFieldsChanged; - fieldController.addListener(onFields: (fields) { - _onFieldsChanged?.call(UnmodifiableListView(fields)); - }); + fieldController.addListener( + onFields: onFieldsChanged, + onFilters: onFiltersChanged, + ); } // Loads the rows from each block Future> openGrid() async { - final result = await _gridFFIService.openGrid(); - return Future( - () => result.fold( + return _gridFFIService.openGrid().then((result) { + return result.fold( (grid) async { _initialBlocks(grid.blocks); _onGridChanged?.call(grid); - return await fieldController.loadFields(fieldIds: grid.fields); + + final result = await fieldController.loadFields( + fieldIds: grid.fields, + ); + return result; }, (err) => right(err), - ), - ); + ); + }); } Future createRow() async { diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart index 5f777eedf7..eba36c33cc 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_header_bloc.dart @@ -15,7 +15,7 @@ class GridHeaderBloc extends Bloc { GridHeaderBloc({ required this.gridId, required this.fieldController, - }) : super(GridHeaderState.initial(fieldController.fieldContexts)) { + }) : super(GridHeaderState.initial(fieldController.fieldInfos)) { on( (event, emit) async { await event.map( @@ -41,7 +41,7 @@ class GridHeaderBloc extends Bloc { Future _moveField( _MoveField value, Emitter emit) async { - final fields = List.from(state.fields); + final fields = List.from(state.fields); fields.insert(value.toIndex, fields.removeAt(value.fromIndex)); emit(state.copyWith(fields: fields)); @@ -69,18 +69,18 @@ class GridHeaderBloc extends Bloc { @freezed class GridHeaderEvent with _$GridHeaderEvent { const factory GridHeaderEvent.initial() = _InitialHeader; - const factory GridHeaderEvent.didReceiveFieldUpdate( - List fields) = _DidReceiveFieldUpdate; + const factory GridHeaderEvent.didReceiveFieldUpdate(List fields) = + _DidReceiveFieldUpdate; const factory GridHeaderEvent.moveField( FieldPB field, int fromIndex, int toIndex) = _MoveField; } @freezed class GridHeaderState with _$GridHeaderState { - const factory GridHeaderState({required List fields}) = + const factory GridHeaderState({required List fields}) = _GridHeaderState; - factory GridHeaderState.initial(List fields) { + factory GridHeaderState.initial(List fields) { // final List newFields = List.from(fields); // newFields.retainWhere((field) => field.visibility); return GridHeaderState(fields: fields); diff --git a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart index a903559093..2f3d437f8b 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/grid_service.dart @@ -42,12 +42,16 @@ class GridFFIService { return GridEventCreateBoardCard(payload).send(); } - Future> getFields( - {required List fieldIds}) { - final payload = GetFieldPayloadPB.create() - ..gridId = gridId - ..fieldIds = RepeatedFieldIdPB(items: fieldIds); - return GridEventGetFields(payload).send(); + Future, FlowyError>> getFields( + {List? fieldIds}) { + var payload = GetFieldPayloadPB.create()..gridId = gridId; + + if (fieldIds != null) { + payload.fieldIds = RepeatedFieldIdPB(items: fieldIds); + } + return GridEventGetFields(payload).send().then((result) { + return result.fold((l) => left(l.items), (r) => right(r)); + }); } Future> closeGrid() { @@ -55,7 +59,7 @@ class GridFFIService { return FolderEventCloseView(request).send(); } - Future> loadGroups() { + Future> loadGroups() { final payload = GridIdPB(value: gridId); return GridEventGetGroup(payload).send(); } diff --git a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart index 5e14c2e445..385319d878 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_bloc.dart @@ -35,7 +35,7 @@ class RowBloc extends Bloc { }, didReceiveCells: (_DidReceiveCells value) async { final cells = value.gridCellMap.values - .map((e) => GridCellEquatable(e.fieldContext)) + .map((e) => GridCellEquatable(e.fieldInfo)) .toList(); emit(state.copyWith( gridCellMap: value.gridCellMap, @@ -88,16 +88,16 @@ class RowState with _$RowState { gridCellMap: cellDataMap, cells: UnmodifiableListView( cellDataMap.values - .map((e) => GridCellEquatable(e.fieldContext)) + .map((e) => GridCellEquatable(e.fieldInfo)) .toList(), ), ); } class GridCellEquatable extends Equatable { - final GridFieldContext _fieldContext; + final FieldInfo _fieldContext; - const GridCellEquatable(GridFieldContext field) : _fieldContext = field; + const GridCellEquatable(FieldInfo field) : _fieldContext = field; @override List get props => [ 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 49bd7ccab3..8361291613 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 @@ -4,18 +4,19 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; import 'package:flowy_sdk/dispatch/dispatch.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart'; import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'row_list.dart'; part 'row_cache.freezed.dart'; typedef RowUpdateCallback = void Function(); abstract class IGridRowFieldNotifier { - UnmodifiableListView get fields; + UnmodifiableListView get fields; void onRowFieldsChanged(VoidCallback callback); - void onRowFieldChanged(void Function(FieldPB) callback); + void onRowFieldChanged(void Function(FieldInfo) callback); void onRowDispose(); } @@ -30,17 +31,14 @@ class GridRowCache { /// _rows containers the current block's rows /// Use List to reverse the order of the GridRow. - List _rowInfos = []; - - /// Use Map for faster access the raw row data. - final HashMap _rowInfoByRowId; + final RowList _rowList = RowList(); final GridCellCache _cellCache; final IGridRowFieldNotifier _fieldNotifier; final _RowChangesetNotifier _rowChangeReasonNotifier; UnmodifiableListView get visibleRows { - var visibleRows = [..._rowInfos]; + var visibleRows = [..._rowList.rows]; visibleRows.retainWhere((element) => element.visible); return UnmodifiableListView(visibleRows); } @@ -52,7 +50,6 @@ class GridRowCache { required this.block, required IGridRowFieldNotifier notifier, }) : _cellCache = GridCellCache(gridId: gridId), - _rowInfoByRowId = HashMap(), _rowChangeReasonNotifier = _RowChangesetNotifier(), _fieldNotifier = notifier { // @@ -63,8 +60,7 @@ class GridRowCache { for (final row in block.rows) { final rowInfo = buildGridRow(row); - _rowInfos.add(rowInfo); - _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo; + _rowList.add(rowInfo); } } @@ -83,90 +79,50 @@ class GridRowCache { } void _deleteRows(List deletedRows) { - if (deletedRows.isEmpty) { - return; + if (deletedRows.isEmpty) return; + + final deletedIndex = _rowList.removeRows(deletedRows); + if (deletedIndex.isNotEmpty) { + _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex)); } - - final List newRows = []; - final DeletedIndexs deletedIndex = []; - final Map deletedRowByRowId = { - for (var rowId in deletedRows) rowId: rowId - }; - - _rowInfos.asMap().forEach((index, RowInfo rowInfo) { - if (deletedRowByRowId[rowInfo.rowPB.id] == null) { - newRows.add(rowInfo); - } else { - _rowInfoByRowId.remove(rowInfo.rowPB.id); - deletedIndex.add(DeletedIndex(index: index, row: rowInfo)); - } - }); - _rowInfos = newRows; - _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedIndex)); } void _insertRows(List insertRows) { - if (insertRows.isEmpty) { - return; - } + if (insertRows.isEmpty) return; - InsertedIndexs insertIndexs = []; - for (final InsertedRowPB insertRow in insertRows) { - final insertIndex = InsertedIndex( - index: insertRow.index, - rowId: insertRow.row.id, - ); - insertIndexs.add(insertIndex); - final rowInfo = buildGridRow(insertRow.row); - _rowInfos.insert(insertRow.index, rowInfo); - _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo; + InsertedIndexs insertIndexs = + _rowList.insertRows(insertRows, (rowPB) => buildGridRow(rowPB)); + if (insertIndexs.isNotEmpty) { + _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs)); } - - _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertIndexs)); } void _updateRows(List updatedRows) { - if (updatedRows.isEmpty) { - return; + if (updatedRows.isEmpty) return; + + final updatedIndexs = + _rowList.updateRows(updatedRows, (rowPB) => buildGridRow(rowPB)); + if (updatedIndexs.isNotEmpty) { + _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); } - - final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - for (final RowPB updatedRow in updatedRows) { - final rowId = updatedRow.id; - final index = _rowInfos.indexWhere( - (rowInfo) => rowInfo.rowPB.id == rowId, - ); - if (index != -1) { - final rowInfo = buildGridRow(updatedRow); - _rowInfoByRowId[rowId] = rowInfo; - - _rowInfos.removeAt(index); - _rowInfos.insert(index, rowInfo); - updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); - } - } - - _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); } void _hideRows(List invisibleRows) { - for (final rowId in invisibleRows) { - _rowInfoByRowId[rowId]?.visible = false; - } + if (invisibleRows.isEmpty) return; - if (invisibleRows.isNotEmpty) { - _rowChangeReasonNotifier - .receive(const RowsChangedReason.filterDidChange()); + final List deletedRows = _rowList.removeRows(invisibleRows); + if (deletedRows.isNotEmpty) { + _rowChangeReasonNotifier.receive(RowsChangedReason.delete(deletedRows)); } } - void _showRows(List visibleRows) { - for (final rowId in visibleRows) { - _rowInfoByRowId[rowId]?.visible = true; - } - if (visibleRows.isNotEmpty) { - _rowChangeReasonNotifier - .receive(const RowsChangedReason.filterDidChange()); + void _showRows(List visibleRows) { + if (visibleRows.isEmpty) return; + + final List insertedRows = + _rowList.insertRows(visibleRows, (rowPB) => buildGridRow(rowPB)); + if (insertedRows.isNotEmpty) { + _rowChangeReasonNotifier.receive(RowsChangedReason.insert(insertedRows)); } } @@ -188,7 +144,7 @@ class GridRowCache { notifyUpdate() { if (onCellUpdated != null) { - final rowInfo = _rowInfoByRowId[rowId]; + final rowInfo = _rowList.get(rowId); if (rowInfo != null) { final GridCellMap cellDataMap = _makeGridCells(rowId, rowInfo.rowPB); @@ -214,7 +170,7 @@ class GridRowCache { } GridCellMap loadGridCells(String rowId) { - final RowPB? data = _rowInfoByRowId[rowId]?.rowPB; + final RowPB? data = _rowList.get(rowId)?.rowPB; if (data == null) { _loadRow(rowId); } @@ -242,7 +198,7 @@ class GridRowCache { cellDataMap[field.id] = GridCellIdentifier( rowId: rowId, gridId: gridId, - fieldContext: field, + fieldInfo: field, ); } } @@ -256,26 +212,20 @@ class GridRowCache { final updatedRow = optionRow.row; updatedRow.freeze(); - final index = - _rowInfos.indexWhere((rowInfo) => rowInfo.rowPB.id == updatedRow.id); - if (index != -1) { - // update the corresponding row in _rows if they are not the same - if (_rowInfos[index].rowPB != updatedRow) { - final rowInfo = _rowInfos.removeAt(index).copyWith(rowPB: updatedRow); - _rowInfos.insert(index, rowInfo); - _rowInfoByRowId[rowInfo.rowPB.id] = rowInfo; + final rowInfo = _rowList.get(updatedRow.id); + final rowIndex = _rowList.indexOfRow(updatedRow.id); + if (rowInfo != null && rowIndex != null) { + final updatedRowInfo = rowInfo.copyWith(rowPB: updatedRow); + _rowList.remove(updatedRow.id); + _rowList.insert(rowIndex, updatedRowInfo); - // Calculate the update index - final UpdatedIndexs updatedIndexs = UpdatedIndexs(); - updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex( - index: index, - rowId: rowInfo.rowPB.id, - ); + final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); + updatedIndexs[rowInfo.rowPB.id] = UpdatedIndex( + index: rowIndex, + rowId: updatedRowInfo.rowPB.id, + ); - // - _rowChangeReasonNotifier - .receive(RowsChangedReason.update(updatedIndexs)); - } + _rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs)); } } @@ -302,7 +252,6 @@ class _RowChangesetNotifier extends ChangeNotifier { update: (_) => notifyListeners(), fieldDidChange: (_) => notifyListeners(), initial: (_) {}, - filterDidChange: (_FilterDidChange value) => notifyListeners(), ); } } @@ -311,7 +260,7 @@ class _RowChangesetNotifier extends ChangeNotifier { class RowInfo with _$RowInfo { factory RowInfo({ required String gridId, - required UnmodifiableListView fields, + required UnmodifiableListView fields, required RowPB rowPB, required bool visible, }) = _RowInfo; @@ -319,15 +268,16 @@ class RowInfo with _$RowInfo { typedef InsertedIndexs = List; typedef DeletedIndexs = List; -typedef UpdatedIndexs = LinkedHashMap; +// key: id of the row +// value: UpdatedIndex +typedef UpdatedIndexMap = LinkedHashMap; @freezed class RowsChangedReason with _$RowsChangedReason { const factory RowsChangedReason.insert(InsertedIndexs items) = _Insert; const factory RowsChangedReason.delete(DeletedIndexs items) = _Delete; - const factory RowsChangedReason.update(UpdatedIndexs indexs) = _Update; + const factory RowsChangedReason.update(UpdatedIndexMap indexs) = _Update; const factory RowsChangedReason.fieldDidChange() = _FieldDidChange; - const factory RowsChangedReason.filterDidChange() = _FilterDidChange; const factory RowsChangedReason.initial() = InitialListState; } @@ -342,10 +292,10 @@ class InsertedIndex { class DeletedIndex { final int index; - final RowInfo row; + final RowInfo rowInfo; DeletedIndex({ required this.index, - required this.row, + required this.rowInfo, }); } 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 new file mode 100644 index 0000000000..af1d9fbf6c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/application/row/row_list.dart @@ -0,0 +1,151 @@ +import 'dart:collection'; + +import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart'; + +import 'row_cache.dart'; + +class RowList { + /// _rows containers the current block's rows + /// Use List to reverse the order of the GridRow. + List _rowInfos = []; + + List get rows => List.from(_rowInfos); + + /// Use Map for faster access the raw row data. + final HashMap _rowInfoByRowId = HashMap(); + + RowInfo? get(String rowId) { + return _rowInfoByRowId[rowId]; + } + + int? indexOfRow(String rowId) { + final rowInfo = _rowInfoByRowId[rowId]; + if (rowInfo != null) { + return _rowInfos.indexOf(rowInfo); + } + return null; + } + + void add(RowInfo rowInfo) { + final rowId = rowInfo.rowPB.id; + if (contains(rowId)) { + final index = + _rowInfos.indexWhere((element) => element.rowPB.id == rowId); + _rowInfos.removeAt(index); + _rowInfos.insert(index, rowInfo); + } else { + _rowInfos.add(rowInfo); + } + _rowInfoByRowId[rowId] = rowInfo; + } + + void insert(int index, RowInfo rowInfo) { + final rowId = rowInfo.rowPB.id; + var insertedIndex = index; + if (_rowInfos.length < insertedIndex) { + insertedIndex = _rowInfos.length; + } + + final oldRowInfo = get(rowId); + if (oldRowInfo != null) { + _rowInfos.insert(insertedIndex, rowInfo); + _rowInfos.remove(oldRowInfo); + } else { + _rowInfos.insert(insertedIndex, rowInfo); + } + _rowInfoByRowId[rowId] = rowInfo; + } + + RowInfo? remove(String rowId) { + final rowInfo = _rowInfoByRowId[rowId]; + if (rowInfo != null) { + final index = _rowInfos.indexOf(rowInfo); + if (index != -1) { + _rowInfoByRowId.remove(rowInfo.rowPB.id); + _rowInfos.remove(rowInfo); + } + } + return rowInfo; + } + + InsertedIndexs insertRows( + List insertedRows, + RowInfo Function(RowPB) builder, + ) { + InsertedIndexs insertIndexs = []; + for (final insertRow in insertedRows) { + final isContains = contains(insertRow.row.id); + + var index = insertRow.index; + if (_rowInfos.length < index) { + index = _rowInfos.length; + } + insert(index, builder(insertRow.row)); + + if (!isContains) { + insertIndexs.add(InsertedIndex( + index: index, + rowId: insertRow.row.id, + )); + } + } + return insertIndexs; + } + + DeletedIndexs removeRows(List rowIds) { + final List newRows = []; + final DeletedIndexs deletedIndex = []; + final Map deletedRowByRowId = { + for (var rowId in rowIds) rowId: rowId + }; + + _rowInfos.asMap().forEach((index, RowInfo rowInfo) { + if (deletedRowByRowId[rowInfo.rowPB.id] == null) { + newRows.add(rowInfo); + } else { + _rowInfoByRowId.remove(rowInfo.rowPB.id); + deletedIndex.add(DeletedIndex(index: index, rowInfo: rowInfo)); + } + }); + _rowInfos = newRows; + return deletedIndex; + } + + UpdatedIndexMap updateRows( + List updatedRows, + RowInfo Function(RowPB) builder, + ) { + final UpdatedIndexMap updatedIndexs = UpdatedIndexMap(); + for (final RowPB updatedRow in updatedRows) { + final rowId = updatedRow.id; + final index = _rowInfos.indexWhere( + (rowInfo) => rowInfo.rowPB.id == rowId, + ); + if (index != -1) { + final rowInfo = builder(updatedRow); + insert(index, rowInfo); + updatedIndexs[rowId] = UpdatedIndex(index: index, rowId: rowId); + } + } + return updatedIndexs; + } + + List markRowsAsInvisible(List rowIds) { + final List deletedRows = []; + + for (final rowId in rowIds) { + final rowInfo = _rowInfoByRowId[rowId]; + if (rowInfo != null) { + final index = _rowInfos.indexOf(rowInfo); + if (index != -1) { + deletedRows.add(DeletedIndex(index: index, rowInfo: rowInfo)); + } + } + } + return deletedRows; + } + + bool contains(String rowId) { + return _rowInfoByRowId[rowId] != null; + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart index a4341517ac..e48c0dbb9e 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/setting/group_bloc.dart @@ -12,14 +12,14 @@ part 'group_bloc.freezed.dart'; class GridGroupBloc extends Bloc { final GridFieldController _fieldController; final SettingFFIService _settingFFIService; - Function(List)? _onFieldsFn; + Function(List)? _onFieldsFn; GridGroupBloc({ required String viewId, required GridFieldController fieldController, }) : _fieldController = fieldController, _settingFFIService = SettingFFIService(viewId: viewId), - super(GridGroupState.initial(viewId, fieldController.fieldContexts)) { + super(GridGroupState.initial(viewId, fieldController.fieldInfos)) { on( (event, emit) async { event.when( @@ -67,19 +67,19 @@ class GridGroupEvent with _$GridGroupEvent { String fieldId, FieldType fieldType, ) = _GroupByField; - const factory GridGroupEvent.didReceiveFieldUpdate( - List fields) = _DidReceiveFieldUpdate; + const factory GridGroupEvent.didReceiveFieldUpdate(List fields) = + _DidReceiveFieldUpdate; } @freezed class GridGroupState with _$GridGroupState { const factory GridGroupState({ required String gridId, - required List fieldContexts, + required List fieldContexts, }) = _GridGroupState; factory GridGroupState.initial( - String gridId, List fieldContexts) => + String gridId, List fieldContexts) => GridGroupState( gridId: gridId, fieldContexts: fieldContexts, diff --git a/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart index c54f16ae12..de0c0cca94 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/setting/property_bloc.dart @@ -10,13 +10,12 @@ part 'property_bloc.freezed.dart'; class GridPropertyBloc extends Bloc { final GridFieldController _fieldController; - Function(List)? _onFieldsFn; + Function(List)? _onFieldsFn; GridPropertyBloc( {required String gridId, required GridFieldController fieldController}) : _fieldController = fieldController, - super( - GridPropertyState.initial(gridId, fieldController.fieldContexts)) { + super(GridPropertyState.initial(gridId, fieldController.fieldInfos)) { on( (event, emit) async { await event.map( @@ -69,7 +68,7 @@ class GridPropertyEvent with _$GridPropertyEvent { const factory GridPropertyEvent.setFieldVisibility( String fieldId, bool visibility) = _SetFieldVisibility; const factory GridPropertyEvent.didReceiveFieldUpdate( - List fields) = _DidReceiveFieldUpdate; + List fields) = _DidReceiveFieldUpdate; const factory GridPropertyEvent.moveField(int fromIndex, int toIndex) = _MoveField; } @@ -78,12 +77,12 @@ class GridPropertyEvent with _$GridPropertyEvent { class GridPropertyState with _$GridPropertyState { const factory GridPropertyState({ required String gridId, - required List fieldContexts, + required List fieldContexts, }) = _GridPropertyState; factory GridPropertyState.initial( String gridId, - List fieldContexts, + List fieldContexts, ) => GridPropertyState( gridId: gridId, diff --git a/frontend/app_flowy/lib/plugins/grid/application/setting/setting_bloc.dart b/frontend/app_flowy/lib/plugins/grid/application/setting/setting_bloc.dart index bea2b87da6..e384c48319 100644 --- a/frontend/app_flowy/lib/plugins/grid/application/setting/setting_bloc.dart +++ b/frontend/app_flowy/lib/plugins/grid/application/setting/setting_bloc.dart @@ -25,7 +25,8 @@ class GridSettingBloc extends Bloc { @freezed class GridSettingEvent with _$GridSettingEvent { - const factory GridSettingEvent.performAction(GridSettingAction action) = _PerformAction; + const factory GridSettingEvent.performAction(GridSettingAction action) = + _PerformAction; } @freezed @@ -40,7 +41,7 @@ class GridSettingState with _$GridSettingState { } enum GridSettingAction { - filter, + showFilters, sortBy, - properties, + showProperties, } 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 2867fb4e2c..d11be8fa21 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/grid_page.dart @@ -1,7 +1,8 @@ import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; import 'package:app_flowy/plugins/grid/application/row/row_data_controller.dart'; -import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; @@ -15,6 +16,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/material.dart'; import 'package:linked_scroll_controller/linked_scroll_controller.dart'; import '../application/row/row_cache.dart'; +import '../application/setting/setting_bloc.dart'; import 'controller/grid_scroll.dart'; import 'layout/layout.dart'; import 'layout/sizes.dart'; @@ -24,17 +26,20 @@ import 'widgets/footer/grid_footer.dart'; import 'widgets/header/grid_header.dart'; import 'widgets/row/row_detail.dart'; import 'widgets/shortcuts.dart'; +import 'widgets/filter/menu.dart'; import 'widgets/toolbar/grid_toolbar.dart'; class GridPage extends StatefulWidget { final ViewPB view; + final GridController gridController; final VoidCallback? onDeleted; GridPage({ required this.view, this.onDeleted, Key? key, - }) : super(key: ValueKey(view.id)); + }) : gridController = GridController(view: view), + super(key: key); @override State createState() => _GridPageState(); @@ -46,8 +51,19 @@ class _GridPageState extends State { return MultiBlocProvider( providers: [ BlocProvider( - create: (context) => getIt(param1: widget.view) - ..add(const GridEvent.initial()), + create: (context) => GridBloc( + view: widget.view, + gridController: widget.gridController, + )..add(const GridEvent.initial()), + ), + BlocProvider( + create: (context) => GridFilterMenuBloc( + viewId: widget.view.id, + fieldController: widget.gridController.fieldController, + )..add(const GridFilterMenuEvent.initial()), + ), + BlocProvider( + create: (context) => GridSettingBloc(gridId: widget.view.id), ), ], child: BlocBuilder( @@ -122,7 +138,8 @@ class _FlowyGridState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const _GridToolbarAdaptor(), + const GridToolbar(), + const GridFilterMenu(), _gridHeader(context, state.gridId), Flexible(child: child), const RowCountBadge(), @@ -166,7 +183,7 @@ class _FlowyGridState extends State { Widget _gridHeader(BuildContext context, String gridId) { final fieldController = - context.read().dataController.fieldController; + context.read().gridController.fieldController; return GridHeaderSliverAdaptor( gridId: gridId, fieldController: fieldController, @@ -175,27 +192,6 @@ class _FlowyGridState extends State { } } -class _GridToolbarAdaptor extends StatelessWidget { - const _GridToolbarAdaptor({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return BlocSelector( - selector: (state) { - final fieldController = - context.read().dataController.fieldController; - return GridToolbarContext( - gridId: state.gridId, - fieldController: fieldController, - ); - }, - builder: (context, toolbarContext) { - return GridToolbar(toolbarContext: toolbarContext); - }, - ); - } -} - class _GridRows extends StatefulWidget { const _GridRows({Key? key}) : super(key: key); @@ -222,7 +218,7 @@ class _GridRowsState extends State<_GridRows> { _key.currentState?.removeItem( item.index, (context, animation) => - _renderRow(context, item.row, animation), + _renderRow(context, item.rowInfo, animation), ); } }, @@ -235,9 +231,13 @@ class _GridRowsState extends State<_GridRows> { initialItemCount: context.read().state.rowInfos.length, itemBuilder: (BuildContext context, int index, Animation animation) { - final RowInfo rowInfo = - context.read().state.rowInfos[index]; - return _renderRow(context, rowInfo, animation); + final rowInfos = context.read().state.rowInfos; + if (index >= rowInfos.length) { + return const SizedBox(); + } else { + final RowInfo rowInfo = rowInfos[index]; + return _renderRow(context, rowInfo, animation); + } }, ); }, @@ -258,7 +258,7 @@ class _GridRowsState extends State<_GridRows> { if (rowCache == null) return const SizedBox(); final fieldController = - context.read().dataController.fieldController; + context.read().gridController.fieldController; final dataController = GridRowDataController( rowInfo: rowInfo, fieldController: fieldController, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart b/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart index 72b1eb643d..67fa9ff709 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/layout/layout.dart @@ -2,7 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; import 'sizes.dart'; class GridLayout { - static double headerWidth(List fields) { + static double headerWidth(List fields) { if (fields.isEmpty) return 0; final fieldsWidth = fields 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 db4f7a9403..ec5e731337 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 @@ -61,7 +61,6 @@ class _SelectOptionCellEditorState extends State { SliverToBoxAdapter( child: _TextField(popoverMutex: popoverMutex), ), - const SliverToBoxAdapter(child: VSpace(6)), const SliverToBoxAdapter(child: TypeOptionSeparator()), const SliverToBoxAdapter(child: VSpace(6)), const SliverToBoxAdapter(child: _Title()), @@ -145,7 +144,7 @@ class _TextField extends StatelessWidget { value: (option) => option); return SizedBox( - height: 62, + height: 52, child: SelectOptionTextField( options: state.options, selectedOptionMap: optionMap, diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart index a67a197c8d..949586d756 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/cell/select_option_cell/text_field.dart @@ -49,6 +49,7 @@ class SelectOptionTextField extends StatefulWidget { class _SelectOptionTextFieldState extends State { late FocusNode focusNode; late TextEditingController controller; + var textLength = 0; @override void initState() { @@ -61,6 +62,14 @@ class _SelectOptionTextFieldState extends State { super.initState(); } + String? _suffixText() { + if (widget.maxLength != null) { + return '${textLength.toString()}/${widget.maxLength.toString()}'; + } else { + return null; + } + } + @override Widget build(BuildContext context) { return TextFieldTags( @@ -83,6 +92,7 @@ class _SelectOptionTextFieldState extends State { focusNode: focusNode, onTap: widget.onClick, onChanged: (text) { + textLength = text.length; if (onChanged != null) { onChanged(text); } @@ -114,6 +124,8 @@ class _SelectOptionTextFieldState extends State { isDense: true, prefixIcon: _renderTags(context, sc), hintText: LocaleKeys.grid_selectOption_searchOption.tr(), + suffixText: _suffixText(), + counterText: "", prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText), focusedBorder: OutlineInputBorder( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/checkbox.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/checkbox.dart new file mode 100644 index 0000000000..fa8fba31b1 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/checkbox.dart @@ -0,0 +1,15 @@ +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter/material.dart'; + +import 'choicechip.dart'; + +class CheckboxFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const CheckboxFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/choicechip.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/choicechip.dart new file mode 100644 index 0000000000..279390b2ca --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/choicechip.dart @@ -0,0 +1,55 @@ +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:flowy_infra/color_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +class ChoiceChipButton extends StatelessWidget { + final FilterInfo filterInfo; + final VoidCallback? onTap; + + const ChoiceChipButton({ + Key? key, + required this.filterInfo, + this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final arrow = Transform.rotate( + angle: -math.pi / 2, + child: svgWidget("home/arrow_left"), + ); + final borderSide = BorderSide( + color: AFThemeExtension.of(context).toggleOffFill, + width: 1.0, + ); + + final decoration = BoxDecoration( + color: Colors.transparent, + border: Border.fromBorderSide(borderSide), + borderRadius: const BorderRadius.all(Radius.circular(14)), + ); + + return SizedBox( + height: 28, + child: FlowyButton( + decoration: decoration, + useIntrinsicWidth: true, + text: FlowyText(filterInfo.field.name, fontSize: 12), + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + radius: const BorderRadius.all(Radius.circular(14)), + leftIcon: svgWidget( + filterInfo.field.fieldType.iconName(), + color: Theme.of(context).colorScheme.onSurface, + ), + rightIcon: arrow, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: onTap, + ), + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/date.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/date.dart new file mode 100644 index 0000000000..84cc03ef0f --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/date.dart @@ -0,0 +1,15 @@ +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter/material.dart'; + +import 'choicechip.dart'; + +class DateFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const DateFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/number.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/number.dart new file mode 100644 index 0000000000..41132ea95e --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/number.dart @@ -0,0 +1,15 @@ +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter/material.dart'; + +import 'choicechip.dart'; + +class NumberFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const NumberFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option.dart new file mode 100644 index 0000000000..0d3dca62c1 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/select_option.dart @@ -0,0 +1,15 @@ +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter/material.dart'; + +import 'choicechip.dart'; + +class SelectOptionFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const SelectOptionFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/text.dart new file mode 100644 index 0000000000..c80936e3d4 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/text.dart @@ -0,0 +1,212 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'package:app_flowy/plugins/grid/application/filter/text_filter_editor_bloc.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/condition_button.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/disclosure_button.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/text_field.dart'; +import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'choicechip.dart'; + +class TextFilterChoicechip extends StatefulWidget { + final FilterInfo filterInfo; + const TextFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + State createState() => _TextFilterChoicechipState(); +} + +class _TextFilterChoicechipState extends State { + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: PopoverController(), + constraints: BoxConstraints.loose(const Size(200, 76)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (BuildContext context) { + return TextFilterEditor(filterInfo: widget.filterInfo); + }, + child: ChoiceChipButton( + filterInfo: widget.filterInfo, + onTap: () {}, + ), + ); + } +} + +class TextFilterEditor extends StatefulWidget { + final FilterInfo filterInfo; + const TextFilterEditor({required this.filterInfo, Key? key}) + : super(key: key); + + @override + State createState() => _TextFilterEditorState(); +} + +class _TextFilterEditorState extends State { + final popoverMutex = PopoverMutex(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => TextFilterEditorBloc(filterInfo: widget.filterInfo) + ..add(const TextFilterEditorEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: Column( + children: [ + _buildFilterPannel(context, state), + const VSpace(4), + _buildFilterTextField(context, state), + ], + ), + ); + }, + ), + ); + } + + Widget _buildFilterPannel(BuildContext context, TextFilterEditorState state) { + return SizedBox( + height: 20, + child: Row( + children: [ + FlowyText(state.filterInfo.field.name), + const HSpace(4), + TextFilterConditionList( + filterInfo: state.filterInfo, + popoverMutex: popoverMutex, + onCondition: (condition) { + context + .read() + .add(TextFilterEditorEvent.updateCondition(condition)); + }, + ), + const Spacer(), + DisclosureButton( + popoverMutex: popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const TextFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + } + + Widget _buildFilterTextField( + BuildContext context, TextFilterEditorState state) { + final textFilter = state.filterInfo.textFilter()!; + return FilterTextField( + text: textFilter.content, + hintText: LocaleKeys.grid_settings_typeAValue.tr(), + autoFucous: false, + onSubmitted: (text) { + context + .read() + .add(TextFilterEditorEvent.updateContent(text)); + }, + ); + } +} + +class TextFilterConditionList extends StatelessWidget { + final FilterInfo filterInfo; + final PopoverMutex popoverMutex; + final Function(TextFilterCondition) onCondition; + const TextFilterConditionList({ + required this.filterInfo, + required this.popoverMutex, + required this.onCondition, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final textFilter = filterInfo.textFilter()!; + return PopoverActionList( + asBarrier: true, + mutex: popoverMutex, + direction: PopoverDirection.bottomWithCenterAligned, + actions: TextFilterCondition.values + .map( + (action) => ConditionWrapper( + action, + textFilter.condition == action, + ), + ) + .toList(), + buildChild: (controller) { + return ConditionButton( + conditionName: textFilter.condition.filterName, + onTap: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + onCondition(action.inner); + controller.close(); + }, + ); + } +} + +class ConditionWrapper extends ActionCell { + final TextFilterCondition inner; + final bool isSelected; + + ConditionWrapper(this.inner, this.isSelected); + + @override + Widget? rightIcon(Color iconColor) { + if (isSelected) { + return svgWidget("grid/checkmark"); + } else { + return null; + } + } + + @override + String get name => inner.filterName; +} + +extension TextFilterConditionExtension on TextFilterCondition { + String get filterName { + switch (this) { + case TextFilterCondition.Contains: + return LocaleKeys.grid_textFilter_contains.tr(); + case TextFilterCondition.DoesNotContain: + return LocaleKeys.grid_textFilter_doesNotContain.tr(); + case TextFilterCondition.EndsWith: + return LocaleKeys.grid_textFilter_endsWith.tr(); + case TextFilterCondition.Is: + return LocaleKeys.grid_textFilter_is.tr(); + case TextFilterCondition.IsNot: + return LocaleKeys.grid_textFilter_isNot.tr(); + case TextFilterCondition.StartsWith: + return LocaleKeys.grid_textFilter_startWith.tr(); + case TextFilterCondition.TextIsEmpty: + return LocaleKeys.grid_textFilter_isEmpty.tr(); + case TextFilterCondition.TextIsNotEmpty: + return LocaleKeys.grid_textFilter_isNotEmpty.tr(); + default: + return ""; + } + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/url.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/url.dart new file mode 100644 index 0000000000..93ce1ff2a4 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/choicechip/url.dart @@ -0,0 +1,14 @@ +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:flutter/material.dart'; +import 'choicechip.dart'; + +class URLFilterChoicechip extends StatelessWidget { + final FilterInfo filterInfo; + const URLFilterChoicechip({required this.filterInfo, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return ChoiceChipButton(filterInfo: filterInfo); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/condition_button.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/condition_button.dart new file mode 100644 index 0000000000..2a248cb48c --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/condition_button.dart @@ -0,0 +1,37 @@ +import 'dart:math' as math; +import 'package:flowy_infra/color_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; + +class ConditionButton extends StatelessWidget { + final String conditionName; + final VoidCallback onTap; + const ConditionButton({ + required this.conditionName, + required this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final arrow = Transform.rotate( + angle: -math.pi / 2, + child: svgWidget("home/arrow_left"), + ); + + return SizedBox( + height: 20, + child: FlowyButton( + useIntrinsicWidth: true, + text: FlowyText(conditionName, fontSize: 10), + margin: const EdgeInsets.symmetric(horizontal: 4), + radius: const BorderRadius.all(Radius.circular(2)), + rightIcon: arrow, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + onTap: onTap, + ), + ); + } +} 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 new file mode 100644 index 0000000000..eecda1676a --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/create_filter_list.dart @@ -0,0 +1,165 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_create_bloc.dart'; +import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/filter/text_field.dart'; +import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class GridCreateFilterList extends StatefulWidget { + final String viewId; + final GridFieldController fieldController; + final VoidCallback onClosed; + + const GridCreateFilterList({ + required this.viewId, + required this.fieldController, + required this.onClosed, + Key? key, + }) : super(key: key); + + @override + State createState() => _GridCreateFilterListState(); +} + +class _GridCreateFilterListState extends State { + late GridCreateFilterBloc editBloc; + + @override + void initState() { + editBloc = GridCreateFilterBloc( + viewId: widget.viewId, + fieldController: widget.fieldController, + )..add(const GridCreateFilterEvent.initial()); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: editBloc, + child: BlocListener( + listener: (context, state) { + if (state.didCreateFilter) { + widget.onClosed(); + } + }, + child: BlocBuilder( + builder: (context, state) { + final cells = state.creatableFields.map((fieldInfo) { + return SizedBox( + height: GridSize.typeOptionItemHeight, + child: _FilterPropertyCell( + fieldInfo: fieldInfo, + onTap: (fieldInfo) => createFilter(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 createFilter(FieldInfo field) { + editBloc.add(GridCreateFilterEvent.createDefaultFilter(field)); + } +} + +class _FilterTextFieldDelegate extends SliverPersistentHeaderDelegate { + _FilterTextFieldDelegate(); + + double fixHeight = 46; + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return Padding( + padding: const EdgeInsets.only(top: 4), + child: Container( + color: Theme.of(context).colorScheme.background, + height: fixHeight, + child: FilterTextField( + hintText: LocaleKeys.grid_settings_filterBy.tr(), + onChanged: (text) { + context + .read() + .add(GridCreateFilterEvent.didReceiveFilterText(text)); + }, + ), + ), + ); + } + + @override + double get maxExtent => fixHeight; + + @override + double get minExtent => fixHeight; + + @override + bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { + return false; + } +} + +class _FilterPropertyCell extends StatelessWidget { + final FieldInfo fieldInfo; + final Function(FieldInfo) onTap; + const _FilterPropertyCell({ + required this.fieldInfo, + required this.onTap, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return FlowyButton( + text: FlowyText.medium(fieldInfo.name, fontSize: 12), + onTap: () => onTap(fieldInfo), + leftIcon: svgWidget( + fieldInfo.fieldType.iconName(), + color: Theme.of(context).colorScheme.onSurface, + ), + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/disclosure_button.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/disclosure_button.dart new file mode 100644 index 0000000000..9e6b94f8b8 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/disclosure_button.dart @@ -0,0 +1,73 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/style_widget/icon_button.dart'; +import 'package:flutter/material.dart'; + +class DisclosureButton extends StatefulWidget { + final PopoverMutex popoverMutex; + final Function(FilterDisclosureAction) onAction; + const DisclosureButton({ + required this.popoverMutex, + required this.onAction, + Key? key, + }) : super(key: key); + + @override + State createState() => _DisclosureButtonState(); +} + +class _DisclosureButtonState extends State { + @override + Widget build(BuildContext context) { + return PopoverActionList( + asBarrier: true, + mutex: widget.popoverMutex, + direction: PopoverDirection.rightWithTopAligned, + actions: FilterDisclosureAction.values + .map((action) => FilterDisclosureActionWrapper(action)) + .toList(), + buildChild: (controller) { + return FlowyIconButton( + width: 20, + icon: svgWidget( + "editor/details", + color: Theme.of(context).colorScheme.onSurface, + ), + onPressed: () => controller.show(), + ); + }, + onSelected: (action, controller) async { + widget.onAction(action.inner); + controller.close(); + }, + ); + } +} + +enum FilterDisclosureAction { + delete, +} + +class FilterDisclosureActionWrapper extends ActionCell { + final FilterDisclosureAction inner; + + FilterDisclosureActionWrapper(this.inner); + + @override + Widget? leftIcon(Color iconColor) => null; + + @override + String get name => inner.name; +} + +extension FilterDisclosureActionExtension on FilterDisclosureAction { + String get name { + switch (this) { + case FilterDisclosureAction.delete: + return LocaleKeys.grid_settings_deleteFilter.tr(); + } + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_info.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_info.dart new file mode 100644 index 0000000000..c1f11bf998 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/filter_info.dart @@ -0,0 +1,35 @@ +import 'package:app_flowy/plugins/grid/application/field/field_controller.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart'; + +class FilterInfo { + final String viewId; + final FilterPB filter; + final FieldInfo field; + + FilterInfo(this.viewId, this.filter, this.field); + + FilterInfo copyWith({FilterPB? filter, FieldInfo? field}) { + return FilterInfo( + viewId, + filter ?? this.filter, + field ?? this.field, + ); + } + + DateFilterPB? dateFilter() { + if (filter.fieldType != FieldType.DateTime) { + return null; + } + return DateFilterPB.fromBuffer(filter.data); + } + + TextFilterPB? textFilter() { + if (filter.fieldType != FieldType.RichText) { + return null; + } + return TextFilterPB.fromBuffer(filter.data); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart new file mode 100644 index 0000000000..7b642dddee --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu.dart @@ -0,0 +1,138 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart'; +import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/color_extension.dart'; +import 'package:flowy_infra/image.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'create_filter_list.dart'; +import 'filter_info.dart'; +import 'menu_item.dart'; + +class GridFilterMenu extends StatelessWidget { + const GridFilterMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state.isVisible) { + return _wrapPadding(Column( + children: [ + buildDivider(context), + const VSpace(6), + buildFilterItems(state.viewId, state.filters), + ], + )); + } else { + return const SizedBox(); + } + }, + ); + } + + Widget _wrapPadding(Widget child) { + return Padding( + padding: EdgeInsets.symmetric( + horizontal: GridSize.leadingHeaderPadding, + vertical: 6, + ), + child: child, + ); + } + + Widget buildDivider(BuildContext context) { + return Divider( + height: 1.0, + color: AFThemeExtension.of(context).toggleOffFill, + ); + } + + Widget buildFilterItems(String viewId, List filters) { + final List children = filters + .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo)) + .toList(); + return Row( + children: [ + SingleChildScrollView( + controller: ScrollController(), + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 4, + children: children, + ), + ), + const HSpace(4), + AddFilterButton(viewId: viewId), + ], + ); + } +} + +class AddFilterButton extends StatefulWidget { + final String viewId; + const AddFilterButton({required this.viewId, Key? key}) : super(key: key); + + @override + State createState() => _AddFilterButtonState(); +} + +class _AddFilterButtonState extends State { + late PopoverController popoverController; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return wrapPopover( + context, + SizedBox( + height: 28, + child: FlowyButton( + text: FlowyText( + LocaleKeys.grid_settings_addFilter.tr(), + fontSize: 12, + ), + useIntrinsicWidth: true, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + leftIcon: svgWidget( + "home/add", + color: Theme.of(context).colorScheme.onSurface, + ), + onTap: () { + popoverController.show(); + }, + ), + ), + ); + } + + Widget wrapPopover(BuildContext buildContext, Widget child) { + return AppFlowyPopover( + controller: popoverController, + constraints: BoxConstraints.loose(const Size(200, 300)), + margin: const EdgeInsets.all(6), + triggerActions: PopoverTriggerFlags.none, + child: child, + popupBuilder: (BuildContext context) { + final bloc = buildContext.read(); + return GridCreateFilterList( + viewId: widget.viewId, + fieldController: bloc.fieldController, + onClosed: () => popoverController.close(), + ); + }, + ); + } +} 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/menu_item.dart new file mode 100644 index 0000000000..ff311ad8b2 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/menu_item.dart @@ -0,0 +1,41 @@ +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pbenum.dart'; +import 'package:flutter/material.dart'; + +import 'choicechip/checkbox.dart'; +import 'choicechip/date.dart'; +import 'choicechip/number.dart'; +import 'choicechip/select_option.dart'; +import 'choicechip/text.dart'; +import 'choicechip/url.dart'; +import 'filter_info.dart'; + +class FilterMenuItem extends StatelessWidget { + final FilterInfo filterInfo; + const FilterMenuItem({required this.filterInfo, Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return buildFilterChoicechip(filterInfo); + } +} + +Widget buildFilterChoicechip(FilterInfo filterInfo) { + switch (filterInfo.field.fieldType) { + case FieldType.Checkbox: + return CheckboxFilterChoicechip(filterInfo: filterInfo); + case FieldType.DateTime: + return DateFilterChoicechip(filterInfo: filterInfo); + case FieldType.MultiSelect: + return SelectOptionFilterChoicechip(filterInfo: filterInfo); + case FieldType.Number: + return NumberFilterChoicechip(filterInfo: filterInfo); + case FieldType.RichText: + return TextFilterChoicechip(filterInfo: filterInfo); + case FieldType.SingleSelect: + return SelectOptionFilterChoicechip(filterInfo: filterInfo); + case FieldType.URL: + return URLFilterChoicechip(filterInfo: filterInfo); + default: + return const SizedBox(); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/text_field.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/text_field.dart new file mode 100644 index 0000000000..09bc31fba2 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/filter/text_field.dart @@ -0,0 +1,76 @@ +import 'package:flowy_infra/size.dart'; +import 'package:flowy_infra/text_style.dart'; +import 'package:flutter/material.dart'; +import 'package:textstyle_extensions/textstyle_extensions.dart'; + +class FilterTextField extends StatefulWidget { + final String hintText; + final String text; + final void Function(String)? onChanged; + final void Function(String)? onSubmitted; + final bool autoFucous; + const FilterTextField({ + this.hintText = "", + this.text = "", + this.onChanged, + this.onSubmitted, + this.autoFucous = true, + Key? key, + }) : super(key: key); + + @override + State createState() => FilterTextFieldState(); +} + +class FilterTextFieldState extends State { + late FocusNode focusNode; + late TextEditingController controller; + + @override + void initState() { + focusNode = FocusNode(); + controller = TextEditingController(); + controller.text = widget.text; + if (widget.autoFucous) { + WidgetsBinding.instance.addPostFrameCallback((_) { + focusNode.requestFocus(); + }); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + focusNode: focusNode, + onChanged: (text) { + widget.onChanged?.call(text); + }, + onSubmitted: (text) { + widget.onSubmitted?.call(text); + }, + maxLines: 1, + style: TextStyles.body1.size(FontSizes.s12), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(10), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 1.0, + ), + borderRadius: Corners.s10Border, + ), + isDense: true, + hintText: widget.hintText, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 1.0, + ), + borderRadius: Corners.s8Border, + ), + ), + ); + } +} diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart index 40bc009663..09d72aaba0 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/field_cell.dart @@ -45,7 +45,7 @@ class _GridFieldCellState extends State { builder: (context, state) { final button = AppFlowyPopover( triggerActions: PopoverTriggerFlags.none, - constraints: BoxConstraints.loose(const Size(240, 840)), + constraints: BoxConstraints.loose(const Size(240, 440)), direction: PopoverDirection.bottomWithLeftAligned, controller: popoverController, popupBuilder: (BuildContext context) { @@ -172,6 +172,7 @@ class FieldCellButton extends StatelessWidget { field.fieldType.iconName(), color: Theme.of(context).colorScheme.onSurface, ), + radius: BorderRadius.zero, text: FlowyText.medium( text, maxLines: maxLines, 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 8fb81f5395..88b16b1ac6 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 @@ -88,9 +88,9 @@ class _EditFieldButton extends StatelessWidget { } class _FieldOperationList extends StatelessWidget { - final GridFieldCellContext fieldContext; + final GridFieldCellContext fieldInfo; final VoidCallback onDismissed; - const _FieldOperationList(this.fieldContext, this.onDismissed, {Key? key}) + const _FieldOperationList(this.fieldInfo, this.onDismissed, {Key? key}) : super(key: key); @override @@ -113,14 +113,14 @@ class _FieldOperationList extends StatelessWidget { bool enable = true; switch (action) { case FieldAction.delete: - enable = !fieldContext.field.isPrimary; + enable = !fieldInfo.field.isPrimary; break; default: break; } return FieldActionCell( - fieldContext: fieldContext, + fieldInfo: fieldInfo, action: action, onTap: onDismissed, enable: enable, @@ -131,13 +131,13 @@ class _FieldOperationList extends StatelessWidget { } class FieldActionCell extends StatelessWidget { - final GridFieldCellContext fieldContext; + final GridFieldCellContext fieldInfo; final VoidCallback onTap; final FieldAction action; final bool enable; const FieldActionCell({ - required this.fieldContext, + required this.fieldInfo, required this.action, required this.onTap, required this.enable, @@ -153,7 +153,7 @@ class FieldActionCell extends StatelessWidget { ), onTap: () { if (enable) { - action.run(context, fieldContext); + action.run(context, fieldInfo); onTap(); } }, @@ -196,7 +196,7 @@ extension _FieldActionExtension on FieldAction { } } - void run(BuildContext context, GridFieldCellContext fieldContext) { + void run(BuildContext context, GridFieldCellContext fieldInfo) { switch (this) { case FieldAction.hide: context @@ -207,8 +207,8 @@ extension _FieldActionExtension on FieldAction { PopoverContainer.of(context).close(); FieldService( - gridId: fieldContext.gridId, - fieldId: fieldContext.field.id, + gridId: fieldInfo.gridId, + fieldId: fieldInfo.field.id, ).duplicateField(); break; @@ -219,8 +219,8 @@ extension _FieldActionExtension on FieldAction { title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), confirm: () { FieldService( - gridId: fieldContext.gridId, - fieldId: fieldContext.field.id, + gridId: fieldInfo.gridId, + fieldId: fieldInfo.field.id, ).deleteField(); }, ).show(context); 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 22655bc42e..87d85ccfc7 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 @@ -115,7 +115,7 @@ class _FieldTypeOptionCell extends StatelessWidget { builder: (context, state) { return state.field.fold( () => const SizedBox(), - (fieldContext) { + (fieldInfo) { final dataController = context.read().dataController; return FieldTypeOptionEditor( 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 65c5e1eae1..8476a41e6f 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 @@ -87,7 +87,7 @@ class _SwitchFieldButton extends StatelessWidget { final widget = AppFlowyPopover( constraints: BoxConstraints.loose(const Size(460, 540)), asBarrier: true, - triggerActions: PopoverTriggerFlags.click | PopoverTriggerFlags.hover, + triggerActions: PopoverTriggerFlags.click, mutex: popoverMutex, offset: const Offset(20, 0), popupBuilder: (popOverContext) { diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart index e6d47a06c0..75bf8149db 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/header/type_option/builder.dart @@ -11,7 +11,7 @@ import 'package:flowy_sdk/protobuf/flowy-grid/number_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/single_select_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/text_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/url_type_option.pb.dart'; -import 'package:protobuf/protobuf.dart'; +import 'package:protobuf/protobuf.dart' hide FieldInfo; import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; import 'package:flutter/material.dart'; import 'checkbox.dart'; @@ -130,18 +130,17 @@ TypeOptionWidgetBuilder makeTypeOptionWidgetBuilder({ TypeOptionContext makeTypeOptionContext({ required String gridId, - required GridFieldContext fieldContext, + required FieldInfo fieldInfo, }) { - final loader = - FieldTypeOptionLoader(gridId: gridId, field: fieldContext.field); + final loader = FieldTypeOptionLoader(gridId: gridId, field: fieldInfo.field); final dataController = TypeOptionDataController( gridId: gridId, loader: loader, - fieldContext: fieldContext, + fieldInfo: fieldInfo, ); return makeTypeOptionContextWithDataController( gridId: gridId, - fieldType: fieldContext.fieldType, + fieldType: fieldInfo.fieldType, dataController: dataController, ); } diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart index 7f489218e0..befb275a72 100755 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/grid_row.dart @@ -224,13 +224,13 @@ class RowContent extends StatelessWidget { final GridCellWidget child = builder.build(cellId); return CellContainer( - width: cellId.fieldContext.width.toDouble(), + width: cellId.fieldInfo.width.toDouble(), rowStateNotifier: Provider.of(context, listen: false), accessoryBuilder: (buildContext) { final builder = child.accessoryBuilder; List accessories = []; - if (cellId.fieldContext.isPrimary) { + if (cellId.fieldInfo.isPrimary) { accessories.add( GridCellAccessoryBuilder( builder: (key) => PrimaryCellAccessory( diff --git a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart index 22066163f9..df88ba45fe 100644 --- a/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/row/row_detail.dart @@ -281,7 +281,7 @@ class _RowDetailCellState extends State<_RowDetailCell> { width: 150, child: FieldCellButton( maxLines: null, - field: widget.cellId.fieldContext.field, + field: widget.cellId.fieldInfo.field, onTap: () => popover.show(), ), ), @@ -297,11 +297,11 @@ class _RowDetailCellState extends State<_RowDetailCell> { Widget buildFieldEditor() { return FieldEditor( gridId: widget.cellId.gridId, - fieldName: widget.cellId.fieldContext.field.name, - isGroupField: widget.cellId.fieldContext.isGroupField, + fieldName: widget.cellId.fieldInfo.field.name, + isGroupField: widget.cellId.fieldInfo.isGroupField, typeOptionLoader: FieldTypeOptionLoader( gridId: widget.cellId.gridId, - field: widget.cellId.fieldContext.field, + field: widget.cellId.fieldInfo.field, ), onDeleted: (fieldId) { popover.close(); 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 new file mode 100644 index 0000000000..2c288cb824 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/filter_button.dart @@ -0,0 +1,76 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/color_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../filter/create_filter_list.dart'; + +class FilterButton extends StatefulWidget { + const FilterButton({Key? key}) : super(key: key); + + @override + State createState() => _FilterButtonState(); +} + +class _FilterButtonState extends State { + final _popoverController = PopoverController(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final textColor = state.filters.isEmpty + ? null + : Theme.of(context).colorScheme.primary; + + return wrapPopover( + context, + SizedBox( + height: 26, + child: FlowyTextButton( + LocaleKeys.grid_settings_filter.tr(), + fontSize: 14, + fontColor: textColor, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 2), + onPressed: () { + final bloc = context.read(); + if (bloc.state.filters.isEmpty) { + _popoverController.show(); + } else { + bloc.add(const GridFilterMenuEvent.toggleMenu()); + } + }, + ), + ), + ); + }, + ); + } + + Widget wrapPopover(BuildContext buildContext, Widget child) { + return AppFlowyPopover( + controller: _popoverController, + direction: PopoverDirection.leftWithTopAligned, + constraints: BoxConstraints.loose(const Size(200, 300)), + offset: const Offset(0, 10), + margin: const EdgeInsets.all(6), + triggerActions: PopoverTriggerFlags.none, + child: child, + popupBuilder: (BuildContext context) { + final bloc = buildContext.read(); + return GridCreateFilterList( + viewId: bloc.viewId, + fieldController: bloc.fieldController, + onClosed: () => _popoverController.close(), + ); + }, + ); + } +} 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 bc31e820e0..472e9fa05c 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 @@ -30,14 +30,14 @@ class GridGroupList extends StatelessWidget { )..add(const GridGroupEvent.initial()), child: BlocBuilder( builder: (context, state) { - final cells = state.fieldContexts.map((fieldContext) { + final cells = state.fieldContexts.map((fieldInfo) { Widget cell = _GridGroupCell( - fieldContext: fieldContext, + fieldInfo: fieldInfo, onSelected: () => onDismissed(), - key: ValueKey(fieldContext.id), + key: ValueKey(fieldInfo.id), ); - if (!fieldContext.canGroup) { + if (!fieldInfo.canGroup) { cell = IgnorePointer(child: Opacity(opacity: 0.3, child: cell)); } return cell; @@ -61,9 +61,9 @@ class GridGroupList extends StatelessWidget { class _GridGroupCell extends StatelessWidget { final VoidCallback onSelected; - final GridFieldContext fieldContext; + final FieldInfo fieldInfo; const _GridGroupCell({ - required this.fieldContext, + required this.fieldInfo, required this.onSelected, Key? key, }) : super(key: key); @@ -71,7 +71,7 @@ class _GridGroupCell extends StatelessWidget { @override Widget build(BuildContext context) { Widget? rightIcon; - if (fieldContext.isGroupField) { + if (fieldInfo.isGroupField) { rightIcon = Padding( padding: const EdgeInsets.all(2.0), child: svgWidget("grid/checkmark"), @@ -81,17 +81,17 @@ class _GridGroupCell extends StatelessWidget { return SizedBox( height: GridSize.typeOptionItemHeight, child: FlowyButton( - text: FlowyText.medium(fieldContext.name), + text: FlowyText.medium(fieldInfo.name), leftIcon: svgWidget( - fieldContext.fieldType.iconName(), + fieldInfo.fieldType.iconName(), color: Theme.of(context).colorScheme.onSurface, ), rightIcon: rightIcon, onTap: () { context.read().add( GridGroupEvent.setGroupByField( - fieldContext.id, - fieldContext.fieldType, + fieldInfo.id, + fieldInfo.fieldType, ), ); onSelected(); 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 2e8113d016..01d5c20dcf 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 @@ -51,7 +51,7 @@ class _GridPropertyListState extends State { return _GridPropertyCell( popoverMutex: _popoverMutex, gridId: widget.gridId, - fieldContext: field, + fieldInfo: field, key: ValueKey(field.id), ); }).toList(); @@ -74,12 +74,12 @@ class _GridPropertyListState extends State { } class _GridPropertyCell extends StatelessWidget { - final GridFieldContext fieldContext; + final FieldInfo fieldInfo; final String gridId; final PopoverMutex popoverMutex; const _GridPropertyCell({ required this.gridId, - required this.fieldContext, + required this.fieldInfo, required this.popoverMutex, Key? key, }) : super(key: key); @@ -87,7 +87,7 @@ class _GridPropertyCell extends StatelessWidget { @override Widget build(BuildContext context) { final checkmark = svgWidget( - fieldContext.visibility ? 'home/show' : 'home/hide', + fieldInfo.visibility ? 'home/show' : 'home/hide', color: Theme.of(context).colorScheme.onSurface, ); @@ -104,7 +104,7 @@ class _GridPropertyCell extends StatelessWidget { onPressed: () { context.read().add( GridPropertyEvent.setFieldVisibility( - fieldContext.id, !fieldContext.visibility)); + fieldInfo.id, !fieldInfo.visibility)); }, icon: checkmark.padding(all: 6), ) @@ -116,21 +116,22 @@ class _GridPropertyCell extends StatelessWidget { return AppFlowyPopover( mutex: popoverMutex, offset: const Offset(20, 0), + direction: PopoverDirection.leftWithTopAligned, constraints: BoxConstraints.loose(const Size(240, 400)), child: FlowyButton( - text: FlowyText.medium(fieldContext.name), + text: FlowyText.medium(fieldInfo.name), leftIcon: svgWidget( - fieldContext.fieldType.iconName(), + fieldInfo.fieldType.iconName(), color: Theme.of(context).colorScheme.onSurface, ), ), popupBuilder: (BuildContext context) { return FieldEditor( gridId: gridId, - fieldName: fieldContext.name, + fieldName: fieldInfo.name, typeOptionLoader: FieldTypeOptionLoader( gridId: gridId, - field: fieldContext.field, + field: fieldInfo.field, ), ); }, 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 1235417f52..08f7dd552e 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 @@ -6,7 +6,6 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:app_flowy/generated/locale_keys.g.dart'; import '../../../application/field/field_controller.dart'; @@ -31,33 +30,11 @@ class GridSettingList extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => GridSettingBloc(gridId: settingContext.gridId), - child: BlocListener( - listenWhen: (previous, current) => - previous.selectedAction != current.selectedAction, - listener: (context, state) { - state.selectedAction.foldLeft(null, (_, action) { - onAction(action, settingContext); - }); - }, - child: BlocBuilder( - builder: (context, state) { - return _renderList(); - }, - ), - ), - ); - } - - String identifier() { - return toString(); - } - - Widget _renderList() { final cells = GridSettingAction.values.where((value) => value.enable()).map((action) { - return _SettingItem(action: action); + return _SettingItem( + action: action, + onAction: (action) => onAction(action, settingContext)); }).toList(); return SizedBox( @@ -80,33 +57,24 @@ class GridSettingList extends StatelessWidget { class _SettingItem extends StatelessWidget { final GridSettingAction action; + final Function(GridSettingAction) onAction; const _SettingItem({ required this.action, + required this.onAction, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { - final isSelected = context - .read() - .state - .selectedAction - .foldLeft(false, (_, selectedAction) => selectedAction == action); - return SizedBox( height: GridSize.typeOptionItemHeight, child: FlowyButton( - isSelected: isSelected, text: FlowyText.medium( action.title(), color: action.enable() ? null : Theme.of(context).disabledColor, ), - onTap: () { - context - .read() - .add(GridSettingEvent.performAction(action)); - }, + onTap: () => onAction(action), leftIcon: svgWidget( action.iconName(), color: Theme.of(context).colorScheme.onSurface, @@ -119,29 +87,29 @@ class _SettingItem extends StatelessWidget { extension _GridSettingExtension on GridSettingAction { String iconName() { switch (this) { - case GridSettingAction.filter: + case GridSettingAction.showFilters: return 'grid/setting/filter'; case GridSettingAction.sortBy: return 'grid/setting/sort'; - case GridSettingAction.properties: + case GridSettingAction.showProperties: return 'grid/setting/properties'; } } String title() { switch (this) { - case GridSettingAction.filter: + case GridSettingAction.showFilters: return LocaleKeys.grid_settings_filter.tr(); case GridSettingAction.sortBy: return LocaleKeys.grid_settings_sortBy.tr(); - case GridSettingAction.properties: + case GridSettingAction.showProperties: return LocaleKeys.grid_settings_Properties.tr(); } } bool enable() { switch (this) { - case GridSettingAction.properties: + case GridSettingAction.showProperties: return true; default: return false; 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 12f5f0545a..7d65c86b8a 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,14 +1,9 @@ -import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart'; -import 'package:flowy_infra/image.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/extension.dart'; -import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flutter/material.dart'; import '../../../application/field/field_controller.dart'; import '../../layout/sizes.dart'; -import 'grid_property.dart'; -import 'grid_setting.dart'; +import 'filter_button.dart'; +import 'setting_button.dart'; class GridToolbarContext { final String gridId; @@ -20,82 +15,21 @@ class GridToolbarContext { } class GridToolbar extends StatelessWidget { - final GridToolbarContext toolbarContext; - const GridToolbar({required this.toolbarContext, Key? key}) : super(key: key); + const GridToolbar({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final settingContext = GridSettingContext( - gridId: toolbarContext.gridId, - fieldController: toolbarContext.fieldController, - ); return SizedBox( height: 40, child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(width: GridSize.leadingHeaderPadding), - _SettingButton(settingContext: settingContext), const Spacer(), + const FilterButton(), + const SettingButton(), ], ), ); } } - -class _SettingButton extends StatelessWidget { - final GridSettingContext settingContext; - const _SettingButton({required this.settingContext, Key? key}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return AppFlowyPopover( - constraints: BoxConstraints.loose(const Size(260, 400)), - offset: const Offset(0, 10), - margin: const EdgeInsets.all(6), - child: FlowyIconButton( - width: 22, - icon: svgWidget( - "grid/setting/setting", - color: Theme.of(context).colorScheme.onSurface, - ).padding(horizontal: 3, vertical: 3), - ), - popupBuilder: (BuildContext context) { - return _GridSettingListPopover(settingContext: settingContext); - }, - ); - } -} - -class _GridSettingListPopover extends StatefulWidget { - final GridSettingContext settingContext; - - const _GridSettingListPopover({Key? key, required this.settingContext}) - : super(key: key); - - @override - State createState() => _GridSettingListPopoverState(); -} - -class _GridSettingListPopoverState extends State<_GridSettingListPopover> { - GridSettingAction? _action; - - @override - Widget build(BuildContext context) { - if (_action == GridSettingAction.properties) { - return GridPropertyList( - gridId: widget.settingContext.gridId, - fieldController: widget.settingContext.fieldController, - ); - } - - return GridSettingList( - settingContext: widget.settingContext, - onAction: (action, settingContext) { - setState(() { - _action = action; - }); - }, - ); - } -} 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 new file mode 100644 index 0000000000..f3003a3f32 --- /dev/null +++ b/frontend/app_flowy/lib/plugins/grid/presentation/widgets/toolbar/setting_button.dart @@ -0,0 +1,100 @@ +import 'package:app_flowy/generated/locale_keys.g.dart'; +import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/setting/setting_bloc.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/color_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'grid_property.dart'; +import 'grid_setting.dart'; + +class SettingButton extends StatefulWidget { + const SettingButton({Key? key}) : super(key: key); + + @override + State createState() => _SettingButtonState(); +} + +class _SettingButtonState extends State { + late PopoverController popoverController; + + @override + void initState() { + popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return BlocSelector( + selector: (state) { + final fieldController = + context.read().gridController.fieldController; + return GridSettingContext( + gridId: state.gridId, + fieldController: fieldController, + ); + }, + builder: (context, settingContext) { + return AppFlowyPopover( + controller: popoverController, + constraints: BoxConstraints.loose(const Size(260, 400)), + direction: PopoverDirection.leftWithTopAligned, + offset: const Offset(0, 10), + margin: const EdgeInsets.all(6), + triggerActions: PopoverTriggerFlags.none, + child: FlowyTextButton( + LocaleKeys.settings_title.tr(), + fontSize: 14, + fillColor: Colors.transparent, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), + onPressed: () { + popoverController.show(); + }, + ), + popupBuilder: (BuildContext context) { + return _GridSettingListPopover(settingContext: settingContext); + }, + ); + }, + ); + } +} + +class _GridSettingListPopover extends StatefulWidget { + final GridSettingContext settingContext; + + const _GridSettingListPopover({Key? key, required this.settingContext}) + : super(key: key); + + @override + State createState() => _GridSettingListPopoverState(); +} + +class _GridSettingListPopoverState extends State<_GridSettingListPopover> { + GridSettingAction? _action; + + @override + Widget build(BuildContext context) { + if (_action == GridSettingAction.showProperties) { + return GridPropertyList( + gridId: widget.settingContext.gridId, + fieldController: widget.settingContext.fieldController, + ); + } + + return GridSettingList( + settingContext: widget.settingContext, + onAction: (action, settingContext) { + setState(() { + _action = action; + }); + }, + ); + } +} diff --git a/frontend/app_flowy/lib/startup/deps_resolver.dart b/frontend/app_flowy/lib/startup/deps_resolver.dart index 9ea40681e0..3b8ad4b418 100644 --- a/frontend/app_flowy/lib/startup/deps_resolver.dart +++ b/frontend/app_flowy/lib/startup/deps_resolver.dart @@ -127,11 +127,6 @@ void _resolveDocDeps(GetIt getIt) { } void _resolveGridDeps(GetIt getIt) { - // GridPB - getIt.registerFactoryParam( - (view, _) => GridBloc(view: view), - ); - getIt.registerFactoryParam( (gridId, fieldController) => GridHeaderBloc( gridId: gridId, diff --git a/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart b/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart index 5c3455454a..5e8120cb8a 100644 --- a/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart +++ b/frontend/app_flowy/lib/workspace/application/home/home_bloc.dart @@ -1,5 +1,4 @@ import 'package:app_flowy/user/application/user_listener.dart'; -import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:flowy_sdk/log.dart'; import 'package:flowy_sdk/protobuf/flowy-error-code/code.pb.dart'; @@ -38,40 +37,12 @@ class HomeBloc extends Bloc { showLoading: (e) async { emit(state.copyWith(isLoading: e.isLoading)); }, - setEditPanel: (e) async { - emit(state.copyWith(panelContext: some(e.editContext))); - }, - dismissEditPanel: (value) async { - emit(state.copyWith(panelContext: none())); - }, - forceCollapse: (e) async { - emit(state.copyWith(forceCollapse: e.forceCollapse)); - }, didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { emit(state.copyWith(workspaceSetting: value.setting)); }, unauthorized: (_Unauthorized value) { emit(state.copyWith(unauthorized: true)); }, - collapseMenu: (_CollapseMenu e) { - emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed)); - }, - editPanelResizeStart: (_EditPanelResizeStart e) { - emit(state.copyWith( - resizeType: MenuResizeType.drag, - resizeStart: state.resizeOffset, - )); - }, - editPanelResized: (_EditPanelResized e) { - final newPosition = - (e.offset + state.resizeStart).clamp(-50, 200).toDouble(); - if (state.resizeOffset != newPosition) { - emit(state.copyWith(resizeOffset: newPosition)); - } - }, - editPanelResizeEnd: (_EditPanelResizeEnd e) { - emit(state.copyWith(resizeType: MenuResizeType.slide)); - }, ); }, ); @@ -112,42 +83,22 @@ extension MenuResizeTypeExtension on MenuResizeType { class HomeEvent with _$HomeEvent { const factory HomeEvent.initial() = _Initial; const factory HomeEvent.showLoading(bool isLoading) = _ShowLoading; - const factory HomeEvent.forceCollapse(bool forceCollapse) = _ForceCollapse; - const factory HomeEvent.setEditPanel(EditPanelContext editContext) = - _ShowEditPanel; - const factory HomeEvent.dismissEditPanel() = _DismissEditPanel; const factory HomeEvent.didReceiveWorkspaceSetting( WorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting; const factory HomeEvent.unauthorized(String msg) = _Unauthorized; - const factory HomeEvent.collapseMenu() = _CollapseMenu; - const factory HomeEvent.editPanelResized(double offset) = _EditPanelResized; - const factory HomeEvent.editPanelResizeStart() = _EditPanelResizeStart; - const factory HomeEvent.editPanelResizeEnd() = _EditPanelResizeEnd; } @freezed class HomeState with _$HomeState { const factory HomeState({ required bool isLoading, - required bool forceCollapse, - required Option panelContext, required WorkspaceSettingPB workspaceSetting, required bool unauthorized, - required bool isMenuCollapsed, - required double resizeOffset, - required double resizeStart, - required MenuResizeType resizeType, }) = _HomeState; factory HomeState.initial(WorkspaceSettingPB workspaceSetting) => HomeState( isLoading: false, - forceCollapse: false, - panelContext: none(), workspaceSetting: workspaceSetting, unauthorized: false, - isMenuCollapsed: false, - resizeOffset: 0, - resizeStart: 0, - resizeType: MenuResizeType.slide, ); } diff --git a/frontend/app_flowy/lib/workspace/application/home/home_setting_bloc.dart b/frontend/app_flowy/lib/workspace/application/home/home_setting_bloc.dart new file mode 100644 index 0000000000..70706a823c --- /dev/null +++ b/frontend/app_flowy/lib/workspace/application/home/home_setting_bloc.dart @@ -0,0 +1,124 @@ +import 'package:app_flowy/user/application/user_listener.dart'; +import 'package:app_flowy/workspace/application/edit_panel/edit_context.dart'; +import 'package:flowy_infra/time/duration.dart'; +import 'package:flowy_sdk/protobuf/flowy-folder/workspace.pb.dart' + show WorkspaceSettingPB; +import 'package:flowy_sdk/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:dartz/dartz.dart'; +part 'home_setting_bloc.freezed.dart'; + +class HomeSettingBloc extends Bloc { + final UserWorkspaceListener _listener; + + HomeSettingBloc( + UserProfilePB user, + WorkspaceSettingPB workspaceSetting, + ) : _listener = UserWorkspaceListener(userProfile: user), + super(HomeSettingState.initial(workspaceSetting)) { + on( + (event, emit) async { + await event.map( + initial: (_Initial value) {}, + setEditPanel: (e) async { + emit(state.copyWith(panelContext: some(e.editContext))); + }, + dismissEditPanel: (value) async { + emit(state.copyWith(panelContext: none())); + }, + forceCollapse: (e) async { + emit(state.copyWith(forceCollapse: e.forceCollapse)); + }, + didReceiveWorkspaceSetting: (_DidReceiveWorkspaceSetting value) { + emit(state.copyWith(workspaceSetting: value.setting)); + }, + collapseMenu: (_CollapseMenu e) { + emit(state.copyWith(isMenuCollapsed: !state.isMenuCollapsed)); + }, + editPanelResizeStart: (_EditPanelResizeStart e) { + emit(state.copyWith( + resizeType: MenuResizeType.drag, + resizeStart: state.resizeOffset, + )); + }, + editPanelResized: (_EditPanelResized e) { + final newPosition = + (e.offset + state.resizeStart).clamp(-50, 200).toDouble(); + if (state.resizeOffset != newPosition) { + emit(state.copyWith(resizeOffset: newPosition)); + } + }, + editPanelResizeEnd: (_EditPanelResizeEnd e) { + emit(state.copyWith(resizeType: MenuResizeType.slide)); + }, + ); + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +enum MenuResizeType { + slide, + drag, +} + +extension MenuResizeTypeExtension on MenuResizeType { + Duration duration() { + switch (this) { + case MenuResizeType.drag: + return 30.milliseconds; + case MenuResizeType.slide: + return 350.milliseconds; + } + } +} + +@freezed +class HomeSettingEvent with _$HomeSettingEvent { + const factory HomeSettingEvent.initial() = _Initial; + const factory HomeSettingEvent.forceCollapse(bool forceCollapse) = + _ForceCollapse; + const factory HomeSettingEvent.setEditPanel(EditPanelContext editContext) = + _ShowEditPanel; + const factory HomeSettingEvent.dismissEditPanel() = _DismissEditPanel; + const factory HomeSettingEvent.didReceiveWorkspaceSetting( + WorkspaceSettingPB setting) = _DidReceiveWorkspaceSetting; + const factory HomeSettingEvent.collapseMenu() = _CollapseMenu; + const factory HomeSettingEvent.editPanelResized(double offset) = + _EditPanelResized; + const factory HomeSettingEvent.editPanelResizeStart() = _EditPanelResizeStart; + const factory HomeSettingEvent.editPanelResizeEnd() = _EditPanelResizeEnd; +} + +@freezed +class HomeSettingState with _$HomeSettingState { + const factory HomeSettingState({ + required bool forceCollapse, + required Option panelContext, + required WorkspaceSettingPB workspaceSetting, + required bool unauthorized, + required bool isMenuCollapsed, + required double resizeOffset, + required double resizeStart, + required MenuResizeType resizeType, + }) = _HomeSettingState; + + factory HomeSettingState.initial(WorkspaceSettingPB workspaceSetting) => + HomeSettingState( + forceCollapse: false, + panelContext: none(), + workspaceSetting: workspaceSetting, + unauthorized: false, + isMenuCollapsed: false, + resizeOffset: 0, + resizeStart: 0, + resizeType: MenuResizeType.slide, + ); +} diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart index e515eb9a1b..d6c033808d 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_layout.dart @@ -1,6 +1,6 @@ import 'dart:io' show Platform; -import 'package:app_flowy/workspace/application/home/home_bloc.dart'; +import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart'; import 'package:flowy_infra/size.dart'; import 'package:flutter/material.dart'; // ignore: import_of_legacy_library_into_null_safe @@ -20,20 +20,19 @@ class HomeLayout { late double menuSpacing; late Duration animDuration; - HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint, - bool forceCollapse) { - final homeBlocState = context.read().state; + HomeLayout(BuildContext context, BoxConstraints homeScreenConstraint) { + final homeSetting = context.read().state; - showEditPanel = homeBlocState.panelContext.isSome(); + showEditPanel = homeSetting.panelContext.isSome(); menuWidth = Sizes.sideBarMed; if (context.widthPx >= PageBreaks.desktop) { menuWidth = Sizes.sideBarLg; } - menuWidth += homeBlocState.resizeOffset; + menuWidth += homeSetting.resizeOffset; - if (forceCollapse) { + if (homeSetting.forceCollapse) { showMenu = false; } else { showMenu = true; @@ -43,7 +42,7 @@ class HomeLayout { homePageLOffset = (showMenu && !menuIsDrawer) ? menuWidth : 0.0; menuSpacing = !showMenu && Platform.isMacOS ? 80.0 : 0.0; - animDuration = homeBlocState.resizeType.duration(); + animDuration = homeSetting.resizeType.duration(); editPanelWidth = HomeSizes.editPanelWidth; homePageROffset = showEditPanel ? editPanelWidth : 0; diff --git a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart index 4995289bc0..7906014848 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/home_screen.dart @@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/blank/blank.dart'; import 'package:app_flowy/startup/plugin/plugin.dart'; import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/workspace/application/home/home_service.dart'; +import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart'; import 'package:app_flowy/workspace/presentation/home/hotkeys.dart'; import 'package:app_flowy/workspace/application/view/view_ext.dart'; @@ -44,6 +45,12 @@ class _HomeScreenState extends State { ..add(const HomeEvent.initial()); }, ), + BlocProvider( + create: (context) { + return HomeSettingBloc(widget.user, widget.workspaceSetting) + ..add(const HomeSettingEvent.initial()); + }, + ), ], child: HomeHotKeys( child: Scaffold( @@ -54,20 +61,20 @@ class _HomeScreenState extends State { Log.error("Push to login screen when user token was invalid"); } }, - child: BlocBuilder( + child: BlocBuilder( buildWhen: (previous, current) => previous != current, builder: (context, state) { final collapsedNotifier = getIt().collapsedNotifier; collapsedNotifier.addPublishListener((isCollapsed) { context - .read() - .add(HomeEvent.forceCollapse(isCollapsed)); + .read() + .add(HomeSettingEvent.forceCollapse(isCollapsed)); }); return FlowyContainer( Theme.of(context).colorScheme.surface, // Colors.white, - child: _buildBody(context, state), + child: _buildBody(context), ); }, ), @@ -76,25 +83,22 @@ class _HomeScreenState extends State { ); } - Widget _buildBody(BuildContext context, HomeState state) { + Widget _buildBody(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - final layout = HomeLayout(context, constraints, state.forceCollapse); + final layout = HomeLayout(context, constraints); final homeStack = HomeStack( layout: layout, delegate: HomeScreenStackAdaptor( buildContext: context, - homeState: state, ), ); final menu = _buildHomeMenu( layout: layout, context: context, - state: state, ); final homeMenuResizer = _buildHomeMenuResizer(context: context); final editPanel = _buildEditPanel( - homeState: state, layout: layout, context: context, ); @@ -111,11 +115,11 @@ class _HomeScreenState extends State { ); } - Widget _buildHomeMenu( - {required HomeLayout layout, - required BuildContext context, - required HomeState state}) { - final workspaceSetting = state.workspaceSetting; + Widget _buildHomeMenu({ + required HomeLayout layout, + required BuildContext context, + }) { + final workspaceSetting = widget.workspaceSetting; final homeMenu = HomeMenu( user: widget.user, workspaceSetting: workspaceSetting, @@ -144,12 +148,12 @@ class _HomeScreenState extends State { return FocusTraversalGroup(child: RepaintBoundary(child: homeMenu)); } - Widget _buildEditPanel( - {required HomeState homeState, - required BuildContext context, - required HomeLayout layout}) { - final homeBloc = context.read(); - return BlocBuilder( + Widget _buildEditPanel({ + required BuildContext context, + required HomeLayout layout, + }) { + final homeBloc = context.read(); + return BlocBuilder( buildWhen: (previous, current) => previous.panelContext != current.panelContext, builder: (context, state) { @@ -160,7 +164,7 @@ class _HomeScreenState extends State { child: EditPanel( panelContext: panelContext, onEndEdit: () => - homeBloc.add(const HomeEvent.dismissEditPanel()), + homeBloc.add(const HomeSettingEvent.dismissEditPanel()), ), ), ), @@ -177,17 +181,17 @@ class _HomeScreenState extends State { child: GestureDetector( dragStartBehavior: DragStartBehavior.down, onHorizontalDragStart: (details) => context - .read() - .add(const HomeEvent.editPanelResizeStart()), + .read() + .add(const HomeSettingEvent.editPanelResizeStart()), onHorizontalDragUpdate: (details) => context - .read() - .add(HomeEvent.editPanelResized(details.localPosition.dx)), + .read() + .add(HomeSettingEvent.editPanelResized(details.localPosition.dx)), onHorizontalDragEnd: (details) => context - .read() - .add(const HomeEvent.editPanelResizeEnd()), + .read() + .add(const HomeSettingEvent.editPanelResizeEnd()), onHorizontalDragCancel: () => context - .read() - .add(const HomeEvent.editPanelResizeEnd()), + .read() + .add(const HomeSettingEvent.editPanelResizeEnd()), behavior: HitTestBehavior.translucent, child: SizedBox( width: 10, @@ -252,11 +256,9 @@ class _HomeScreenState extends State { class HomeScreenStackAdaptor extends HomeStackDelegate { final BuildContext buildContext; - final HomeState homeState; HomeScreenStackAdaptor({ required this.buildContext, - required this.homeState, }); @override diff --git a/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart index 0ac9cbc704..baad36036d 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/hotkeys.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:app_flowy/startup/startup.dart'; -import 'package:app_flowy/workspace/application/home/home_bloc.dart'; +import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:flutter/material.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -22,7 +22,9 @@ class HomeHotKeys extends StatelessWidget { hotKeyManager.register( hotKey, keyDownHandler: (hotKey) { - context.read().add(const HomeEvent.collapseMenu()); + context + .read() + .add(const HomeSettingEvent.collapseMenu()); getIt().collapsedNotifier.value = !getIt().collapsedNotifier.currentValue!; }, diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart index ab1a8ee86c..9c26f80bb0 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/add_button.dart @@ -54,7 +54,7 @@ class AddButtonActionWrapper extends ActionCell { AddButtonActionWrapper({required this.pluginBuilder}); @override - Widget? icon(Color iconColor) => + Widget? leftIcon(Color iconColor) => svgWidget(pluginBuilder.menuIcon, color: iconColor); @override diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart index 497e9e7a33..932f7e1ac1 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/header/header.dart @@ -187,7 +187,7 @@ class DisclosureActionWrapper extends ActionCell { DisclosureActionWrapper(this.inner); @override - Widget? icon(Color iconColor) => inner.icon(iconColor); + Widget? leftIcon(Color iconColor) => inner.icon(iconColor); @override String get name => inner.name; diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart index e89b5e60ad..0874eff25b 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/app/section/item.dart @@ -211,7 +211,7 @@ class ViewDisclosureActionWrapper extends ActionCell { ViewDisclosureActionWrapper(this.inner); @override - Widget? icon(Color iconColor) => inner.icon(iconColor); + Widget? leftIcon(Color iconColor) => inner.icon(iconColor); @override String get name => inner.name; diff --git a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart index b68f205684..72ffba956f 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/menu/menu.dart @@ -4,6 +4,7 @@ export './app/menu_app.dart'; import 'dart:io' show Platform; import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/plugins/trash/menu.dart'; +import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart'; import 'package:app_flowy/workspace/presentation/home/home_sizes.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -21,7 +22,6 @@ import 'package:expandable/expandable.dart'; import 'package:flowy_infra/time/duration.dart'; import 'package:app_flowy/startup/startup.dart'; import 'package:app_flowy/workspace/application/menu/menu_bloc.dart'; -import 'package:app_flowy/workspace/application/home/home_bloc.dart'; import 'package:app_flowy/core/frameless_window.dart'; // import 'package:app_flowy/workspace/presentation/home/home_sizes.dart'; import 'package:flowy_infra/image.dart'; @@ -68,7 +68,7 @@ class HomeMenu extends StatelessWidget { getIt().setPlugin(state.plugin); }, ), - BlocListener( + BlocListener( listenWhen: (p, c) => p.isMenuCollapsed != c.isMenuCollapsed, listener: (context, state) { _collapsedNotifier.value = state.isMenuCollapsed; @@ -231,8 +231,8 @@ class MenuTopBar extends StatelessWidget { width: 28, hoverColor: Colors.transparent, onPressed: () => context - .read() - .add(const HomeEvent.collapseMenu()), + .read() + .add(const HomeSettingEvent.collapseMenu()), iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4), icon: svgWidget( "home/hide_menu", diff --git a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart index 9845d2e79a..1c11136063 100644 --- a/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart +++ b/frontend/app_flowy/lib/workspace/presentation/home/navigation.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:app_flowy/generated/locale_keys.g.dart'; -import 'package:app_flowy/workspace/application/home/home_bloc.dart'; +import 'package:app_flowy/workspace/application/home/home_setting_bloc.dart'; import 'package:app_flowy/workspace/presentation/home/home_stack.dart'; import 'package:flowy_infra/color_extension.dart'; import 'package:flowy_infra/image.dart'; @@ -36,26 +36,6 @@ class NavigationNotifier with ChangeNotifier { } } -// [[diagram: HomeStack navigation flow]] -// ┌───────────────────────┐ -// 2.notify listeners ┌──────│DefaultHomeStackContext│ -// ┌────────────────┐ ┌───────────┐ ┌────────────────┐ │ └───────────────────────┘ -// │HomeStackNotifie│◀──────────│ HomeStack │◀──│HomeStackContext│◀─ impl -// └────────────────┘ └───────────┘ └────────────────┘ │ ┌───────────────────┐ -// │ ▲ └───────│ DocStackContext │ -// │ │ └───────────────────┘ -// 3.notify change 1.set context -// │ │ -// ▼ │ -// ┌───────────────────┐ ┌──────────────────┐ -// │NavigationNotifier │ │ ViewSectionItem │ -// └───────────────────┘ └──────────────────┘ -// │ -// │ -// ▼ -// ┌─────────────────┐ -// │ FlowyNavigation │ 4.render navigation items -// └─────────────────┘ class FlowyNavigation extends StatelessWidget { const FlowyNavigation({Key? key}) : super(key: key); @@ -109,7 +89,9 @@ class FlowyNavigation extends StatelessWidget { hoverColor: Colors.transparent, onPressed: () { notifier.value = false; - ctx.read().add(const HomeEvent.collapseMenu()); + ctx + .read() + .add(const HomeSettingEvent.collapseMenu()); }, iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2), icon: svgWidget( diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart index a3c056b806..5f068916c9 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/emoji_picker/src/emoji_button.dart @@ -94,7 +94,6 @@ class _BuildEmojiPickerViewState extends State { return Stack( children: [ Positioned( - //TODO @gaganyadav80: Not sure about the calculated position. top: widget.offset!.dy - MediaQuery.of(context).size.height / 2.83 - 30, @@ -103,7 +102,6 @@ class _BuildEmojiPickerViewState extends State { child: Material( borderRadius: BorderRadius.circular(8.0), child: SizedBox( - //TODO @gaganyadav80: FIXIT: Gets too large when fullscreen. height: MediaQuery.of(context).size.height / 2.83 + 20, width: MediaQuery.of(context).size.width / 3.92, child: ClipRRect( diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart index 23ad1eb96d..0020ca4004 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/float_bubble/question_bubble.dart @@ -167,7 +167,7 @@ class BubbleActionWrapper extends ActionCell { BubbleActionWrapper(this.inner); @override - Widget? icon(Color iconColor) => FlowyText.regular(inner.emoji); + Widget? leftIcon(Color iconColor) => FlowyText.regular(inner.emoji); @override String get name => inner.name; diff --git a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart index 2acd2dd14f..a2ce989b9d 100644 --- a/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart +++ b/frontend/app_flowy/lib/workspace/presentation/widgets/pop_up_action.dart @@ -8,21 +8,25 @@ import 'package:styled_widget/styled_widget.dart'; class PopoverActionList extends StatefulWidget { final List actions; + final PopoverMutex? mutex; final Function(T, PopoverController) onSelected; final BoxConstraints constraints; final PopoverDirection direction; final Widget Function(PopoverController) buildChild; final VoidCallback? onClosed; + final bool asBarrier; const PopoverActionList({ required this.actions, required this.buildChild, required this.onSelected, + this.mutex, this.onClosed, this.direction = PopoverDirection.rightWithTopAligned, + this.asBarrier = false, this.constraints = const BoxConstraints( minWidth: 120, - maxWidth: 360, + maxWidth: 460, maxHeight: 300, ), Key? key, @@ -47,9 +51,11 @@ class _PopoverActionListState final child = widget.buildChild(popoverController); return AppFlowyPopover( + asBarrier: widget.asBarrier, controller: popoverController, constraints: widget.constraints, direction: widget.direction, + mutex: widget.mutex, triggerActions: PopoverTriggerFlags.none, onClose: widget.onClosed, popupBuilder: (BuildContext popoverContext) { @@ -82,7 +88,8 @@ class _PopoverActionListState } abstract class ActionCell extends PopoverAction { - Widget? icon(Color iconColor); + Widget? leftIcon(Color iconColor) => null; + Widget? rightIcon(Color iconColor) => null; String get name; } @@ -113,7 +120,11 @@ class ActionCellWidget extends StatelessWidget { @override Widget build(BuildContext context) { final actionCell = action as ActionCell; - final icon = actionCell.icon(Theme.of(context).colorScheme.onSurface); + final leftIcon = + actionCell.leftIcon(Theme.of(context).colorScheme.onSurface); + + final rightIcon = + actionCell.rightIcon(Theme.of(context).colorScheme.onSurface); return FlowyHover( child: GestureDetector( @@ -123,13 +134,20 @@ class ActionCellWidget extends StatelessWidget { height: itemHeight, child: Row( children: [ - if (icon != null) ...[icon, HSpace(ActionListSizes.itemHPadding)], + if (leftIcon != null) ...[ + leftIcon, + HSpace(ActionListSizes.itemHPadding) + ], Expanded( child: FlowyText.medium( actionCell.name, overflow: TextOverflow.visible, ), ), + if (rightIcon != null) ...[ + HSpace(ActionListSizes.itemHPadding), + rightIcon, + ], ], ), ).padding( 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 d750d57163..b3f3f19974 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 @@ -16,6 +16,8 @@ class FlowyButton extends StatelessWidget { final Color? hoverColor; final bool isSelected; final BorderRadius radius; + final BoxDecoration? decoration; + final bool useIntrinsicWidth; const FlowyButton({ Key? key, @@ -28,6 +30,8 @@ class FlowyButton extends StatelessWidget { this.hoverColor, this.isSelected = false, this.radius = const BorderRadius.all(Radius.circular(6)), + this.decoration, + this.useIntrinsicWidth = false, }) : super(key: key); @override @@ -63,12 +67,21 @@ class FlowyButton extends StatelessWidget { SizedBox.fromSize(size: const Size.square(16), child: rightIcon!)); } - return Padding( - padding: margin, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: children, + Widget child = Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: children, + ); + + if (useIntrinsicWidth) { + child = IntrinsicWidth(child: child); + } + + return Container( + decoration: decoration, + child: Padding( + padding: margin, + child: child, ), ); } @@ -89,6 +102,7 @@ class FlowyTextButton extends StatelessWidget { final BorderRadius? radius; final MainAxisAlignment mainAxisAlignment; final String? tooltip; + final BoxConstraints constraints; // final HoverDisplayConfig? hoverDisplay; const FlowyTextButton( @@ -106,6 +120,7 @@ class FlowyTextButton extends StatelessWidget { this.radius, this.mainAxisAlignment = MainAxisAlignment.start, this.tooltip, + this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0), }) : super(key: key); @override @@ -146,6 +161,7 @@ class FlowyTextButton extends StatelessWidget { splashColor: Colors.transparent, highlightColor: Colors.transparent, elevation: 0, + constraints: constraints, onPressed: onPressed, child: child, ); @@ -161,18 +177,3 @@ class FlowyTextButton extends StatelessWidget { return child; } } -// return TextButton( -// style: ButtonStyle( -// textStyle: MaterialStateProperty.all(TextStyle(fontSize: fontSize)), -// alignment: Alignment.centerLeft, -// foregroundColor: MaterialStateProperty.all(Colors.black), -// padding: MaterialStateProperty.all( -// const EdgeInsets.symmetric(horizontal: 2)), -// ), -// onPressed: onPressed, -// child: Text( -// text, -// overflow: TextOverflow.ellipsis, -// softWrap: false, -// ), -// ); diff --git a/frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart b/frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart index ff7c101506..9b15075689 100644 --- a/frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart +++ b/frontend/app_flowy/test/bloc_test/board_test/create_or_edit_field_test.dart @@ -25,16 +25,16 @@ void main() { boardBloc = BoardBloc(view: context.gridView) ..add(const BoardEvent.initial()); - final fieldContext = context.singleSelectFieldContext(); + final fieldInfo = context.singleSelectFieldContext(); final loader = FieldTypeOptionLoader( gridId: context.gridView.id, - field: fieldContext.field, + field: fieldInfo.field, ); editorBloc = FieldEditorBloc( gridId: context.gridView.id, - fieldName: fieldContext.name, - isGroupField: fieldContext.isGroupField, + fieldName: fieldInfo.name, + isGroupField: fieldInfo.isGroupField, loader: loader, )..add(const FieldEditorEvent.initial()); diff --git a/frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart b/frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart index 660734d8c8..eb5d02c1d3 100644 --- a/frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart +++ b/frontend/app_flowy/test/bloc_test/board_test/group_by_unsupport_field_test.dart @@ -14,9 +14,9 @@ void main() { setUpAll(() async { boardTest = await AppFlowyBoardTest.ensureInitialized(); context = await boardTest.createTestBoard(); - final fieldContext = context.singleSelectFieldContext(); + final fieldInfo = context.singleSelectFieldContext(); editorBloc = context.createFieldEditor( - fieldContext: fieldContext, + fieldInfo: fieldInfo, )..add(const FieldEditorEvent.initial()); await boardResponseFuture(); diff --git a/frontend/app_flowy/test/bloc_test/board_test/util.dart b/frontend/app_flowy/test/bloc_test/board_test/util.dart index 32ae041fb2..1a198b9538 100644 --- a/frontend/app_flowy/test/bloc_test/board_test/util.dart +++ b/frontend/app_flowy/test/bloc_test/board_test/util.dart @@ -78,26 +78,26 @@ class BoardTestContext { return _boardDataController.blocks; } - List get fieldContexts => fieldController.fieldContexts; + List get fieldContexts => fieldController.fieldInfos; GridFieldController get fieldController { return _boardDataController.fieldController; } FieldEditorBloc createFieldEditor({ - GridFieldContext? fieldContext, + FieldInfo? fieldInfo, }) { IFieldTypeOptionLoader loader; - if (fieldContext == null) { + if (fieldInfo == null) { loader = NewFieldTypeOptionLoader(gridId: gridView.id); } else { loader = - FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field); + FieldTypeOptionLoader(gridId: gridView.id, field: fieldInfo.field); } final editorBloc = FieldEditorBloc( - fieldName: fieldContext?.name ?? '', - isGroupField: fieldContext?.isGroupField ?? false, + fieldName: fieldInfo?.name ?? '', + isGroupField: fieldInfo?.isGroupField ?? false, loader: loader, gridId: gridView.id, ); @@ -146,10 +146,10 @@ class BoardTestContext { return Future(() => editorBloc); } - GridFieldContext singleSelectFieldContext() { - final fieldContext = fieldContexts + FieldInfo singleSelectFieldContext() { + final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.SingleSelect); - return fieldContext; + return fieldInfo; } GridFieldCellContext singleSelectFieldCellContext() { @@ -157,15 +157,15 @@ class BoardTestContext { return GridFieldCellContext(gridId: gridView.id, field: field); } - GridFieldContext textFieldContext() { - final fieldContext = fieldContexts + FieldInfo textFieldContext() { + final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.RichText); - return fieldContext; + return fieldInfo; } - GridFieldContext checkboxFieldContext() { - final fieldContext = fieldContexts + FieldInfo checkboxFieldContext() { + final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.Checkbox); - return fieldContext; + return fieldInfo; } } diff --git a/frontend/app_flowy/test/bloc_test/grid_test/create_filter_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/create_filter_test.dart new file mode 100644 index 0000000000..374c60faac --- /dev/null +++ b/frontend/app_flowy/test/bloc_test/grid_test/create_filter_test.dart @@ -0,0 +1,146 @@ +import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart'; +import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test('create a text filter)', () async { + final context = await gridTest.createTestGrid(); + final service = FilterFFIService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterCondition.TextIsEmpty, + content: ""); + await gridResponseFuture(); + assert(context.fieldController.filterInfos.length == 1); + }); + + test('delete a text filter)', () async { + final context = await gridTest.createTestGrid(); + final service = FilterFFIService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterCondition.TextIsEmpty, + content: ""); + await gridResponseFuture(); + + final filterInfo = context.fieldController.filterInfos.first; + service.deleteFilter( + fieldId: textField.id, + filterId: filterInfo.filter.id, + fieldType: textField.fieldType, + ); + await gridResponseFuture(); + + assert(context.fieldController.filterInfos.isEmpty); + }); + + test('filter rows with condition: text is empty', () async { + final context = await gridTest.createTestGrid(); + final service = FilterFFIService(viewId: context.gridView.id); + final gridController = GridController(view: context.gridView); + final gridBloc = GridBloc( + view: context.gridView, + gridController: gridController, + )..add(const GridEvent.initial()); + await gridResponseFuture(); + + final textField = context.textFieldContext(); + service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterCondition.TextIsEmpty, + content: ""); + await gridResponseFuture(); + + assert(gridBloc.state.rowInfos.length == 3); + }); + + test('filter rows with condition: text is empty(After edit the row)', + () async { + final context = await gridTest.createTestGrid(); + final service = FilterFFIService(viewId: context.gridView.id); + final gridController = GridController(view: context.gridView); + final gridBloc = GridBloc( + view: context.gridView, + gridController: gridController, + )..add(const GridEvent.initial()); + await gridResponseFuture(); + + final textField = context.textFieldContext(); + service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterCondition.TextIsEmpty, + content: ""); + await gridResponseFuture(); + + final controller = await context.makeTextCellController(); + controller.saveCellData("edit text cell content"); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 2); + + controller.saveCellData(""); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 3); + }); + + test('filter rows with condition: text is not empty', () async { + final context = await gridTest.createTestGrid(); + final service = FilterFFIService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + await gridResponseFuture(); + service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterCondition.TextIsNotEmpty, + content: ""); + await gridResponseFuture(); + assert(context.rowInfos.isEmpty); + }); + + test('filter rows with condition: checkbox uncheck', () async { + final context = await gridTest.createTestGrid(); + final checkboxField = context.checkboxFieldContext(); + final service = FilterFFIService(viewId: context.gridView.id); + final gridController = GridController(view: context.gridView); + final gridBloc = GridBloc( + view: context.gridView, + gridController: gridController, + )..add(const GridEvent.initial()); + + await gridResponseFuture(); + service.insertCheckboxFilter( + fieldId: checkboxField.id, + condition: CheckboxFilterCondition.IsUnChecked, + ); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.length == 3); + }); + + test('filter rows with condition: checkbox check', () async { + final context = await gridTest.createTestGrid(); + final checkboxField = context.checkboxFieldContext(); + final service = FilterFFIService(viewId: context.gridView.id); + final gridController = GridController(view: context.gridView); + final gridBloc = GridBloc( + view: context.gridView, + gridController: gridController, + )..add(const GridEvent.initial()); + + await gridResponseFuture(); + service.insertCheckboxFilter( + fieldId: checkboxField.id, + condition: CheckboxFilterCondition.IsChecked, + ); + await gridResponseFuture(); + assert(gridBloc.state.rowInfos.isEmpty); + }); +} diff --git a/frontend/app_flowy/test/bloc_test/grid_test/edit_field_change_filter_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/edit_field_change_filter_test.dart new file mode 100644 index 0000000000..1a8c3edf06 --- /dev/null +++ b/frontend/app_flowy/test/bloc_test/grid_test/edit_field_change_filter_test.dart @@ -0,0 +1,56 @@ +import 'package:app_flowy/plugins/grid/application/field/field_editor_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_menu_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/filter/filter_service.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart'; +import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'util.dart'; + +void main() { + late AppFlowyGridTest gridTest; + setUpAll(() async { + gridTest = await AppFlowyGridTest.ensureInitialized(); + }); + + test("create a text filter and then alter the filter's field)", () async { + final context = await gridTest.createTestGrid(); + final service = FilterFFIService(viewId: context.gridView.id); + final textField = context.textFieldContext(); + + // Create the filter menu bloc + final menuBloc = GridFilterMenuBloc( + fieldController: context.fieldController, + viewId: context.gridView.id, + )..add(const GridFilterMenuEvent.initial()); + + // Insert filter for the text field + service.insertTextFilter( + fieldId: textField.id, + condition: TextFilterCondition.TextIsEmpty, + content: ""); + await gridResponseFuture(); + assert(menuBloc.state.filters.length == 1); + + // Edit the text field + final loader = FieldTypeOptionLoader( + gridId: context.gridView.id, + field: textField.field, + ); + + final editorBloc = FieldEditorBloc( + gridId: context.gridView.id, + fieldName: textField.field.name, + isGroupField: false, + loader: loader, + )..add(const FieldEditorEvent.initial()); + await gridResponseFuture(); + + // Alter the field type to Number + editorBloc.add(const FieldEditorEvent.switchToField(FieldType.Number)); + await gridResponseFuture(); + + // Check the number of filters + assert(menuBloc.state.filters.isEmpty); + }); +} diff --git a/frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/edit_field_edit_test.dart similarity index 88% rename from frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart rename to frontend/app_flowy/test/bloc_test/grid_test/edit_field_edit_test.dart index 108a1837cd..5a8173b7bd 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/field_edit_bloc_test.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/edit_field_edit_test.dart @@ -7,16 +7,16 @@ import 'util.dart'; Future createEditorBloc(AppFlowyGridTest gridTest) async { final context = await gridTest.createTestGrid(); - final fieldContext = context.singleSelectFieldContext(); + final fieldInfo = context.singleSelectFieldContext(); final loader = FieldTypeOptionLoader( gridId: context.gridView.id, - field: fieldContext.field, + field: fieldInfo.field, ); return FieldEditorBloc( gridId: context.gridView.id, - fieldName: fieldContext.name, - isGroupField: fieldContext.isGroupField, + fieldName: fieldInfo.name, + isGroupField: fieldInfo.isGroupField, loader: loader, )..add(const FieldEditorEvent.initial()); } @@ -33,16 +33,16 @@ void main() { setUp(() async { final context = await gridTest.createTestGrid(); - final fieldContext = context.singleSelectFieldContext(); + final fieldInfo = context.singleSelectFieldContext(); final loader = FieldTypeOptionLoader( gridId: context.gridView.id, - field: fieldContext.field, + field: fieldInfo.field, ); editorBloc = FieldEditorBloc( gridId: context.gridView.id, - fieldName: fieldContext.name, - isGroupField: fieldContext.isGroupField, + fieldName: fieldInfo.name, + isGroupField: fieldInfo.isGroupField, loader: loader, )..add(const FieldEditorEvent.initial()); diff --git a/frontend/app_flowy/test/bloc_test/grid_test/filter_bloc_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/filter_bloc_test.dart deleted file mode 100644 index 5434ba0fee..0000000000 --- a/frontend/app_flowy/test/bloc_test/grid_test/filter_bloc_test.dart +++ /dev/null @@ -1,144 +0,0 @@ -import 'package:app_flowy/plugins/grid/application/filter/filter_bloc.dart'; -import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart'; -import 'package:flowy_sdk/protobuf/flowy-grid/text_filter.pb.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:bloc_test/bloc_test.dart'; -import 'util.dart'; - -void main() { - late AppFlowyGridTest gridTest; - setUpAll(() async { - gridTest = await AppFlowyGridTest.ensureInitialized(); - }); - - group('$GridFilterBloc', () { - late GridTestContext context; - setUp(() async { - context = await gridTest.createTestGrid(); - }); - - blocTest( - "create a text filter", - build: () => GridFilterBloc(viewId: context.gridView.id) - ..add(const GridFilterEvent.initial()), - act: (bloc) async { - final textField = context.textFieldContext(); - bloc.add( - GridFilterEvent.createTextFilter( - fieldId: textField.id, - condition: TextFilterCondition.TextIsEmpty, - content: ""), - ); - }, - wait: const Duration(milliseconds: 300), - verify: (bloc) { - assert(bloc.state.filters.length == 1); - }, - ); - - blocTest( - "delete a text filter", - build: () => GridFilterBloc(viewId: context.gridView.id) - ..add(const GridFilterEvent.initial()), - act: (bloc) async { - final textField = context.textFieldContext(); - bloc.add( - GridFilterEvent.createTextFilter( - fieldId: textField.id, - condition: TextFilterCondition.TextIsEmpty, - content: ""), - ); - await gridResponseFuture(); - final filter = bloc.state.filters.first; - bloc.add( - GridFilterEvent.deleteFilter( - fieldId: textField.id, - filterId: filter.id, - fieldType: textField.fieldType, - ), - ); - }, - wait: const Duration(milliseconds: 300), - verify: (bloc) { - assert(bloc.state.filters.isEmpty); - }, - ); - }); - - test('filter rows with condition: text is empty', () async { - final context = await gridTest.createTestGrid(); - final filterBloc = GridFilterBloc(viewId: context.gridView.id) - ..add(const GridFilterEvent.initial()); - - final gridBloc = GridBloc(view: context.gridView) - ..add(const GridEvent.initial()); - - final textField = context.textFieldContext(); - await gridResponseFuture(); - filterBloc.add( - GridFilterEvent.createTextFilter( - fieldId: textField.id, - condition: TextFilterCondition.TextIsEmpty, - content: ""), - ); - - await gridResponseFuture(); - assert(gridBloc.state.rowInfos.length == 3); - }); - - test('filter rows with condition: text is not empty', () async { - final context = await gridTest.createTestGrid(); - final filterBloc = GridFilterBloc(viewId: context.gridView.id) - ..add(const GridFilterEvent.initial()); - - final textField = context.textFieldContext(); - await gridResponseFuture(); - filterBloc.add( - GridFilterEvent.createTextFilter( - fieldId: textField.id, - condition: TextFilterCondition.TextIsNotEmpty, - content: ""), - ); - await gridResponseFuture(); - assert(context.rowInfos.isEmpty); - }); - - test('filter rows with condition: checkbox uncheck', () async { - final context = await gridTest.createTestGrid(); - final checkboxField = context.checkboxFieldContext(); - final filterBloc = GridFilterBloc(viewId: context.gridView.id) - ..add(const GridFilterEvent.initial()); - final gridBloc = GridBloc(view: context.gridView) - ..add(const GridEvent.initial()); - - await gridResponseFuture(); - filterBloc.add( - GridFilterEvent.createCheckboxFilter( - fieldId: checkboxField.id, - condition: CheckboxFilterCondition.IsUnChecked, - ), - ); - await gridResponseFuture(); - assert(gridBloc.state.rowInfos.length == 3); - }); - - test('filter rows with condition: checkbox check', () async { - final context = await gridTest.createTestGrid(); - final checkboxField = context.checkboxFieldContext(); - final filterBloc = GridFilterBloc(viewId: context.gridView.id) - ..add(const GridFilterEvent.initial()); - final gridBloc = GridBloc(view: context.gridView) - ..add(const GridEvent.initial()); - - await gridResponseFuture(); - filterBloc.add( - GridFilterEvent.createCheckboxFilter( - fieldId: checkboxField.id, - condition: CheckboxFilterCondition.IsChecked, - ), - ); - await gridResponseFuture(); - assert(gridBloc.state.rowInfos.isEmpty); - }); -} diff --git a/frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart b/frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart index 0afff2cbc6..8020c77ac2 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/grid_bloc_test.dart @@ -1,4 +1,5 @@ import 'package:app_flowy/plugins/grid/application/grid_bloc.dart'; +import 'package:app_flowy/plugins/grid/application/grid_data_controller.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:bloc_test/bloc_test.dart'; import 'util.dart'; @@ -17,8 +18,10 @@ void main() { // The initial number of rows is 3 for each grid. blocTest( "create a row", - build: () => - GridBloc(view: context.gridView)..add(const GridEvent.initial()), + build: () => GridBloc( + view: context.gridView, + gridController: GridController(view: context.gridView)) + ..add(const GridEvent.initial()), act: (bloc) => bloc.add(const GridEvent.createRow()), wait: const Duration(milliseconds: 300), verify: (bloc) { @@ -28,8 +31,10 @@ void main() { blocTest( "delete the last row", - build: () => - GridBloc(view: context.gridView)..add(const GridEvent.initial()), + build: () => GridBloc( + view: context.gridView, + gridController: GridController(view: context.gridView)) + ..add(const GridEvent.initial()), act: (bloc) async { await gridResponseFuture(); bloc.add(GridEvent.deleteRow(bloc.state.rowInfos.last)); diff --git a/frontend/app_flowy/test/bloc_test/grid_test/util.dart b/frontend/app_flowy/test/bloc_test/grid_test/util.dart index d0c13471d0..8e9dd16fec 100644 --- a/frontend/app_flowy/test/bloc_test/grid_test/util.dart +++ b/frontend/app_flowy/test/bloc_test/grid_test/util.dart @@ -18,42 +18,42 @@ import '../../util.dart'; class GridTestContext { final ViewPB gridView; - final GridDataController _gridDataController; + final GridController _gridController; - GridTestContext(this.gridView, this._gridDataController); + GridTestContext(this.gridView, this._gridController); List get rowInfos { - return _gridDataController.rowInfos; + return _gridController.rowInfos; } UnmodifiableMapView get blocks { - return _gridDataController.blocks; + return _gridController.blocks; } - List get fieldContexts => fieldController.fieldContexts; + List get fieldContexts => fieldController.fieldInfos; GridFieldController get fieldController { - return _gridDataController.fieldController; + return _gridController.fieldController; } Future createRow() async { - return _gridDataController.createRow(); + return _gridController.createRow(); } FieldEditorBloc createFieldEditor({ - GridFieldContext? fieldContext, + FieldInfo? fieldInfo, }) { IFieldTypeOptionLoader loader; - if (fieldContext == null) { + if (fieldInfo == null) { loader = NewFieldTypeOptionLoader(gridId: gridView.id); } else { loader = - FieldTypeOptionLoader(gridId: gridView.id, field: fieldContext.field); + FieldTypeOptionLoader(gridId: gridView.id, field: fieldInfo.field); } final editorBloc = FieldEditorBloc( - fieldName: fieldContext?.name ?? '', - isGroupField: fieldContext?.isGroupField ?? false, + fieldName: fieldInfo?.name ?? '', + isGroupField: fieldInfo?.isGroupField ?? false, loader: loader, gridId: gridView.id, ); @@ -71,7 +71,7 @@ class GridTestContext { final RowInfo rowInfo = rowInfos.last; final blockCache = blocks[rowInfo.rowPB.blockId]; final rowCache = blockCache?.rowCache; - final fieldController = _gridDataController.fieldController; + final fieldController = _gridController.fieldController; final rowDataController = GridRowDataController( rowInfo: rowInfo, @@ -101,10 +101,10 @@ class GridTestContext { return Future(() => editorBloc); } - GridFieldContext singleSelectFieldContext() { - final fieldContext = fieldContexts + FieldInfo singleSelectFieldContext() { + final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.SingleSelect); - return fieldContext; + return fieldInfo; } GridFieldCellContext singleSelectFieldCellContext() { @@ -112,16 +112,36 @@ class GridTestContext { return GridFieldCellContext(gridId: gridView.id, field: field); } - GridFieldContext textFieldContext() { - final fieldContext = fieldContexts + FieldInfo textFieldContext() { + final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.RichText); - return fieldContext; + return fieldInfo; } - GridFieldContext checkboxFieldContext() { - final fieldContext = fieldContexts + FieldInfo checkboxFieldContext() { + final fieldInfo = fieldContexts .firstWhere((element) => element.fieldType == FieldType.Checkbox); - return fieldContext; + return fieldInfo; + } + + Future makeSelectOptionCellController( + FieldType fieldType) async { + assert(fieldType == FieldType.SingleSelect || + fieldType == FieldType.MultiSelect); + + final field = + fieldContexts.firstWhere((element) => element.fieldType == fieldType); + final cellController = + await makeCellController(field.id) as GridSelectOptionCellController; + return cellController; + } + + Future makeTextCellController() async { + final field = fieldContexts + .firstWhere((element) => element.fieldType == FieldType.RichText); + final cellController = + await makeCellController(field.id) as GridCellController; + return cellController; } } @@ -150,8 +170,8 @@ class AppFlowyGridTest { .then((result) { return result.fold( (view) async { - final context = GridTestContext(view, GridDataController(view: view)); - final result = await context._gridDataController.openGrid(); + final context = GridTestContext(view, GridController(view: view)); + final result = await context._gridController.openGrid(); result.fold((l) => null, (r) => throw Exception(r)); return context; }, @@ -186,15 +206,7 @@ class AppFlowyGridCellTest { Future makeCellController( FieldType fieldType) async { - assert(fieldType == FieldType.SingleSelect || - fieldType == FieldType.MultiSelect); - - final fieldContexts = context.fieldContexts; - final field = - fieldContexts.firstWhere((element) => element.fieldType == fieldType); - final cellController = await context.makeCellController(field.id) - as GridSelectOptionCellController; - return cellController; + return context.makeSelectOptionCellController(fieldType); } } diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index cf12cbe38f..5ff62a50c5 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -21,7 +21,7 @@ members = [ [profile.dev] opt-level = 0 #https://doc.rust-lang.org/rustc/codegen-options/index.html#debug-assertions -split-debuginfo = "unpacked" +#split-debuginfo = "unpacked" [profile.release] opt-level = 3 diff --git a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs index 05c9f28636..252e5527de 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/block_entities.rs @@ -133,6 +133,14 @@ impl InsertedRowPB { is_new: false, } } + + pub fn with_index(row: RowPB, index: i32) -> Self { + Self { + row, + index: Some(index), + is_new: false, + } + } } impl std::convert::From for InsertedRowPB { @@ -167,7 +175,7 @@ pub struct GridBlockChangesetPB { pub updated_rows: Vec, #[pb(index = 5)] - pub visible_rows: Vec, + pub visible_rows: Vec, #[pb(index = 6)] pub invisible_rows: Vec, diff --git a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs index a46d1f99f8..3f32debcb3 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/field_entities.rs @@ -349,13 +349,13 @@ pub struct GetFieldPayloadPB { #[pb(index = 1)] pub grid_id: String, - #[pb(index = 2)] - pub field_ids: RepeatedFieldIdPB, + #[pb(index = 2, one_of)] + pub field_ids: Option, } pub struct GetFieldParams { pub grid_id: String, - pub field_ids: RepeatedFieldIdPB, + pub field_ids: Option>, } impl TryInto for GetFieldPayloadPB { @@ -363,9 +363,17 @@ impl TryInto for GetFieldPayloadPB { fn try_into(self) -> Result { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; + let field_ids = self.field_ids.map(|repeated| { + repeated + .items + .into_iter() + .map(|item| item.field_id) + .collect::>() + }); + Ok(GetFieldParams { grid_id: grid_id.0, - field_ids: self.field_ids, + field_ids, }) } } @@ -401,9 +409,8 @@ pub struct FieldChangesetPB { #[pb(index = 8, one_of)] pub width: Option, - - #[pb(index = 9, one_of)] - pub type_option_data: Option>, + // #[pb(index = 9, one_of)] + // pub type_option_data: Option>, } impl TryInto for FieldChangesetPB { @@ -413,11 +420,11 @@ impl TryInto for FieldChangesetPB { let grid_id = NotEmptyStr::parse(self.grid_id).map_err(|_| ErrorCode::GridIdIsEmpty)?; let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; let field_type = self.field_type.map(FieldTypeRevision::from); - if let Some(type_option_data) = self.type_option_data.as_ref() { - if type_option_data.is_empty() { - return Err(ErrorCode::TypeOptionDataIsEmpty); - } - } + // if let Some(type_option_data) = self.type_option_data.as_ref() { + // if type_option_data.is_empty() { + // return Err(ErrorCode::TypeOptionDataIsEmpty); + // } + // } Ok(FieldChangesetParams { field_id: field_id.0, @@ -428,7 +435,7 @@ impl TryInto for FieldChangesetPB { frozen: self.frozen, visibility: self.visibility, width: self.width, - type_option_data: self.type_option_data, + // type_option_data: self.type_option_data, }) } } @@ -450,8 +457,7 @@ pub struct FieldChangesetParams { pub visibility: Option, pub width: Option, - - pub type_option_data: Option>, + // pub type_option_data: Option>, } /// Certain field types have user-defined options such as color, date format, number format, /// or a list of values for a multi-select list. These options are defined within a specialization diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs index 25c5b6b2ce..09345fbabe 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/filter_changeset.rs @@ -11,6 +11,18 @@ pub struct FilterChangesetNotificationPB { #[pb(index = 3)] pub delete_filters: Vec, + + #[pb(index = 4)] + pub update_filters: Vec, +} + +#[derive(Debug, Default, ProtoBuf)] +pub struct UpdatedFilter { + #[pb(index = 1)] + pub filter_id: String, + + #[pb(index = 2, one_of)] + pub filter: Option, } impl FilterChangesetNotificationPB { @@ -19,6 +31,7 @@ impl FilterChangesetNotificationPB { view_id: view_id.to_string(), insert_filters: filters, delete_filters: Default::default(), + update_filters: Default::default(), } } pub fn from_delete(view_id: &str, filters: Vec) -> Self { @@ -26,6 +39,16 @@ impl FilterChangesetNotificationPB { view_id: view_id.to_string(), insert_filters: Default::default(), delete_filters: filters, + update_filters: Default::default(), + } + } + + pub fn from_update(view_id: &str, filters: Vec) -> Self { + Self { + view_id: view_id.to_string(), + insert_filters: Default::default(), + delete_filters: Default::default(), + update_filters: filters, } } } diff --git a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs index 0cfb5c5e60..18c73269f8 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/filter_entities/util.rs @@ -17,15 +17,18 @@ pub struct FilterPB { pub id: String, #[pb(index = 2)] - pub ty: FieldType, + pub field_id: String, #[pb(index = 3)] + pub field_type: FieldType, + + #[pb(index = 4)] pub data: Vec, } impl std::convert::From<&FilterRevision> for FilterPB { fn from(rev: &FilterRevision) -> Self { - let field_type: FieldType = rev.field_type_rev.into(); + let field_type: FieldType = rev.field_type.into(); let bytes: Bytes = match field_type { FieldType::RichText => TextFilterPB::from(rev).try_into().unwrap(), FieldType::Number => NumberFilterPB::from(rev).try_into().unwrap(), @@ -37,7 +40,8 @@ impl std::convert::From<&FilterRevision> for FilterPB { }; Self { id: rev.id.clone(), - ty: rev.field_type_rev.into(), + field_id: rev.field_id.clone(), + field_type: rev.field_type.into(), data: bytes.to_vec(), } } @@ -103,36 +107,44 @@ pub struct DeleteFilterParams { } #[derive(ProtoBuf, Debug, Default, Clone)] -pub struct CreateFilterPayloadPB { +pub struct AlterFilterPayloadPB { #[pb(index = 1)] pub field_id: String, #[pb(index = 2)] pub field_type: FieldType, - #[pb(index = 3)] + #[pb(index = 3, one_of)] + pub filter_id: Option, + + #[pb(index = 4)] pub data: Vec, } -impl CreateFilterPayloadPB { +impl AlterFilterPayloadPB { #[allow(dead_code)] pub fn new>(field_rev: &FieldRevision, data: T) -> Self { let data = data.try_into().unwrap_or_else(|_| Bytes::new()); Self { field_id: field_rev.id.clone(), field_type: field_rev.ty.into(), + filter_id: None, data: data.to_vec(), } } } -impl TryInto for CreateFilterPayloadPB { +impl TryInto for AlterFilterPayloadPB { type Error = ErrorCode; - fn try_into(self) -> Result { + fn try_into(self) -> Result { let field_id = NotEmptyStr::parse(self.field_id) .map_err(|_| ErrorCode::FieldIdIsEmpty)? .0; + let filter_id = match self.filter_id { + None => None, + Some(filter_id) => Some(NotEmptyStr::parse(filter_id).map_err(|_| ErrorCode::FilterIdIsEmpty)?.0), + }; let condition; let mut content = "".to_string(); let bytes: &[u8] = self.data.as_ref(); @@ -169,9 +181,10 @@ impl TryInto for CreateFilterPayloadPB { } } - Ok(CreateFilterParams { + Ok(AlterFilterParams { field_id, - field_type_rev: self.field_type.into(), + filter_id, + field_type: self.field_type.into(), condition, content, }) @@ -179,9 +192,10 @@ impl TryInto for CreateFilterPayloadPB { } #[derive(Debug)] -pub struct CreateFilterParams { +pub struct AlterFilterParams { pub field_id: String, - pub field_type_rev: FieldTypeRevision, + pub filter_id: Option, + pub field_type: FieldTypeRevision, pub condition: u8, pub content: String, } diff --git a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs index ea2639ea05..7a09581979 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/group_entities/group.rs @@ -39,7 +39,7 @@ impl TryInto for CreateBoardCardPayloadPB { } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct GridGroupConfigurationPB { +pub struct GroupConfigurationPB { #[pb(index = 1)] pub id: String, @@ -47,9 +47,9 @@ pub struct GridGroupConfigurationPB { pub field_id: String, } -impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationPB { +impl std::convert::From<&GroupConfigurationRevision> for GroupConfigurationPB { fn from(rev: &GroupConfigurationRevision) -> Self { - GridGroupConfigurationPB { + GroupConfigurationPB { id: rev.id.clone(), field_id: rev.field_id.clone(), } @@ -57,19 +57,19 @@ impl std::convert::From<&GroupConfigurationRevision> for GridGroupConfigurationP } #[derive(ProtoBuf, Debug, Default, Clone)] -pub struct RepeatedGridGroupPB { +pub struct RepeatedGroupPB { #[pb(index = 1)] pub items: Vec, } -impl std::ops::Deref for RepeatedGridGroupPB { +impl std::ops::Deref for RepeatedGroupPB { type Target = Vec; fn deref(&self) -> &Self::Target { &self.items } } -impl std::ops::DerefMut for RepeatedGridGroupPB { +impl std::ops::DerefMut for RepeatedGroupPB { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items } @@ -110,20 +110,20 @@ impl std::convert::From for GroupPB { } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] -pub struct RepeatedGridGroupConfigurationPB { +pub struct RepeatedGroupConfigurationPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } -impl std::convert::From> for RepeatedGridGroupConfigurationPB { - fn from(items: Vec) -> Self { +impl std::convert::From> for RepeatedGroupConfigurationPB { + fn from(items: Vec) -> Self { Self { items } } } -impl std::convert::From>> for RepeatedGridGroupConfigurationPB { +impl std::convert::From>> for RepeatedGroupConfigurationPB { fn from(revs: Vec>) -> Self { - RepeatedGridGroupConfigurationPB { + RepeatedGroupConfigurationPB { items: revs.iter().map(|rev| rev.as_ref().into()).collect(), } } 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 fbeb2117d6..7e33acc0b7 100644 --- a/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-grid/src/entities/setting_entities.rs @@ -1,7 +1,7 @@ use crate::entities::parser::NotEmptyStr; use crate::entities::{ - CreateFilterParams, CreateFilterPayloadPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, - DeleteGroupPayloadPB, InsertGroupParams, InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGridGroupConfigurationPB, + AlterFilterParams, AlterFilterPayloadPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteGroupParams, + DeleteGroupPayloadPB, InsertGroupParams, InsertGroupPayloadPB, RepeatedFilterPB, RepeatedGroupConfigurationPB, }; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; @@ -20,10 +20,10 @@ pub struct GridSettingPB { pub layout_type: GridLayout, #[pb(index = 3)] - pub filter_configurations: RepeatedFilterPB, + pub filters: RepeatedFilterPB, #[pb(index = 4)] - pub group_configurations: RepeatedGridGroupConfigurationPB, + pub group_configurations: RepeatedGroupConfigurationPB, } #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -83,7 +83,7 @@ pub struct GridSettingChangesetPB { pub layout_type: GridLayout, #[pb(index = 3, one_of)] - pub insert_filter: Option, + pub insert_filter: Option, #[pb(index = 4, one_of)] pub delete_filter: Option, @@ -137,7 +137,7 @@ impl TryInto for GridSettingChangesetPB { pub struct GridSettingChangesetParams { pub grid_id: String, pub layout_type: LayoutRevision, - pub insert_filter: Option, + pub insert_filter: Option, pub delete_filter: Option, pub insert_group: Option, pub delete_group: Option, diff --git a/frontend/rust-lib/flowy-grid/src/event_handler.rs b/frontend/rust-lib/flowy-grid/src/event_handler.rs index 0820a54d56..9ab59b790b 100644 --- a/frontend/rust-lib/flowy-grid/src/event_handler.rs +++ b/frontend/rust-lib/flowy-grid/src/event_handler.rs @@ -51,8 +51,8 @@ pub(crate) async fn update_grid_setting_handler( let _ = editor.delete_group(delete_params).await?; } - if let Some(create_filter) = params.insert_filter { - let _ = editor.create_filter(create_filter).await?; + if let Some(alter_filter) = params.insert_filter { + let _ = editor.create_or_update_filter(alter_filter).await?; } if let Some(delete_filter) = params.delete_filter { @@ -92,13 +92,7 @@ pub(crate) async fn get_fields_handler( ) -> DataResult { let params: GetFieldParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id).await?; - let field_orders = params - .field_ids - .items - .into_iter() - .map(|field_order| field_order.field_id) - .collect(); - let field_revs = editor.get_field_revs(Some(field_orders)).await?; + let field_revs = editor.get_field_revs(params.field_ids).await?; let repeated_field: RepeatedFieldPB = field_revs.into_iter().map(FieldPB::from).collect::>().into(); data_result(repeated_field) } @@ -121,8 +115,14 @@ pub(crate) async fn update_field_type_option_handler( ) -> Result<(), FlowyError> { let params: TypeOptionChangesetParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id).await?; + let old_field_rev = editor.get_field_rev(¶ms.field_id).await; let _ = editor - .update_field_type_option(¶ms.grid_id, ¶ms.field_id, params.type_option_data) + .did_update_field_type_option( + ¶ms.grid_id, + ¶ms.field_id, + params.type_option_data, + old_field_rev, + ) .await?; Ok(()) } @@ -145,20 +145,21 @@ pub(crate) async fn switch_to_field_handler( ) -> Result<(), FlowyError> { let params: EditFieldParams = data.into_inner().try_into()?; let editor = manager.get_grid_editor(¶ms.grid_id).await?; + let old_field_rev = editor.get_field_rev(¶ms.field_id).await; editor .switch_to_field_type(¶ms.field_id, ¶ms.field_type) .await?; // Get the field_rev with field_id, if it doesn't exist, we create the default FieldRevision from the FieldType. - let field_rev = editor + let new_field_rev = editor .get_field_rev(¶ms.field_id) .await .unwrap_or(Arc::new(editor.next_field_rev(¶ms.field_type).await?)); // Update the type-option data after the field type has been changed - let type_option_data = get_type_option_data(&field_rev, ¶ms.field_type).await?; + let type_option_data = get_type_option_data(&new_field_rev, ¶ms.field_type).await?; let _ = editor - .update_field_type_option(¶ms.grid_id, &field_rev.id, type_option_data) + .did_update_field_type_option(¶ms.grid_id, &new_field_rev.id, type_option_data, old_field_rev) .await?; Ok(()) @@ -462,7 +463,7 @@ pub(crate) async fn update_date_cell_handler( pub(crate) async fn get_groups_handler( data: Data, manager: AppData>, -) -> DataResult { +) -> DataResult { let params: GridIdPB = data.into_inner(); let editor = manager.get_grid_editor(¶ms.value).await?; let group = editor.load_groups().await?; diff --git a/frontend/rust-lib/flowy-grid/src/event_map.rs b/frontend/rust-lib/flowy-grid/src/event_map.rs index 96b7f48fce..f833cefece 100644 --- a/frontend/rust-lib/flowy-grid/src/event_map.rs +++ b/frontend/rust-lib/flowy-grid/src/event_map.rs @@ -212,7 +212,7 @@ pub enum GridEvent { #[event(input = "DateChangesetPB")] UpdateDateCell = 80, - #[event(input = "GridIdPB", output = "RepeatedGridGroupPB")] + #[event(input = "GridIdPB", output = "RepeatedGroupPB")] GetGroup = 100, #[event(input = "CreateBoardCardPayloadPB", output = "RowPB")] diff --git a/frontend/rust-lib/flowy-grid/src/services/block_editor.rs b/frontend/rust-lib/flowy-grid/src/services/block_editor.rs index b8e1fc1874..0fa417ff9c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_editor.rs @@ -109,9 +109,8 @@ impl GridBlockRevisionEditor { self.pad.read().await.index_of_row(row_id) } - pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult>> { - let row_ids = vec![Cow::Borrowed(row_id)]; - let row_rev = self.get_row_revs(Some(row_ids)).await?.pop(); + pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult)>> { + let row_rev = self.pad.read().await.get_row_rev(row_id); Ok(row_rev) } diff --git a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs index 18addf2620..f84cd8af52 100644 --- a/frontend/rust-lib/flowy-grid/src/services/block_manager.rs +++ b/frontend/rust-lib/flowy-grid/src/services/block_manager.rs @@ -110,8 +110,8 @@ impl GridBlockManager { let _ = editor.update_row(changeset.clone()).await?; match editor.get_row_rev(&changeset.row_id).await? { None => tracing::error!("Update row failed, can't find the row with id: {}", changeset.row_id), - Some(row_rev) => { - let row_pb = make_row_from_row_rev(row_rev.clone()); + Some((_, row_rev)) => { + let row_pb = make_row_from_row_rev(row_rev); let block_order_changeset = GridBlockChangesetPB::update(&editor.block_id, vec![row_pb]); let _ = self .notify_did_update_block(&editor.block_id, block_order_changeset) @@ -128,7 +128,7 @@ impl GridBlockManager { let editor = self.get_block_editor(&block_id).await?; match editor.get_row_rev(&row_id).await? { None => Ok(None), - Some(row_rev) => { + Some((_, row_rev)) => { let _ = editor.delete_rows(vec![Cow::Borrowed(&row_id)]).await?; let _ = self .notify_did_update_block( @@ -198,15 +198,9 @@ impl GridBlockManager { Ok(()) } - pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult>> { + pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult)>> { let editor = self.get_editor_from_row_id(row_id).await?; - let row_ids = vec![Cow::Borrowed(row_id)]; - let mut row_revs = editor.get_row_revs(Some(row_ids)).await?; - if row_revs.is_empty() { - Ok(None) - } else { - Ok(row_revs.pop()) - } + editor.get_row_rev(row_id).await } pub async fn get_row_revs(&self, block_id: &str) -> FlowyResult>> { diff --git a/frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs b/frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs index 3c358af6f6..2924f4f19c 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/field_operation.rs @@ -18,10 +18,12 @@ where }; if let Some(mut type_option) = get_type_option.await { + let old_field_rev = editor.get_field_rev(field_id).await; + action(&mut type_option); let bytes = type_option.protobuf_bytes().to_vec(); let _ = editor - .update_field_type_option(&editor.grid_id, field_id, bytes) + .did_update_field_type_option(&editor.grid_id, field_id, bytes, old_field_rev) .await?; } diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs index f677021e3a..c4ee0dd3db 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -64,7 +64,9 @@ impl CellDisplayable for CheckboxTypeOptionPB { } } -impl CellDataOperation for CheckboxTypeOptionPB { +pub type CheckboxCellChangeset = String; + +impl CellDataOperation for CheckboxTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs index f4116e9118..2cefab11f9 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/number_type_option/number_type_option.rs @@ -130,7 +130,9 @@ impl CellDisplayable for NumberTypeOptionPB { } } -impl CellDataOperation for NumberTypeOptionPB { +pub type NumberCellChangeset = String; + +impl CellDataOperation for NumberTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, diff --git a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs index a91d44454b..bac3516135 100644 --- a/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs +++ b/frontend/rust-lib/flowy-grid/src/services/field/type_options/url_type_option/url_type_option.rs @@ -58,7 +58,9 @@ impl CellDisplayable for URLTypeOptionPB { } } -impl CellDataOperation for URLTypeOptionPB { +pub type URLCellChangeset = String; + +impl CellDataOperation for URLTypeOptionPB { fn decode_cell_data( &self, cell_data: CellData, 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 c3b9b31d6d..458a258a5a 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/controller.rs @@ -1,6 +1,5 @@ use crate::entities::filter_entities::*; - -use crate::entities::FieldType; +use crate::entities::{FieldType, InsertedRowPB, RowPB}; use crate::services::cell::{CellFilterOperation, TypeCellData}; use crate::services::field::*; use crate::services::filter::{FilterChangeset, FilterMap, FilterResult, FilterResultNotification, FilterType}; @@ -10,16 +9,19 @@ use flowy_error::FlowyResult; use flowy_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; use grid_rev_model::{CellRevision, FieldId, FieldRevision, FilterRevision, RowRevision}; use lib_infra::future::Fut; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; type RowId = String; pub trait FilterDelegate: Send + Sync + 'static { - fn get_filter_rev(&self, filter_id: FilterType) -> Fut>>; + fn get_filter_rev(&self, filter_type: FilterType) -> Fut>>; fn get_field_rev(&self, field_id: &str) -> Fut>>; fn get_field_revs(&self, field_ids: Option>) -> Fut>>; fn get_blocks(&self) -> Fut>; + fn get_row_rev(&self, rows_id: &str) -> Fut)>>; } pub struct FilterController { @@ -53,7 +55,7 @@ impl FilterController { task_scheduler, notifier, }; - this.load_filters(filter_revs).await; + this.cache_filters(filter_revs).await; this } @@ -62,12 +64,12 @@ impl FilterController { } #[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))] - async fn gen_task(&mut self, predicate: &str) { + async fn gen_task(&mut self, task_type: FilterEvent) { let task_id = self.task_scheduler.read().await.next_task_id(); let task = Task::new( &self.handler_id, task_id, - TaskContent::Text(predicate.to_owned()), + TaskContent::Text(task_type.to_string()), QualityOfService::UserInteractive, ); self.task_scheduler.write().await.add_task(task); @@ -105,24 +107,64 @@ impl FilterController { } #[tracing::instrument(name = "receive_task_result", level = "trace", skip_all, fields(filter_result), err)] - pub async fn process(&mut self, _predicate: &str) -> FlowyResult<()> { + pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> { + let event_type = FilterEvent::from_str(predicate).unwrap(); + match event_type { + FilterEvent::FilterDidChanged => self.filter_all_rows().await?, + FilterEvent::RowDidChanged(row_id) => self.filter_row(row_id).await?, + } + Ok(()) + } + + async fn filter_row(&mut self, row_id: String) -> FlowyResult<()> { + if let Some((_, row_rev)) = self.delegate.get_row_rev(&row_id).await { + let field_rev_by_field_id = self.get_filter_revs_map().await; + let mut notification = FilterResultNotification::new(self.view_id.clone(), row_rev.block_id.clone()); + if let Some((row_id, is_visible)) = filter_row( + &row_rev, + &self.filter_map, + &mut self.result_by_row_id, + &field_rev_by_field_id, + ) { + if is_visible { + if let Some((index, row_rev)) = self.delegate.get_row_rev(&row_id).await { + let row_pb = RowPB::from(row_rev.as_ref()); + notification + .visible_rows + .push(InsertedRowPB::with_index(row_pb, index as i32)) + } + } else { + notification.invisible_rows.push(row_id); + } + } + + let _ = self + .notifier + .send(GridViewChanged::DidReceiveFilterResult(notification)); + } + Ok(()) + } + + async fn filter_all_rows(&mut self) -> FlowyResult<()> { let field_rev_by_field_id = self.get_filter_revs_map().await; for block in self.delegate.get_blocks().await.into_iter() { // The row_ids contains the row that its visibility was changed. let mut visible_rows = vec![]; let mut invisible_rows = vec![]; - for row_rev in &block.row_revs { - let (row_id, is_visible) = filter_row( + for (index, row_rev) in block.row_revs.iter().enumerate() { + if let Some((row_id, is_visible)) = filter_row( row_rev, &self.filter_map, &mut self.result_by_row_id, &field_rev_by_field_id, - ); - if is_visible { - visible_rows.push(row_id) - } else { - invisible_rows.push(row_id); + ) { + if is_visible { + let row_pb = RowPB::from(row_rev.as_ref()); + visible_rows.push(InsertedRowPB::with_index(row_pb, index as i32)) + } else { + invisible_rows.push(row_id); + } } } @@ -137,25 +179,78 @@ impl FilterController { .notifier .send(GridViewChanged::DidReceiveFilterResult(notification)); } - Ok(()) } - pub async fn apply_changeset(&mut self, changeset: FilterChangeset) { - if let Some(filter_id) = &changeset.insert_filter { - let filter_revs = self.delegate.get_filter_rev(filter_id.clone()).await; - let _ = self.load_filters(filter_revs).await; + pub async fn did_receive_row_changed(&mut self, row_id: &str) { + self.gen_task(FilterEvent::RowDidChanged(row_id.to_string())).await + } + + #[tracing::instrument(level = "trace", skip(self))] + pub async fn did_receive_filter_changed( + &mut self, + changeset: FilterChangeset, + ) -> Option { + let mut notification: Option = None; + if let Some(filter_type) = &changeset.insert_filter { + if let Some(filter) = self.filter_from_filter_type(filter_type).await { + notification = Some(FilterChangesetNotificationPB::from_insert(&self.view_id, vec![filter])); + } + if let Some(filter_rev) = self.delegate.get_filter_rev(filter_type.clone()).await { + let _ = self.cache_filters(vec![filter_rev]).await; + } } - if let Some(filter_id) = &changeset.delete_filter { - self.filter_map.remove(filter_id); + if let Some(updated_filter_type) = changeset.update_filter { + if let Some(old_filter_type) = updated_filter_type.old { + let new_filter = self.filter_from_filter_type(&updated_filter_type.new).await; + let old_filter = self.filter_from_filter_type(&old_filter_type).await; + + // Get the filter id + let mut filter_id = old_filter.map(|filter| filter.id); + if filter_id.is_none() { + filter_id = new_filter.as_ref().map(|filter| filter.id.clone()); + } + + // Update the cached filter + if let Some(filter_rev) = self.delegate.get_filter_rev(updated_filter_type.new.clone()).await { + let _ = self.cache_filters(vec![filter_rev]).await; + } + + if let Some(filter_id) = filter_id { + let updated_filter = UpdatedFilter { + filter_id, + filter: new_filter, + }; + notification = Some(FilterChangesetNotificationPB::from_update( + &self.view_id, + vec![updated_filter], + )); + } + } } - self.gen_task("").await; + if let Some(filter_type) = &changeset.delete_filter { + if let Some(filter) = self.filter_from_filter_type(filter_type).await { + notification = Some(FilterChangesetNotificationPB::from_delete(&self.view_id, vec![filter])); + } + self.filter_map.remove(filter_type); + } + + let _ = self.gen_task(FilterEvent::FilterDidChanged).await; + tracing::trace!("{:?}", notification); + notification + } + + async fn filter_from_filter_type(&self, filter_type: &FilterType) -> Option { + self.delegate + .get_filter_rev(filter_type.clone()) + .await + .map(|filter| FilterPB::from(filter.as_ref())) } #[tracing::instrument(level = "trace", skip_all)] - async fn load_filters(&mut self, filter_revs: Vec>) { + async fn cache_filters(&mut self, filter_revs: Vec>) { for filter_rev in filter_revs { if let Some(field_rev) = self.delegate.get_field_rev(&filter_rev.field_id).await { let filter_type = FilterType::from(&field_rev); @@ -210,7 +305,7 @@ fn filter_row( filter_map: &FilterMap, result_by_row_id: &mut HashMap, field_rev_by_field_id: &HashMap>, -) -> (String, bool) { +) -> Option<(String, bool)> { // Create a filter result cache if it's not exist let filter_result = result_by_row_id .entry(row_rev.id.clone()) @@ -220,11 +315,6 @@ fn filter_row( for (field_id, field_rev) in field_rev_by_field_id { let filter_type = FilterType::from(field_rev); if !filter_map.has_filter(&filter_type) { - // tracing::trace!( - // "Can't find filter for filter type: {:?}. Current filters: {:?}", - // filter_type, - // filter_map - // ); continue; } @@ -232,18 +322,22 @@ fn filter_row( // if the visibility of the cell_rew is changed, which means the visibility of the // row is changed too. if let Some(is_visible) = filter_cell(&filter_type, field_rev, filter_map, cell_rev) { + let old_is_visible = filter_result.visible_by_filter_id.get(&filter_type).cloned(); filter_result.visible_by_filter_id.insert(filter_type, is_visible); - return (row_rev.id.clone(), is_visible); + return if old_is_visible != Some(is_visible) { + Some((row_rev.id.clone(), is_visible)) + } else { + None + }; } } - - (row_rev.id.clone(), true) + None } // Returns None if there is no change in this cell after applying the filter // Returns Some if the visibility of the cell is changed -#[tracing::instrument(level = "trace", skip_all)] +#[tracing::instrument(level = "trace", skip_all, fields(cell_content))] fn filter_cell( filter_id: &FilterType, field_rev: &Arc, @@ -260,8 +354,7 @@ fn filter_cell( } }, }; - tracing::trace!("filter cell: {:?}", any_cell_data); - + let cloned_cell_data = any_cell_data.data.clone(); let is_visible = match &filter_id.field_type { FieldType::RichText => filter_map.text_filter.get(filter_id).and_then(|filter| { Some( @@ -320,6 +413,28 @@ fn filter_cell( ) }), }?; - + tracing::Span::current().record( + "cell_content", + &format!("{} => {:?}", cloned_cell_data, is_visible.unwrap()).as_str(), + ); is_visible } + +#[derive(Serialize, Deserialize, Clone, Debug)] +enum FilterEvent { + FilterDidChanged, + RowDidChanged(String), +} + +impl ToString for FilterEvent { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl FromStr for FilterEvent { + type Err = serde_json::Error; + fn from_str(s: &str) -> Result { + serde_json::from_str(s) + } +} diff --git a/frontend/rust-lib/flowy-grid/src/services/filter/entities.rs b/frontend/rust-lib/flowy-grid/src/services/filter/entities.rs index 291c5fe544..bfc7e666bb 100644 --- a/frontend/rust-lib/flowy-grid/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-grid/src/services/filter/entities.rs @@ -1,24 +1,47 @@ -use crate::entities::{CreateFilterParams, DeleteFilterParams, FieldType, GridSettingChangesetParams}; +use crate::entities::{AlterFilterParams, DeleteFilterParams, FieldType, GridSettingChangesetParams, InsertedRowPB}; use grid_rev_model::{FieldRevision, FieldTypeRevision}; use std::sync::Arc; +#[derive(Debug)] pub struct FilterChangeset { pub(crate) insert_filter: Option, + pub(crate) update_filter: Option, pub(crate) delete_filter: Option, } +#[derive(Debug)] +pub struct UpdatedFilterType { + pub old: Option, + pub new: FilterType, +} + +impl UpdatedFilterType { + pub fn new(old: Option, new: FilterType) -> UpdatedFilterType { + Self { old, new } + } +} + impl FilterChangeset { - pub fn from_insert(filter_id: FilterType) -> Self { + pub fn from_insert(filter_type: FilterType) -> Self { Self { - insert_filter: Some(filter_id), + insert_filter: Some(filter_type), + update_filter: None, delete_filter: None, } } - pub fn from_delete(filter_id: FilterType) -> Self { + pub fn from_update(filter_type: UpdatedFilterType) -> Self { Self { insert_filter: None, - delete_filter: Some(filter_id), + update_filter: Some(filter_type), + delete_filter: None, + } + } + pub fn from_delete(filter_type: FilterType) -> Self { + Self { + insert_filter: None, + update_filter: None, + delete_filter: Some(filter_type), } } } @@ -27,7 +50,7 @@ impl std::convert::From<&GridSettingChangesetParams> for FilterChangeset { fn from(params: &GridSettingChangesetParams) -> Self { let insert_filter = params.insert_filter.as_ref().map(|insert_filter_params| FilterType { field_id: insert_filter_params.field_id.clone(), - field_type: insert_filter_params.field_type_rev.into(), + field_type: insert_filter_params.field_type.into(), }); let delete_filter = params @@ -36,6 +59,7 @@ impl std::convert::From<&GridSettingChangesetParams> for FilterChangeset { .map(|delete_filter_params| delete_filter_params.filter_type.clone()); FilterChangeset { insert_filter, + update_filter: None, delete_filter, } } @@ -62,9 +86,9 @@ impl std::convert::From<&Arc> for FilterType { } } -impl std::convert::From<&CreateFilterParams> for FilterType { - fn from(params: &CreateFilterParams) -> Self { - let field_type: FieldType = params.field_type_rev.into(); +impl std::convert::From<&AlterFilterParams> for FilterType { + fn from(params: &AlterFilterParams) -> Self { + let field_type: FieldType = params.field_type.into(); Self { field_id: params.field_id.clone(), field_type, @@ -82,6 +106,17 @@ impl std::convert::From<&DeleteFilterParams> for FilterType { pub struct FilterResultNotification { pub view_id: String, pub block_id: String, - pub visible_rows: Vec, + pub visible_rows: Vec, pub invisible_rows: Vec, } + +impl FilterResultNotification { + pub fn new(view_id: String, block_id: String) -> Self { + Self { + view_id, + block_id, + visible_rows: vec![], + invisible_rows: vec![], + } + } +} 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 5389ed6eea..8460671c57 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor.rs @@ -16,7 +16,7 @@ use crate::services::row::{GridBlock, RowRevisionBuilder}; use crate::services::view_editor::{GridViewChanged, GridViewManager}; use bytes::Bytes; use flowy_database::ConnectionPool; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_error::{FlowyError, FlowyResult}; use flowy_http_model::revision::Revision; use flowy_revision::{ RevisionCloudService, RevisionManager, RevisionMergeable, RevisionObjectDeserializer, RevisionObjectSerializer, @@ -108,11 +108,12 @@ impl GridRevisionEditor { /// * `type_option_data`: the updated type-option data. The `type-option` data might be empty /// if there is no type-option config for that field. For example, the `RichTextTypeOptionPB`. /// - pub async fn update_field_type_option( + pub async fn did_update_field_type_option( &self, - grid_id: &str, + _grid_id: &str, field_id: &str, type_option_data: Vec, + old_field_rev: Option>, ) -> FlowyResult<()> { let result = self.get_field_rev(field_id).await; if result.is_none() { @@ -120,13 +121,29 @@ impl GridRevisionEditor { return Ok(()); } let field_rev = result.unwrap(); - let changeset = FieldChangesetParams { - field_id: field_id.to_owned(), - grid_id: grid_id.to_owned(), - type_option_data: Some(type_option_data), - ..Default::default() - }; - let _ = self.update_field_rev(changeset, field_rev.ty.into()).await?; + let _ = self + .modify(|grid| { + let changeset = grid.modify_field(field_id, |field| { + let deserializer = TypeOptionJsonDeserializer(field_rev.ty.into()); + match deserializer.deserialize(type_option_data) { + Ok(json_str) => { + let field_type = field.ty; + field.insert_type_option_str(&field_type, json_str); + } + Err(err) => { + tracing::error!("Deserialize data to type option json failed: {}", err); + } + } + Ok(Some(())) + })?; + Ok(changeset) + }) + .await?; + + let _ = self + .view_manager + .did_update_view_field_type_option(field_id, old_field_rev) + .await?; let _ = self.notify_did_update_grid_field(field_id).await?; Ok(()) } @@ -161,21 +178,34 @@ impl GridRevisionEditor { pub async fn update_field(&self, params: FieldChangesetParams) -> FlowyResult<()> { let field_id = params.field_id.clone(); - let field_type: Option = self - .grid_pad - .read() - .await - .get_field_rev(params.field_id.as_str()) - .map(|(_, field_rev)| field_rev.ty.into()); - - match field_type { - None => Err(ErrorCode::FieldDoesNotExist.into()), - Some(field_type) => { - let _ = self.update_field_rev(params, field_type).await?; - let _ = self.notify_did_update_grid_field(&field_id).await?; - Ok(()) - } - } + let _ = self + .modify(|grid| { + let changeset = grid.modify_field(¶ms.field_id, |field| { + if let Some(name) = params.name { + field.name = name; + } + if let Some(desc) = params.desc { + field.desc = desc; + } + if let Some(field_type) = params.field_type { + field.ty = field_type; + } + if let Some(frozen) = params.frozen { + field.frozen = frozen; + } + if let Some(visibility) = params.visibility { + field.visibility = visibility; + } + if let Some(width) = params.width { + field.width = width; + } + Ok(Some(())) + })?; + Ok(changeset) + }) + .await?; + let _ = self.notify_did_update_grid_field(&field_id).await?; + Ok(()) } pub async fn modify_field_rev(&self, field_id: &str, f: F) -> FlowyResult<()> @@ -183,6 +213,7 @@ impl GridRevisionEditor { F: for<'a> FnOnce(&'a mut FieldRevision) -> FlowyResult>, { let mut is_changed = false; + let old_field_rev = self.get_field_rev(field_id).await; let _ = self .modify(|grid| { let changeset = grid.modify_field(field_id, |field_rev| { @@ -194,7 +225,11 @@ impl GridRevisionEditor { .await?; if is_changed { - match self.view_manager.did_update_view_field_type_option(field_id).await { + match self + .view_manager + .did_update_view_field_type_option(field_id, old_field_rev) + .await + { Ok(_) => {} Err(e) => tracing::error!("View manager update field failed: {:?}", e), } @@ -295,66 +330,71 @@ impl GridRevisionEditor { Ok(field_revs) } - /// Apply the changeset to field. Including the `name`,`field_type`,`width`,`visibility`,and `type_option_data`. - /// Do nothing if the passed-in params doesn't carry any changes. - /// - /// # Arguments - /// - /// * `params`: contains the changesets that is going to applied to the field. - /// Ignore the change if one of the properties is None. - /// - /// * `field_type`: is used by `TypeOptionJsonDeserializer` to deserialize the type_option_data - /// - #[tracing::instrument(level = "debug", skip_all, err)] - async fn update_field_rev(&self, params: FieldChangesetParams, field_type: FieldType) -> FlowyResult<()> { - let mut is_type_option_changed = false; - let _ = self - .modify(|grid| { - let changeset = grid.modify_field(¶ms.field_id, |field| { - if let Some(name) = params.name { - field.name = name; - } - if let Some(desc) = params.desc { - field.desc = desc; - } - if let Some(field_type) = params.field_type { - field.ty = field_type; - } - if let Some(frozen) = params.frozen { - field.frozen = frozen; - } - if let Some(visibility) = params.visibility { - field.visibility = visibility; - } - if let Some(width) = params.width { - field.width = width; - } - if let Some(type_option_data) = params.type_option_data { - let deserializer = TypeOptionJsonDeserializer(field_type); - is_type_option_changed = true; - match deserializer.deserialize(type_option_data) { - Ok(json_str) => { - let field_type = field.ty; - field.insert_type_option_str(&field_type, json_str); - } - Err(err) => { - tracing::error!("Deserialize data to type option json failed: {}", err); - } - } - } - Ok(Some(())) - })?; - Ok(changeset) - }) - .await?; - if is_type_option_changed { - let _ = self - .view_manager - .did_update_view_field_type_option(¶ms.field_id) - .await?; - } - Ok(()) - } + // /// Apply the changeset to field. Including the `name`,`field_type`,`width`,`visibility`,and `type_option_data`. + // /// Do nothing if the passed-in params doesn't carry any changes. + // /// + // /// # Arguments + // /// + // /// * `params`: contains the changesets that is going to applied to the field. + // /// Ignore the change if one of the properties is None. + // /// + // /// * `field_type`: is used by `TypeOptionJsonDeserializer` to deserialize the type_option_data + // /// + // #[tracing::instrument(level = "debug", skip_all, err)] + // async fn did_update_field_rev( + // &self, + // params: FieldChangesetParams, + // field_type: FieldType, + // old_field_rev: Option>, + // ) -> FlowyResult<()> { + // let mut is_type_option_changed = false; + // let _ = self + // .modify(|grid| { + // let changeset = grid.modify_field(¶ms.field_id, |field| { + // if let Some(name) = params.name { + // field.name = name; + // } + // if let Some(desc) = params.desc { + // field.desc = desc; + // } + // if let Some(field_type) = params.field_type { + // field.ty = field_type; + // } + // if let Some(frozen) = params.frozen { + // field.frozen = frozen; + // } + // if let Some(visibility) = params.visibility { + // field.visibility = visibility; + // } + // if let Some(width) = params.width { + // field.width = width; + // } + // if let Some(type_option_data) = params.type_option_data { + // let deserializer = TypeOptionJsonDeserializer(field_type); + // is_type_option_changed = true; + // match deserializer.deserialize(type_option_data) { + // Ok(json_str) => { + // let field_type = field.ty; + // field.insert_type_option_str(&field_type, json_str); + // } + // Err(err) => { + // tracing::error!("Deserialize data to type option json failed: {}", err); + // } + // } + // } + // Ok(Some(())) + // })?; + // Ok(changeset) + // }) + // .await?; + // if is_type_option_changed { + // let _ = self + // .view_manager + // .did_update_view_field_type_option(¶ms.field_id, old_field_rev) + // .await?; + // } + // Ok(()) + // } pub async fn create_block(&self, block_meta_rev: GridBlockMetaRevision) -> FlowyResult<()> { let _ = self @@ -427,7 +467,7 @@ impl GridRevisionEditor { pub async fn get_row_rev(&self, row_id: &str) -> FlowyResult>> { match self.block_manager.get_row_rev(row_id).await? { None => Ok(None), - Some(row_rev) => Ok(Some(row_rev)), + Some((_, row_rev)) => Ok(Some(row_rev)), } } @@ -440,8 +480,8 @@ impl GridRevisionEditor { Ok(()) } - pub async fn subscribe_view_changed(&self) -> broadcast::Receiver { - self.view_manager.subscribe_view_changed().await + pub async fn subscribe_view_changed(&self, view_id: &str) -> FlowyResult> { + self.view_manager.subscribe_view_changed(view_id).await } pub async fn duplicate_row(&self, _row_id: &str) -> FlowyResult<()> { @@ -460,16 +500,15 @@ impl GridRevisionEditor { async fn decode_any_cell_data(&self, params: &CellPathParams) -> Option<(FieldType, CellBytes)> { let field_rev = self.get_field_rev(¶ms.field_id).await?; - let row_rev = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??; + let (_, row_rev) = self.block_manager.get_row_rev(¶ms.row_id).await.ok()??; let cell_rev = row_rev.cells.get(¶ms.field_id)?.clone(); Some(decode_any_cell_data(cell_rev.data, &field_rev)) } pub async fn get_cell_rev(&self, row_id: &str, field_id: &str) -> FlowyResult> { - let row_rev = self.block_manager.get_row_rev(row_id).await?; - match row_rev { + match self.block_manager.get_row_rev(row_id).await? { None => Ok(None), - Some(row_rev) => { + Some((_, row_rev)) => { let cell_rev = row_rev.cells.get(field_id).cloned(); Ok(cell_rev) } @@ -601,8 +640,8 @@ impl GridRevisionEditor { self.view_manager.delete_group(params).await } - pub async fn create_filter(&self, params: CreateFilterParams) -> FlowyResult<()> { - let _ = self.view_manager.insert_or_update_filter(params).await?; + pub async fn create_or_update_filter(&self, params: AlterFilterParams) -> FlowyResult<()> { + let _ = self.view_manager.create_or_update_filter(params).await?; Ok(()) } @@ -620,7 +659,7 @@ impl GridRevisionEditor { match self.block_manager.get_row_rev(&from_row_id).await? { None => tracing::warn!("Move row failed, can not find the row:{}", from_row_id), - Some(row_rev) => { + Some((_, row_rev)) => { match ( self.block_manager.index_of_row(&from_row_id).await, self.block_manager.index_of_row(&to_row_id).await, @@ -650,7 +689,7 @@ impl GridRevisionEditor { match self.block_manager.get_row_rev(&from_row_id).await? { None => tracing::warn!("Move row failed, can not find the row:{}", from_row_id), - Some(row_rev) => { + Some((_, row_rev)) => { let block_manager = self.block_manager.clone(); self.view_manager .move_group_row(row_rev, to_group_id, to_row_id.clone(), |row_changeset| { @@ -740,7 +779,7 @@ impl GridRevisionEditor { } #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn load_groups(&self) -> FlowyResult { + pub async fn load_groups(&self) -> FlowyResult { self.view_manager.load_groups().await } diff --git a/frontend/rust-lib/flowy-grid/src/services/grid_editor_trait_impl.rs b/frontend/rust-lib/flowy-grid/src/services/grid_editor_trait_impl.rs index fd257dbb7d..391b0447b1 100644 --- a/frontend/rust-lib/flowy-grid/src/services/grid_editor_trait_impl.rs +++ b/frontend/rust-lib/flowy-grid/src/services/grid_editor_trait_impl.rs @@ -40,12 +40,12 @@ impl GridViewEditorDelegate for GridViewEditorDelegateImpl { to_future(async move { block_manager.index_of_row(&row_id).await }) } - fn get_row_rev(&self, row_id: &str) -> Fut>> { + fn get_row_rev(&self, row_id: &str) -> Fut)>> { let block_manager = self.block_manager.clone(); let row_id = row_id.to_owned(); to_future(async move { match block_manager.get_row_rev(&row_id).await { - Ok(row_rev) => row_rev, + Ok(indexed_row) => indexed_row, Err(_) => None, } }) diff --git a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs index 16ee630cc7..8d21427a2d 100644 --- a/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs +++ b/frontend/rust-lib/flowy-grid/src/services/setting/setting_builder.rs @@ -1,4 +1,4 @@ -use crate::entities::{CreateFilterParams, DeleteFilterParams, GridLayout, GridSettingChangesetParams}; +use crate::entities::{AlterFilterParams, DeleteFilterParams, GridLayout, GridSettingChangesetParams}; pub struct GridSettingChangesetBuilder { params: GridSettingChangesetParams, @@ -17,7 +17,7 @@ impl GridSettingChangesetBuilder { Self { params } } - pub fn insert_filter(mut self, params: CreateFilterParams) -> Self { + pub fn insert_filter(mut self, params: AlterFilterParams) -> Self { self.params.insert_filter = Some(params); self } 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 93dcae3f09..122a548f01 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 @@ -1,7 +1,6 @@ use crate::dart_notification::{send_dart_notification, GridDartNotification}; use crate::entities::*; -use crate::services::filter::{FilterChangeset, FilterController, FilterTaskHandler, FilterType}; - +use crate::services::filter::{FilterChangeset, FilterController, FilterTaskHandler, FilterType, UpdatedFilterType}; use crate::services::group::{ default_group_configuration, find_group_field, make_group_controller, Group, GroupConfigurationReader, GroupController, MoveGroupRowContext, @@ -9,6 +8,7 @@ use crate::services::group::{ use crate::services::row::GridBlock; use crate::services::view_editor::changed_notifier::GridViewChangedNotifier; use crate::services::view_editor::trait_impl::*; +use crate::services::view_editor::GridViewChangedReceiverRunner; use flowy_database::ConnectionPool; use flowy_error::FlowyResult; use flowy_revision::RevisionManager; @@ -20,7 +20,7 @@ use lib_infra::ref_map::RefCountValue; use nanoid::nanoid; use std::future::Future; use std::sync::Arc; -use tokio::sync::RwLock; +use tokio::sync::{broadcast, RwLock}; pub trait GridViewEditorDelegate: Send + Sync + 'static { /// If the field_ids is None, then it will return all the field revisions @@ -28,7 +28,7 @@ pub trait GridViewEditorDelegate: Send + Sync + 'static { fn get_field_rev(&self, field_id: &str) -> Fut>>; fn index_of_row(&self, row_id: &str) -> Fut>; - fn get_row_rev(&self, row_id: &str) -> Fut>>; + fn get_row_rev(&self, row_id: &str) -> Fut)>>; fn get_row_revs(&self) -> Fut>>; fn get_blocks(&self) -> Fut>; @@ -43,6 +43,7 @@ pub struct GridViewRevisionEditor { delegate: Arc, group_controller: Arc>>, filter_controller: Arc>, + pub notifier: GridViewChangedNotifier, } impl GridViewRevisionEditor { @@ -52,9 +53,10 @@ impl GridViewRevisionEditor { token: &str, view_id: String, delegate: Arc, - notifier: GridViewChangedNotifier, mut rev_manager: RevisionManager>, ) -> FlowyResult { + let (notifier, _) = broadcast::channel(100); + tokio::spawn(GridViewChangedReceiverRunner(Some(notifier.subscribe())).run()); let cloud = Arc::new(GridViewRevisionCloudService { token: token.to_owned(), }); @@ -81,6 +83,7 @@ impl GridViewRevisionEditor { delegate, group_controller, filter_controller, + notifier, }) } @@ -166,6 +169,12 @@ impl GridViewRevisionEditor { self.notify_did_update_group_rows(changeset).await; } } + + let filter_controller = self.filter_controller.clone(); + let row_id = row_rev.id.clone(); + tokio::spawn(async move { + filter_controller.write().await.did_receive_row_changed(&row_id).await; + }); } pub async fn move_view_group_row( @@ -291,31 +300,52 @@ impl GridViewRevisionEditor { } #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn insert_view_filter(&self, params: CreateFilterParams) -> FlowyResult<()> { + pub async fn insert_view_filter(&self, params: AlterFilterParams) -> FlowyResult<()> { let filter_type = FilterType::from(¶ms); + let is_exist = params.filter_id.is_some(); + let filter_id = match params.filter_id { + None => gen_grid_filter_id(), + Some(filter_id) => filter_id, + }; let filter_rev = FilterRevision { - id: gen_grid_filter_id(), + id: filter_id.clone(), field_id: params.field_id.clone(), - field_type_rev: params.field_type_rev, + field_type: params.field_type, condition: params.condition, content: params.content, }; - let filter_pb = FilterPB::from(&filter_rev); - let _ = self - .modify(|pad| { - let changeset = pad.insert_filter(¶ms.field_id, ¶ms.field_type_rev, filter_rev)?; + let mut filter_controller = self.filter_controller.write().await; + let changeset = if is_exist { + let old_filter_type = self + .delegate + .get_field_rev(¶ms.field_id) + .await + .map(|field| FilterType::from(&field)); + self.modify(|pad| { + let changeset = pad.update_filter(¶ms.field_id, filter_rev)?; Ok(changeset) }) .await?; + filter_controller + .did_receive_filter_changed(FilterChangeset::from_update(UpdatedFilterType::new( + old_filter_type, + filter_type, + ))) + .await + } else { + self.modify(|pad| { + let changeset = pad.insert_filter(¶ms.field_id, filter_rev)?; + Ok(changeset) + }) + .await?; + filter_controller + .did_receive_filter_changed(FilterChangeset::from_insert(filter_type)) + .await + }; - self.filter_controller - .write() - .await - .apply_changeset(FilterChangeset::from_insert(filter_type)) - .await; - - let changeset = FilterChangesetNotificationPB::from_insert(&self.view_id, vec![filter_pb]); - self.notify_did_update_filter(changeset).await; + if let Some(changeset) = changeset { + self.notify_did_update_filter(changeset).await; + } Ok(()) } @@ -323,12 +353,12 @@ impl GridViewRevisionEditor { pub async fn delete_view_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> { let filter_type = params.filter_type; let field_type_rev = filter_type.field_type_rev(); - let filters = self - .get_view_filters(&filter_type) + let changeset = self + .filter_controller + .write() .await - .into_iter() - .map(|filter| FilterPB::from(filter.as_ref())) - .collect(); + .did_receive_filter_changed(FilterChangeset::from_delete(filter_type.clone())) + .await; let _ = self .modify(|pad| { let changeset = pad.delete_filter(¶ms.filter_id, &filter_type.field_id, &field_type_rev)?; @@ -336,27 +366,32 @@ impl GridViewRevisionEditor { }) .await?; - self.filter_controller - .write() - .await - .apply_changeset(FilterChangeset::from_delete(filter_type)) - .await; - - let changeset = FilterChangesetNotificationPB::from_delete(&self.view_id, filters); - self.notify_did_update_filter(changeset).await; + if changeset.is_some() { + self.notify_did_update_filter(changeset.unwrap()).await; + } Ok(()) } #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> { + pub async fn did_update_view_field_type_option( + &self, + field_id: &str, + old_field_rev: Option>, + ) -> FlowyResult<()> { if let Some(field_rev) = self.delegate.get_field_rev(field_id).await { - let filter_type = FilterType::from(&field_rev); - let filter_changeset = FilterChangeset::from_insert(filter_type); - self.filter_controller + let old = old_field_rev.map(|old_field_rev| FilterType::from(&old_field_rev)); + let new = FilterType::from(&field_rev); + let filter_type = UpdatedFilterType::new(old, new); + let filter_changeset = FilterChangeset::from_update(filter_type); + if let Some(changeset) = self + .filter_controller .write() .await - .apply_changeset(filter_changeset) - .await; + .did_receive_filter_changed(filter_changeset) + .await + { + self.notify_did_update_filter(changeset).await; + } } Ok(()) } 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 d6e65a4963..5ba8f37c1a 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 @@ -1,6 +1,6 @@ use crate::entities::{ - CreateFilterParams, CreateRowParams, DeleteFilterParams, DeleteGroupParams, GridSettingPB, InsertGroupParams, - MoveGroupParams, RepeatedGridGroupPB, RowPB, + AlterFilterParams, CreateRowParams, DeleteFilterParams, DeleteGroupParams, GridSettingPB, InsertGroupParams, + MoveGroupParams, RepeatedGroupPB, RowPB, }; use crate::manager::GridUser; use crate::services::filter::FilterType; @@ -13,7 +13,7 @@ use flowy_error::FlowyResult; use flowy_revision::{ RevisionManager, RevisionPersistence, RevisionPersistenceConfiguration, SQLiteRevisionSnapshotPersistence, }; -use grid_rev_model::{FilterRevision, RowChangeset, RowRevision}; +use grid_rev_model::{FieldRevision, FilterRevision, RowChangeset, RowRevision}; use lib_infra::future::Fut; use lib_infra::ref_map::RefCountHashMap; use std::sync::Arc; @@ -24,7 +24,6 @@ pub struct GridViewManager { user: Arc, delegate: Arc, view_editors: RwLock>>, - pub notifier: broadcast::Sender, } impl GridViewManager { @@ -33,15 +32,12 @@ impl GridViewManager { user: Arc, delegate: Arc, ) -> FlowyResult { - let (notifier, _) = broadcast::channel(100); - tokio::spawn(GridViewChangedReceiverRunner(Some(notifier.subscribe())).run()); let view_editors = RwLock::new(RefCountHashMap::default()); Ok(Self { grid_id, user, delegate, view_editors, - notifier, }) } @@ -49,8 +45,8 @@ impl GridViewManager { self.view_editors.write().await.remove(view_id); } - pub async fn subscribe_view_changed(&self) -> broadcast::Receiver { - self.notifier.subscribe() + pub async fn subscribe_view_changed(&self, view_id: &str) -> FlowyResult> { + Ok(self.get_view_editor(view_id).await?.notifier.subscribe()) } pub async fn filter_rows(&self, block_id: &str, rows: Vec>) -> FlowyResult>> { @@ -85,7 +81,7 @@ impl GridViewManager { None => { tracing::warn!("Can not find the row in grid view"); } - Some(row_rev) => { + Some((_, row_rev)) => { for view_editor in self.view_editors.read().await.values() { view_editor.did_update_view_cell(&row_rev).await; } @@ -120,7 +116,7 @@ impl GridViewManager { Ok(view_editor.get_view_filters(filter_id).await) } - pub async fn insert_or_update_filter(&self, params: CreateFilterParams) -> FlowyResult<()> { + pub async fn create_or_update_filter(&self, params: AlterFilterParams) -> FlowyResult<()> { let view_editor = self.get_default_view_editor().await?; view_editor.insert_view_filter(params).await } @@ -130,10 +126,10 @@ impl GridViewManager { view_editor.delete_view_filter(params).await } - pub async fn load_groups(&self) -> FlowyResult { + pub async fn load_groups(&self) -> FlowyResult { let view_editor = self.get_default_view_editor().await?; let groups = view_editor.load_view_groups().await?; - Ok(RepeatedGridGroupPB { items: groups }) + Ok(RepeatedGroupPB { items: groups }) } pub async fn insert_or_update_group(&self, params: InsertGroupParams) -> FlowyResult<()> { @@ -187,13 +183,19 @@ impl GridViewManager { /// * `field_id`: the id of the field in current view /// #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn did_update_view_field_type_option(&self, field_id: &str) -> FlowyResult<()> { + pub async fn did_update_view_field_type_option( + &self, + field_id: &str, + old_field_rev: Option>, + ) -> FlowyResult<()> { let view_editor = self.get_default_view_editor().await?; if view_editor.group_id().await == field_id { let _ = view_editor.group_by_view_field(field_id).await?; } - let _ = view_editor.did_update_view_field_type_option(field_id).await?; + let _ = view_editor + .did_update_view_field_type_option(field_id, old_field_rev) + .await?; Ok(()) } @@ -219,15 +221,7 @@ impl GridViewManager { let token = self.user.token()?; let view_id = view_id.to_owned(); - GridViewRevisionEditor::new( - &user_id, - &token, - view_id, - self.delegate.clone(), - self.notifier.clone(), - rev_manager, - ) - .await + GridViewRevisionEditor::new(&user_id, &token, view_id, self.delegate.clone(), rev_manager).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 35b4e1df1a..c4311f6f39 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 @@ -1,4 +1,4 @@ -use crate::entities::{FilterPB, GridGroupConfigurationPB, GridLayout, GridLayoutPB, GridSettingPB}; +use crate::entities::{GridLayout, GridLayoutPB, GridSettingPB}; use crate::services::filter::{FilterDelegate, FilterType}; use crate::services::group::{GroupConfigurationReader, GroupConfigurationWriter}; use crate::services::row::GridBlock; @@ -12,7 +12,7 @@ use flowy_revision::{ }; use flowy_sync::client_grid::{GridViewRevisionChangeset, GridViewRevisionPad}; use flowy_sync::util::make_operations_from_revisions; -use grid_rev_model::{FieldRevision, FieldTypeRevision, FilterRevision, GroupConfigurationRevision}; +use grid_rev_model::{FieldRevision, FieldTypeRevision, FilterRevision, GroupConfigurationRevision, RowRevision}; use lib_infra::future::{to_future, Fut, FutureResult}; use lib_ot::core::EmptyAttributes; use std::sync::Arc; @@ -118,22 +118,12 @@ 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) - .into_iter() - .map(|filter| FilterPB::from(filter.as_ref())) - .collect::>(); - - let group_configurations = view_pad - .get_groups_by_field_revs(field_revs) - .into_iter() - .map(|group| GridGroupConfigurationPB::from(group.as_ref())) - .collect::>(); - + let filter_configurations = view_pad.get_all_filters(field_revs); + let group_configurations = view_pad.get_groups_by_field_revs(field_revs); GridSettingPB { layouts: GridLayoutPB::all(), layout_type, - filter_configurations: filter_configurations.into(), + filters: filter_configurations.into(), group_configurations: group_configurations.into(), } } @@ -144,11 +134,17 @@ pub(crate) struct GridViewFilterDelegateImpl { } impl FilterDelegate for GridViewFilterDelegateImpl { - fn get_filter_rev(&self, filter_id: FilterType) -> Fut>> { + fn get_filter_rev(&self, filter_id: FilterType) -> Fut>> { let pad = self.view_revision_pad.clone(); to_future(async move { let field_type_rev: FieldTypeRevision = filter_id.field_type.into(); - pad.read().await.get_filters(&filter_id.field_id, &field_type_rev) + let mut filters = pad.read().await.get_filters(&filter_id.field_id, &field_type_rev); + if filters.is_empty() { + None + } else { + debug_assert_eq!(filters.len(), 1); + filters.pop() + } }) } @@ -163,4 +159,8 @@ impl FilterDelegate for GridViewFilterDelegateImpl { fn get_blocks(&self) -> Fut> { self.editor_delegate.get_blocks() } + + fn get_row_rev(&self, row_id: &str) -> Fut)>> { + self.editor_delegate.get_row_rev(row_id) + } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs index c187549ff8..aff7c22489 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/script.rs @@ -13,6 +13,10 @@ pub enum FieldScript { field_rev: FieldRevision, }, AssertFieldCount(usize), + AssertFieldFrozen { + field_index: usize, + frozen: bool, + }, AssertFieldTypeOptionEqual { field_index: usize, expected_type_option_data: String, @@ -70,6 +74,11 @@ impl GridFieldTest { FieldScript::AssertFieldCount(count) => { assert_eq!(self.editor.get_field_revs(None).await.unwrap().len(), count); } + FieldScript::AssertFieldFrozen { field_index, frozen } => { + let field_revs = self.editor.get_field_revs(None).await.unwrap(); + let field_rev = field_revs[field_index].as_ref(); + assert_eq!(field_rev.frozen, frozen); + } FieldScript::AssertFieldTypeOptionEqual { field_index, expected_type_option_data, diff --git a/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs index 71de319c3a..592bdfa253 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/field_test/test.rs @@ -4,7 +4,6 @@ use crate::grid::field_test::util::*; use flowy_grid::entities::FieldChangesetParams; use flowy_grid::services::field::selection_type_option::SelectOptionPB; use flowy_grid::services::field::SingleSelectTypeOptionPB; -use grid_rev_model::TypeOptionDataSerializer; #[tokio::test] async fn grid_create_field() { @@ -86,7 +85,6 @@ async fn grid_update_field() { grid_id: test.grid_id(), frozen: Some(true), width: Some(1000), - type_option_data: Some(single_select_type_option.protobuf_bytes().to_vec()), ..Default::default() }; @@ -98,9 +96,9 @@ async fn grid_update_field() { let scripts = vec![ UpdateField { changeset }, - AssertFieldTypeOptionEqual { + AssertFieldFrozen { field_index: create_field_index, - expected_type_option_data: expected_field_rev.get_type_option_str(expected_field_rev.ty).unwrap(), + frozen: true, }, ]; test.run_scripts(scripts).await; diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs index edd0fd351a..dbf85d9fa2 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/script.rs @@ -6,22 +6,36 @@ use std::time::Duration; use bytes::Bytes; use futures::TryFutureExt; -use flowy_grid::entities::{CreateFilterParams, CreateFilterPayloadPB, DeleteFilterParams, GridLayout, GridSettingChangesetParams, GridSettingPB, RowPB, TextFilterCondition, FieldType, NumberFilterCondition, CheckboxFilterCondition, DateFilterCondition, DateFilterContent, SelectOptionCondition, TextFilterPB, NumberFilterPB, CheckboxFilterPB, DateFilterPB, SelectOptionFilterPB}; -use flowy_grid::services::field::SelectOptionIds; +use flowy_grid::entities::{AlterFilterParams, AlterFilterPayloadPB, DeleteFilterParams, GridLayout, GridSettingChangesetParams, GridSettingPB, RowPB, TextFilterCondition, FieldType, NumberFilterCondition, CheckboxFilterCondition, DateFilterCondition, DateFilterContent, SelectOptionCondition, TextFilterPB, NumberFilterPB, CheckboxFilterPB, DateFilterPB, SelectOptionFilterPB, CellChangesetPB, FilterPB}; +use flowy_grid::services::field::{SelectOptionCellChangeset, SelectOptionIds}; use flowy_grid::services::setting::GridSettingChangesetBuilder; use grid_rev_model::{FieldRevision, FieldTypeRevision}; +use flowy_grid::services::cell::insert_select_option_cell; use flowy_grid::services::filter::FilterType; use flowy_grid::services::view_editor::GridViewChanged; use crate::grid::grid_editor::GridEditorTest; pub enum FilterScript { + UpdateTextCell { + row_index: usize, + text: String, + }, + UpdateSingleSelectCell { + row_index: usize, + option_id: String, + }, InsertFilter { - payload: CreateFilterPayloadPB, + payload: AlterFilterPayloadPB, }, CreateTextFilter { condition: TextFilterCondition, content: String, }, + UpdateTextFilter { + filter: FilterPB, + condition: TextFilterCondition, + content: String, + }, CreateNumberFilter { condition: NumberFilterCondition, content: String, @@ -81,6 +95,10 @@ impl GridFilterTest { } } + pub async fn get_all_filters(&self) -> Vec { + self.editor.get_all_filters().await.unwrap() + } + pub async fn run_scripts(&mut self, scripts: Vec) { for script in scripts { self.run_script(script).await; @@ -89,6 +107,13 @@ impl GridFilterTest { pub async fn run_script(&mut self, script: FilterScript) { match script { + FilterScript::UpdateTextCell { row_index, text} => { + self.update_text_cell(row_index, &text).await; + } + + FilterScript::UpdateSingleSelectCell { row_index, option_id} => { + self.update_single_select_cell(row_index, &option_id).await; + } FilterScript::InsertFilter { payload } => { self.insert_filter(payload).await; } @@ -100,9 +125,19 @@ impl GridFilterTest { content }; let payload = - CreateFilterPayloadPB::new(field_rev, text_filter); + AlterFilterPayloadPB::new(field_rev, text_filter); self.insert_filter(payload).await; } + FilterScript::UpdateTextFilter { filter, condition, content} => { + let params = AlterFilterParams { + field_id: filter.field_id, + filter_id: Some(filter.id), + field_type: filter.field_type.into(), + condition: condition as u8, + content + }; + self.editor.create_or_update_filter(params).await.unwrap(); + } FilterScript::CreateNumberFilter {condition, content} => { let field_rev = self.get_field_rev(FieldType::Number); let number_filter = NumberFilterPB { @@ -110,7 +145,7 @@ impl GridFilterTest { content }; let payload = - CreateFilterPayloadPB::new(field_rev, number_filter); + AlterFilterPayloadPB::new(field_rev, number_filter); self.insert_filter(payload).await; } FilterScript::CreateCheckboxFilter {condition} => { @@ -119,7 +154,7 @@ impl GridFilterTest { condition }; let payload = - CreateFilterPayloadPB::new(field_rev, checkbox_filter); + AlterFilterPayloadPB::new(field_rev, checkbox_filter); self.insert_filter(payload).await; } FilterScript::CreateDateFilter { condition, start, end, timestamp} => { @@ -132,21 +167,21 @@ impl GridFilterTest { }; let payload = - CreateFilterPayloadPB::new(field_rev, date_filter); + AlterFilterPayloadPB::new(field_rev, date_filter); self.insert_filter(payload).await; } FilterScript::CreateMultiSelectFilter { condition, option_ids} => { let field_rev = self.get_field_rev(FieldType::MultiSelect); let filter = SelectOptionFilterPB { condition, option_ids }; let payload = - CreateFilterPayloadPB::new(field_rev, filter); + AlterFilterPayloadPB::new(field_rev, filter); self.insert_filter(payload).await; } FilterScript::CreateSingleSelectFilter { condition, option_ids} => { let field_rev = self.get_field_rev(FieldType::SingleSelect); let filter = SelectOptionFilterPB { condition, option_ids }; let payload = - CreateFilterPayloadPB::new(field_rev, filter); + AlterFilterPayloadPB::new(field_rev, filter); self.insert_filter(payload).await; } FilterScript::AssertFilterCount { count } => { @@ -168,12 +203,16 @@ impl GridFilterTest { assert_eq!(expected_setting, setting); } FilterScript::AssertFilterChanged { visible_row_len, hide_row_len} => { - let mut receiver = self.editor.subscribe_view_changed().await; - let changed = receiver.recv().await.unwrap(); - match changed { GridViewChanged::DidReceiveFilterResult(changed) => { - assert_eq!(changed.visible_rows.len(), visible_row_len); - assert_eq!(changed.invisible_rows.len(), hide_row_len); - } } + let mut receiver = self.editor.subscribe_view_changed(&self.grid_id).await.unwrap(); + match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await { + Ok(changed) => match changed.unwrap() { GridViewChanged::DidReceiveFilterResult(changed) => { + assert_eq!(changed.visible_rows.len(), visible_row_len); + assert_eq!(changed.invisible_rows.len(), hide_row_len); + } }, + Err(e) => { + panic!("Process task timeout: {:?}", e); + } + } } FilterScript::AssertNumberOfVisibleRows { expected } => { // @@ -187,9 +226,42 @@ impl GridFilterTest { } } - async fn insert_filter(&self, payload: CreateFilterPayloadPB) { - let params: CreateFilterParams = payload.try_into().unwrap(); - let _ = self.editor.create_filter(params).await.unwrap(); + async fn insert_filter(&self, payload: AlterFilterPayloadPB) { + let params: AlterFilterParams = payload.try_into().unwrap(); + let _ = self.editor.create_or_update_filter(params).await.unwrap(); + } + + async fn update_text_cell(&self, row_index: usize, content: &str) { + let row_rev = &self.inner.row_revs[row_index]; + let field_rev = self.inner.field_revs.iter().find(|field_rev| { + let field_type: FieldType = field_rev.ty.into(); + field_type == FieldType::RichText + }).unwrap(); + let changeset =CellChangesetPB { + grid_id: self.grid_id.clone(), + row_id: row_rev.id.clone(), + field_id: field_rev.id.clone(), + content: content.to_string(), + }; + self.editor.update_cell_with_changeset(changeset).await.unwrap(); + + } + async fn update_single_select_cell(&self, row_index: usize, option_id: &str) { + let row_rev = &self.inner.row_revs[row_index]; + let field_rev = self.inner.field_revs.iter().find(|field_rev| { + let field_type: FieldType = field_rev.ty.into(); + field_type == FieldType::SingleSelect + }).unwrap(); + + let content = SelectOptionCellChangeset::from_insert_option_id(&option_id).to_str(); + let changeset =CellChangesetPB { + grid_id: self.grid_id.clone(), + row_id: row_rev.id.clone(), + field_id: field_rev.id.clone(), + content, + }; + self.editor.update_cell_with_changeset(changeset).await.unwrap(); + } } diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs index 2a7bf18f8a..db17def286 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/select_option_filter_test.rs @@ -82,3 +82,36 @@ async fn grid_filter_single_select_is_test() { ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn grid_filter_single_select_is_test2() { + let mut test = GridFilterTest::new().await; + let mut options = test.get_single_select_type_option(); + let option = options.remove(0); + let scripts = vec![ + CreateSingleSelectFilter { + condition: SelectOptionCondition::OptionIs, + option_ids: vec![option.id.clone()], + }, + AssertNumberOfVisibleRows { expected: 2 }, + UpdateSingleSelectCell { + row_index: 1, + option_id: option.id.clone(), + }, + AssertFilterChanged { + visible_row_len: 1, + hide_row_len: 0, + }, + AssertNumberOfVisibleRows { expected: 3 }, + UpdateSingleSelectCell { + row_index: 1, + option_id: "".to_string(), + }, + // AssertFilterChanged { + // visible_row_len: 0, + // hide_row_len: 1, + // }, + // AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs index 5a5e2282ab..1167a7e6e6 100644 --- a/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs +++ b/frontend/rust-lib/flowy-grid/tests/grid/filter_test/text_filter_test.rs @@ -1,6 +1,6 @@ use crate::grid::filter_test::script::FilterScript::*; use crate::grid::filter_test::script::*; -use flowy_grid::entities::{CreateFilterPayloadPB, FieldType, TextFilterCondition, TextFilterPB}; +use flowy_grid::entities::{AlterFilterPayloadPB, FieldType, TextFilterCondition, TextFilterPB}; use flowy_grid::services::filter::FilterType; #[tokio::test] @@ -69,6 +69,45 @@ async fn grid_filter_contain_text_test() { test.run_scripts(scripts).await; } +#[tokio::test] +async fn grid_filter_contain_text_test2() { + let mut test = GridFilterTest::new().await; + let scripts = vec![ + CreateTextFilter { + condition: TextFilterCondition::Contains, + content: "A".to_string(), + }, + AssertFilterChanged { + visible_row_len: 3, + hide_row_len: 2, + }, + UpdateTextCell { + row_index: 1, + text: "ABC".to_string(), + }, + AssertFilterChanged { + visible_row_len: 1, + hide_row_len: 0, + }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_does_not_contain_text_test() { + let mut test = GridFilterTest::new().await; + let scripts = vec![ + CreateTextFilter { + condition: TextFilterCondition::DoesNotContain, + content: "AB".to_string(), + }, + AssertFilterChanged { + visible_row_len: 5, + hide_row_len: 0, + }, + ]; + test.run_scripts(scripts).await; +} #[tokio::test] async fn grid_filter_start_with_text_test() { let mut test = GridFilterTest::new().await; @@ -98,6 +137,32 @@ async fn grid_filter_ends_with_text_test() { test.run_scripts(scripts).await; } +#[tokio::test] +async fn grid_update_text_filter_test() { + let mut test = GridFilterTest::new().await; + let scripts = vec![ + CreateTextFilter { + condition: TextFilterCondition::EndsWith, + content: "A".to_string(), + }, + AssertNumberOfVisibleRows { expected: 2 }, + ]; + test.run_scripts(scripts).await; + + // Update the filter + let filter = test.get_all_filters().await.pop().unwrap(); + let scripts = vec![ + UpdateTextFilter { + filter, + condition: TextFilterCondition::Is, + content: "A".to_string(), + }, + AssertNumberOfVisibleRows { expected: 1 }, + AssertFilterCount { count: 1 }, + ]; + test.run_scripts(scripts).await; +} + #[tokio::test] async fn grid_filter_delete_test() { let mut test = GridFilterTest::new().await; @@ -106,7 +171,7 @@ async fn grid_filter_delete_test() { condition: TextFilterCondition::TextIsEmpty, content: "".to_string(), }; - let payload = CreateFilterPayloadPB::new(&field_rev, text_filter); + let payload = AlterFilterPayloadPB::new(&field_rev, text_filter); let scripts = vec![ InsertFilter { payload }, AssertFilterCount { count: 1 }, @@ -125,3 +190,28 @@ async fn grid_filter_delete_test() { ]) .await; } + +#[tokio::test] +async fn grid_filter_update_empty_text_cell_test() { + let mut test = GridFilterTest::new().await; + let scripts = vec![ + CreateTextFilter { + condition: TextFilterCondition::TextIsEmpty, + content: "".to_string(), + }, + AssertFilterCount { count: 1 }, + AssertFilterChanged { + visible_row_len: 1, + hide_row_len: 4, + }, + UpdateTextCell { + row_index: 0, + text: "".to_string(), + }, + AssertFilterChanged { + visible_row_len: 1, + hide_row_len: 0, + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-revision/src/rev_manager.rs b/frontend/rust-lib/flowy-revision/src/rev_manager.rs index 4bffdea872..eeeb501bcf 100644 --- a/frontend/rust-lib/flowy-revision/src/rev_manager.rs +++ b/frontend/rust-lib/flowy-revision/src/rev_manager.rs @@ -159,7 +159,7 @@ impl RevisionManager { Ok(()) } - #[tracing::instrument(level = "debug", skip_all, err)] + // #[tracing::instrument(level = "trace", skip_all, err)] pub async fn add_local_revision(&self, revision: &Revision) -> Result<(), FlowyError> { if revision.bytes.is_empty() { return Err(FlowyError::internal().context("Local revisions is empty")); diff --git a/shared-lib/flowy-error-code/src/code.rs b/shared-lib/flowy-error-code/src/code.rs index c2c95caab1..2bba87af2e 100644 --- a/shared-lib/flowy-error-code/src/code.rs +++ b/shared-lib/flowy-error-code/src/code.rs @@ -110,6 +110,8 @@ pub enum ErrorCode { FieldNotExists = 443, #[display(fmt = "The operation in this field is invalid")] FieldInvalidOperation = 444, + #[display(fmt = "Filter id is empty")] + FilterIdIsEmpty = 445, #[display(fmt = "Field's type-option data should not be empty")] TypeOptionDataIsEmpty = 450, diff --git a/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs index 85133c034a..2d5fcebba7 100644 --- a/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/block_revision_pad.rs @@ -93,6 +93,15 @@ impl GridBlockRevisionPad { }) } + pub fn get_row_rev(&self, row_id: &str) -> Option<(usize, Arc)> { + for (index, row) in self.block.rows.iter().enumerate() { + if row.id == row_id { + return Some((index, row.clone())); + } + } + None + } + pub fn get_row_revs(&self, row_ids: Option>>) -> CollaborateResult>> where T: AsRef + ToOwned + ?Sized, diff --git a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs index 43aa801d1f..37d76fbeec 100644 --- a/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs +++ b/shared-lib/flowy-sync/src/client_grid/view_revision_pad.rs @@ -142,18 +142,48 @@ impl GridViewRevisionPad { self.filters.get_objects(field_id, field_type_rev).unwrap_or_default() } + pub fn get_filter( + &self, + field_id: &str, + field_type_rev: &FieldTypeRevision, + filter_id: &str, + ) -> Option> { + self.filters + .get_object(field_id, field_type_rev, |filter| filter.id == filter_id) + } + pub fn insert_filter( &mut self, field_id: &str, - field_type: &FieldTypeRevision, filter_rev: FilterRevision, ) -> CollaborateResult> { self.modify(|view| { - view.filters.add_object(field_id, field_type, filter_rev); + let field_type = filter_rev.field_type; + view.filters.add_object(field_id, &field_type, filter_rev); Ok(Some(())) }) } + pub fn update_filter( + &mut self, + field_id: &str, + filter_rev: FilterRevision, + ) -> CollaborateResult> { + self.modify(|view| { + if let Some(filter) = view + .filters + .get_mut_object(field_id, &filter_rev.field_type, |filter| filter.id == filter_rev.id) + { + let filter = Arc::make_mut(filter); + filter.condition = filter_rev.condition; + filter.content = filter_rev.content; + Ok(Some(())) + } else { + Ok(None) + } + }) + } + pub fn delete_filter( &mut self, filter_id: &str, diff --git a/shared-lib/grid-rev-model/src/filter_rev.rs b/shared-lib/grid-rev-model/src/filter_rev.rs index 4bf6d30de6..0dd65d4dc8 100644 --- a/shared-lib/grid-rev-model/src/filter_rev.rs +++ b/shared-lib/grid-rev-model/src/filter_rev.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; pub struct FilterRevision { pub id: String, pub field_id: String, - pub field_type_rev: FieldTypeRevision, + pub field_type: FieldTypeRevision, pub condition: u8, #[serde(default)] pub content: String, diff --git a/shared-lib/grid-rev-model/src/grid_setting_rev.rs b/shared-lib/grid-rev-model/src/grid_setting_rev.rs index 31d1b2ba47..785df6cc52 100644 --- a/shared-lib/grid-rev-model/src/grid_setting_rev.rs +++ b/shared-lib/grid-rev-model/src/grid_setting_rev.rs @@ -47,16 +47,39 @@ where let value = self .inner .get_mut(field_id) - .and_then(|object_rev_map| object_rev_map.get_mut(field_type)); + .and_then(|object_map| object_map.get_mut(field_type)); if value.is_none() { eprintln!("[Configuration] Can't find the {:?} with", std::any::type_name::()); } value } + + pub fn get_object( + &self, + field_id: &str, + field_type: &FieldTypeRevision, + predicate: impl Fn(&Arc) -> bool, + ) -> Option> { + let objects = self.get_objects(field_id, field_type)?; + let index = objects.iter().position(|object| predicate(object))?; + objects.get(index).map(|object| object.clone()) + } + + pub fn get_mut_object( + &mut self, + field_id: &str, + field_type: &FieldTypeRevision, + predicate: impl Fn(&Arc) -> bool, + ) -> Option<&mut Arc> { + let objects = self.get_mut_objects(field_id, field_type)?; + let index = objects.iter().position(|object| predicate(object))?; + objects.get_mut(index) + } + pub fn get_objects(&self, field_id: &str, field_type_rev: &FieldTypeRevision) -> Option>> { self.inner .get(field_id) - .and_then(|object_rev_map| object_rev_map.get(field_type_rev)) + .and_then(|object_map| object_map.get(field_type_rev)) .cloned() }