diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart index 0f57a32cb7..8e9e068920 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -56,15 +57,20 @@ class SelectOptionCellEditorBloc VoidCallback? _onCellChangedFn; + final List allOptions = []; + String filter = ""; + void _dispatch() { on( (event, emit) async { await event.when( didReceiveOptions: (options, selectedOptions) { - final result = _makeOptions(state.filter, options); + final result = _getVisibleOptions(options); + allOptions + ..clear() + ..addAll(options); emit( state.copyWith( - allOptions: options, options: result.options, createSelectOptionSuggestion: result.createSelectOptionSuggestion, @@ -76,22 +82,19 @@ class SelectOptionCellEditorBloc if (state.createSelectOptionSuggestion == null) { return; } + filter = ""; await _createOption( name: state.createSelectOptionSuggestion!.name, color: state.createSelectOptionSuggestion!.color, ); - emit( - state.copyWith( - filter: null, - ), - ); + emit(state.copyWith(clearFilter: true)); }, deleteOption: (option) async { await _deleteOption([option]); }, deleteAllOptions: () async { - if (state.allOptions.isNotEmpty) { - await _deleteOption(state.allOptions); + if (allOptions.isNotEmpty) { + await _deleteOption(allOptions); } }, updateOption: (option) async { @@ -99,27 +102,17 @@ class SelectOptionCellEditorBloc }, selectOption: (optionId) async { await _selectOptionService.select(optionIds: [optionId]); - final selectedOption = [ - ...state.selectedOptions, - state.options.firstWhere( - (element) => element.id == optionId, - ), - ]; - emit( - state.copyWith( - selectedOptions: selectedOption, - ), - ); }, unSelectOption: (optionId) async { await _selectOptionService.unSelect(optionIds: [optionId]); - final selectedOptions = [...state.selectedOptions] - ..removeWhere((e) => e.id == optionId); - emit( - state.copyWith( - selectedOptions: selectedOptions, - ), - ); + }, + unSelectLastOption: () async { + if (state.selectedOptions.isEmpty) { + return; + } + final lastSelectedOptionId = state.selectedOptions.last.id; + await _selectOptionService + .unSelect(optionIds: [lastSelectedOptionId]); }, submitTextField: () { _submitTextFieldValue(emit); @@ -132,64 +125,31 @@ class SelectOptionCellEditorBloc }, reorderOption: (fromOptionId, toOptionId) { final options = _typeOptionAction.reorderOption( - state.allOptions, + allOptions, fromOptionId, toOptionId, ); - final result = _makeOptions(state.filter, options); - emit( - state.copyWith( - allOptions: options, - options: result.options, - ), - ); + allOptions + ..clear() + ..addAll(options); + final result = _getVisibleOptions(options); + emit(state.copyWith(options: result.options)); }, - filterOption: (optionName) { - _filterOption(optionName, emit); + filterOption: (filterText) { + _filterOption(filterText, emit); }, focusPreviousOption: () { - if (state.options.isEmpty) { - return; - } - if (state.focusedOptionId == null) { - emit(state.copyWith(focusedOptionId: state.options.last.id)); - } else { - final currentIndex = state.options - .indexWhere((option) => option.id == state.focusedOptionId); - - if (currentIndex != -1) { - final newIndex = (currentIndex - 1) % state.options.length; - emit( - state.copyWith( - focusedOptionId: state.options[newIndex].id, - ), - ); - } - } + _focusOption(true, emit); }, focusNextOption: () { - if (state.options.isEmpty) { - return; - } - if (state.focusedOptionId == null) { - emit(state.copyWith(focusedOptionId: state.options.first.id)); - } else { - final currentIndex = state.options - .indexWhere((option) => option.id == state.focusedOptionId); - - if (currentIndex != -1) { - final newIndex = (currentIndex + 1) % state.options.length; - emit( - state.copyWith( - focusedOptionId: state.options[newIndex].id, - ), - ); - } - } + _focusOption(false, emit); }, updateFocusedOption: (optionId) { emit(state.copyWith(focusedOptionId: optionId)); }, + resetClearFilterFlag: () { + emit(state.copyWith(clearFilter: false)); + }, ); }, ); @@ -233,59 +193,57 @@ class SelectOptionCellEditorBloc return; } - final optionId = state.focusedOptionId!; + final focusedOptionId = state.focusedOptionId!; - if (optionId == createSelectOptionSuggestionId) { + if (focusedOptionId == createSelectOptionSuggestionId) { + filter = ""; _createOption( name: state.createSelectOptionSuggestion!.name, color: state.createSelectOptionSuggestion!.color, ); emit( state.copyWith( - filter: null, createSelectOptionSuggestion: null, + clearFilter: true, ), ); - } else if (!state.selectedOptions.any((option) => option.id == optionId)) { - _selectOptionService.select(optionIds: [optionId]); + } else if (!state.selectedOptions + .any((option) => option.id == focusedOptionId)) { + _selectOptionService.select(optionIds: [focusedOptionId]); } } void _selectMultipleOptions(List optionNames) { - // The options are unordered. So in order to keep the inserted [optionNames] - // order, it needs to get the option id in the [optionNames] order. - final lowerCaseNames = optionNames.map((e) => e.toLowerCase()); - final Map optionIdsMap = {}; - for (final option in state.options) { - optionIdsMap[option.name.toLowerCase()] = option.id; - } - - final optionIds = lowerCaseNames - .where((name) => optionIdsMap[name] != null) - .map((name) => optionIdsMap[name]!) + final optionIds = optionNames + .map( + (name) => allOptions.firstWhereOrNull( + (option) => option.name.toLowerCase() == name.toLowerCase(), + ), + ) + .nonNulls + .map((option) => option.id) .toList(); _selectOptionService.select(optionIds: optionIds); } void _filterOption( - String optionName, + String filterText, Emitter emit, ) { - final _MakeOptionResult result = _makeOptions( - optionName, - state.allOptions, + filter = filterText; + final _MakeOptionResult result = _getVisibleOptions( + allOptions, ); final focusedOptionId = result.options.isEmpty ? result.createSelectOptionSuggestion == null ? null : createSelectOptionSuggestionId - : result.options.length != state.options.length - ? result.options.first.id - : state.focusedOptionId; + : result.options.any((option) => option.id == state.focusedOptionId) + ? state.focusedOptionId + : result.options.first.id; emit( state.copyWith( - filter: optionName, options: result.options, createSelectOptionSuggestion: result.createSelectOptionSuggestion, focusedOptionId: focusedOptionId, @@ -314,39 +272,66 @@ class SelectOptionCellEditorBloc ); } - _MakeOptionResult _makeOptions( - String? filter, + _MakeOptionResult _getVisibleOptions( List allOptions, ) { final List options = List.from(allOptions); - String? newOptionName = filter; + String newOptionName = filter; - if (filter != null && filter.isNotEmpty) { + if (filter.isNotEmpty) { options.retainWhere((option) { final name = option.name.toLowerCase(); final lFilter = filter.toLowerCase(); if (name == lFilter) { - newOptionName = null; + newOptionName = ""; } return name.contains(lFilter); }); - } else { - newOptionName = null; } return _MakeOptionResult( options: options, - createSelectOptionSuggestion: newOptionName != null - ? CreateSelectOptionSuggestion( - name: newOptionName!, + createSelectOptionSuggestion: newOptionName.isEmpty + ? null + : CreateSelectOptionSuggestion( + name: newOptionName, color: newSelectOptionColor(allOptions), - ) - : null, + ), ); } + void _focusOption(bool previous, Emitter emit) { + if (state.options.isEmpty && state.createSelectOptionSuggestion == null) { + return; + } + + final optionIds = [ + ...state.options.map((e) => e.id), + if (state.createSelectOptionSuggestion != null) + createSelectOptionSuggestionId, + ]; + + if (state.focusedOptionId == null) { + emit( + state.copyWith( + focusedOptionId: previous ? optionIds.last : optionIds.first, + ), + ); + return; + } + + final currentIndex = + optionIds.indexWhere((id) => id == state.focusedOptionId); + + final newIndex = currentIndex == -1 + ? 0 + : (currentIndex + (previous ? -1 : 1)) % optionIds.length; + + emit(state.copyWith(focusedOptionId: optionIds[newIndex])); + } + void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (selectOptionContext) { @@ -370,6 +355,8 @@ class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent { _SelectOption; const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) = _UnSelectOption; + const factory SelectOptionCellEditorEvent.unSelectLastOption() = + _UnSelectLastOption; const factory SelectOptionCellEditorEvent.updateOption( SelectOptionPB option, ) = _UpdateOption; @@ -382,7 +369,7 @@ class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent { String fromOptionId, String toOptionId, ) = _ReorderOption; - const factory SelectOptionCellEditorEvent.filterOption(String optionName) = + const factory SelectOptionCellEditorEvent.filterOption(String filterText) = _SelectOptionFilter; const factory SelectOptionCellEditorEvent.submitTextField() = _SubmitTextField; @@ -397,17 +384,18 @@ class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent { const factory SelectOptionCellEditorEvent.updateFocusedOption( String? optionId, ) = _UpdateFocusedOption; + const factory SelectOptionCellEditorEvent.resetClearFilterFlag() = + _ResetClearFilterFlag; } @freezed class SelectOptionCellEditorState with _$SelectOptionCellEditorState { const factory SelectOptionCellEditorState({ required List options, - required List allOptions, required List selectedOptions, required CreateSelectOptionSuggestion? createSelectOptionSuggestion, - required String? filter, required String? focusedOptionId, + required bool clearFilter, }) = _SelectOptionEditorState; factory SelectOptionCellEditorState.initial( @@ -416,11 +404,10 @@ class SelectOptionCellEditorState with _$SelectOptionCellEditorState { final data = context.getCellData(loadIfNotExist: false); return SelectOptionCellEditorState( options: data?.options ?? [], - allOptions: data?.options ?? [], selectedOptions: data?.selectOptions ?? [], createSelectOptionSuggestion: null, - filter: null, focusedOptionId: null, + clearFilter: false, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart index 278f2a424f..84e1284822 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart @@ -36,7 +36,7 @@ class SelectOptionFilterListBloc emit: emit, ); }, - unselectOption: (option) { + unSelectOption: (option) { final selectedOptionIds = Set.from(state.selectedOptionIds); selectedOptionIds.remove(option.id); @@ -121,7 +121,7 @@ class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent { SelectOptionPB option, SelectOptionFilterConditionPB condition, ) = _SelectOption; - const factory SelectOptionFilterListEvent.unselectOption( + const factory SelectOptionFilterListEvent.unSelectOption( SelectOptionPB option, ) = _UnSelectOption; const factory SelectOptionFilterListEvent.didReceiveOptions( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart index a8ec42f2f6..b3c1482453 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart @@ -89,7 +89,7 @@ class _SelectOptionFilterCellState extends State { if (widget.isSelected) { context .read() - .add(SelectOptionFilterListEvent.unselectOption(widget.option)); + .add(SelectOptionFilterListEvent.unSelectOption(widget.option)); } else { context.read().add( SelectOptionFilterListEvent.selectOption( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart index bbb6ee50ed..7f76cb26b3 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart @@ -45,21 +45,20 @@ class _SelectOptionCellEditorState extends State { super.initState(); focusNode = FocusNode( onKeyEvent: (node, event) { - if (event is KeyUpEvent) { - return KeyEventResult.ignored; - } switch (event.logicalKey) { - case LogicalKeyboardKey.arrowUp: + case LogicalKeyboardKey.arrowUp when event is! KeyUpEvent: if (textEditingController.value.composing.isCollapsed) { bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption()); return KeyEventResult.handled; } - case LogicalKeyboardKey.arrowDown: + break; + case LogicalKeyboardKey.arrowDown when event is! KeyUpEvent: if (textEditingController.value.composing.isCollapsed) { bloc.add(const SelectOptionCellEditorEvent.focusNextOption()); return KeyEventResult.handled; } - case LogicalKeyboardKey.escape: + break; + case LogicalKeyboardKey.escape when event is! KeyUpEvent: if (!textEditingController.value.composing.isCollapsed) { final end = textEditingController.value.composing.end; final text = textEditingController.text; @@ -70,6 +69,13 @@ class _SelectOptionCellEditorState extends State { ); return KeyEventResult.handled; } + break; + case LogicalKeyboardKey.backspace when event is KeyUpEvent: + if (!textEditingController.text.isNotEmpty) { + bloc.add(const SelectOptionCellEditorEvent.unSelectLastOption()); + return KeyEventResult.handled; + } + break; } return KeyEventResult.ignored; }, @@ -126,7 +132,18 @@ class _OptionList extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocConsumer( + listenWhen: (previous, current) => + previous.clearFilter != current.clearFilter, + listener: (context, state) { + if (state.clearFilter) { + textEditingController.clear(); + context + .read() + .add(const SelectOptionCellEditorEvent.resetClearFilterFlag()); + } + }, buildWhen: (previous, current) => !listEquals(previous.options, current.options) || previous.createSelectOptionSuggestion != @@ -231,7 +248,6 @@ class _TextField extends StatelessWidget { context .read() .add(const SelectOptionCellEditorEvent.submitTextField()); - textEditingController.clear(); focusNode.requestFocus(); }, onPaste: (tagNames, remainder) { diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart index 62f11e5b80..7801fed3e7 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart @@ -184,7 +184,7 @@ void main() { assert(bloc.state.selectedOptions.length == 1); expect(bloc.state.selectedOptions[0].name, "A"); - expect(bloc.state.filter, "x"); + expect(bloc.filter, "x"); }); test('filter options', () async { @@ -234,12 +234,12 @@ void main() { reason: "Options: ${bloc.state.options}", ); expect( - bloc.state.allOptions.length, + bloc.allOptions.length, 3, reason: "Options: ${bloc.state.options}", ); expect(bloc.state.createSelectOptionSuggestion!.name, "a"); - expect(bloc.state.filter, "a"); + expect(bloc.filter, "a"); }); }); }