chore: more select option cell editor improvements (#5019)

This commit is contained in:
Richard Shiue 2024-03-31 19:03:02 +08:00 committed by GitHub
parent 129d56e494
commit 53dbef25ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 131 additions and 128 deletions

View File

@ -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/plugins/database/domain/select_option_cell_service.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -56,15 +57,20 @@ class SelectOptionCellEditorBloc
VoidCallback? _onCellChangedFn; VoidCallback? _onCellChangedFn;
final List<SelectOptionPB> allOptions = [];
String filter = "";
void _dispatch() { void _dispatch() {
on<SelectOptionCellEditorEvent>( on<SelectOptionCellEditorEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
didReceiveOptions: (options, selectedOptions) { didReceiveOptions: (options, selectedOptions) {
final result = _makeOptions(state.filter, options); final result = _getVisibleOptions(options);
allOptions
..clear()
..addAll(options);
emit( emit(
state.copyWith( state.copyWith(
allOptions: options,
options: result.options, options: result.options,
createSelectOptionSuggestion: createSelectOptionSuggestion:
result.createSelectOptionSuggestion, result.createSelectOptionSuggestion,
@ -76,22 +82,19 @@ class SelectOptionCellEditorBloc
if (state.createSelectOptionSuggestion == null) { if (state.createSelectOptionSuggestion == null) {
return; return;
} }
filter = "";
await _createOption( await _createOption(
name: state.createSelectOptionSuggestion!.name, name: state.createSelectOptionSuggestion!.name,
color: state.createSelectOptionSuggestion!.color, color: state.createSelectOptionSuggestion!.color,
); );
emit( emit(state.copyWith(clearFilter: true));
state.copyWith(
filter: null,
),
);
}, },
deleteOption: (option) async { deleteOption: (option) async {
await _deleteOption([option]); await _deleteOption([option]);
}, },
deleteAllOptions: () async { deleteAllOptions: () async {
if (state.allOptions.isNotEmpty) { if (allOptions.isNotEmpty) {
await _deleteOption(state.allOptions); await _deleteOption(allOptions);
} }
}, },
updateOption: (option) async { updateOption: (option) async {
@ -99,27 +102,17 @@ class SelectOptionCellEditorBloc
}, },
selectOption: (optionId) async { selectOption: (optionId) async {
await _selectOptionService.select(optionIds: [optionId]); await _selectOptionService.select(optionIds: [optionId]);
final selectedOption = [
...state.selectedOptions,
state.options.firstWhere(
(element) => element.id == optionId,
),
];
emit(
state.copyWith(
selectedOptions: selectedOption,
),
);
}, },
unSelectOption: (optionId) async { unSelectOption: (optionId) async {
await _selectOptionService.unSelect(optionIds: [optionId]); await _selectOptionService.unSelect(optionIds: [optionId]);
final selectedOptions = [...state.selectedOptions] },
..removeWhere((e) => e.id == optionId); unSelectLastOption: () async {
emit( if (state.selectedOptions.isEmpty) {
state.copyWith( return;
selectedOptions: selectedOptions, }
), final lastSelectedOptionId = state.selectedOptions.last.id;
); await _selectOptionService
.unSelect(optionIds: [lastSelectedOptionId]);
}, },
submitTextField: () { submitTextField: () {
_submitTextFieldValue(emit); _submitTextFieldValue(emit);
@ -132,64 +125,31 @@ class SelectOptionCellEditorBloc
}, },
reorderOption: (fromOptionId, toOptionId) { reorderOption: (fromOptionId, toOptionId) {
final options = _typeOptionAction.reorderOption( final options = _typeOptionAction.reorderOption(
state.allOptions, allOptions,
fromOptionId, fromOptionId,
toOptionId, toOptionId,
); );
final result = _makeOptions(state.filter, options); allOptions
emit( ..clear()
state.copyWith( ..addAll(options);
allOptions: options, final result = _getVisibleOptions(options);
options: result.options, emit(state.copyWith(options: result.options));
),
);
}, },
filterOption: (optionName) { filterOption: (filterText) {
_filterOption(optionName, emit); _filterOption(filterText, emit);
}, },
focusPreviousOption: () { focusPreviousOption: () {
if (state.options.isEmpty) { _focusOption(true, emit);
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,
),
);
}
}
}, },
focusNextOption: () { focusNextOption: () {
if (state.options.isEmpty) { _focusOption(false, emit);
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,
),
);
}
}
}, },
updateFocusedOption: (optionId) { updateFocusedOption: (optionId) {
emit(state.copyWith(focusedOptionId: optionId)); emit(state.copyWith(focusedOptionId: optionId));
}, },
resetClearFilterFlag: () {
emit(state.copyWith(clearFilter: false));
},
); );
}, },
); );
@ -233,59 +193,57 @@ class SelectOptionCellEditorBloc
return; return;
} }
final optionId = state.focusedOptionId!; final focusedOptionId = state.focusedOptionId!;
if (optionId == createSelectOptionSuggestionId) { if (focusedOptionId == createSelectOptionSuggestionId) {
filter = "";
_createOption( _createOption(
name: state.createSelectOptionSuggestion!.name, name: state.createSelectOptionSuggestion!.name,
color: state.createSelectOptionSuggestion!.color, color: state.createSelectOptionSuggestion!.color,
); );
emit( emit(
state.copyWith( state.copyWith(
filter: null,
createSelectOptionSuggestion: null, createSelectOptionSuggestion: null,
clearFilter: true,
), ),
); );
} else if (!state.selectedOptions.any((option) => option.id == optionId)) { } else if (!state.selectedOptions
_selectOptionService.select(optionIds: [optionId]); .any((option) => option.id == focusedOptionId)) {
_selectOptionService.select(optionIds: [focusedOptionId]);
} }
} }
void _selectMultipleOptions(List<String> optionNames) { void _selectMultipleOptions(List<String> optionNames) {
// The options are unordered. So in order to keep the inserted [optionNames] final optionIds = optionNames
// order, it needs to get the option id in the [optionNames] order. .map(
final lowerCaseNames = optionNames.map((e) => e.toLowerCase()); (name) => allOptions.firstWhereOrNull(
final Map<String, String> optionIdsMap = {}; (option) => option.name.toLowerCase() == name.toLowerCase(),
for (final option in state.options) { ),
optionIdsMap[option.name.toLowerCase()] = option.id; )
} .nonNulls
.map((option) => option.id)
final optionIds = lowerCaseNames
.where((name) => optionIdsMap[name] != null)
.map((name) => optionIdsMap[name]!)
.toList(); .toList();
_selectOptionService.select(optionIds: optionIds); _selectOptionService.select(optionIds: optionIds);
} }
void _filterOption( void _filterOption(
String optionName, String filterText,
Emitter<SelectOptionCellEditorState> emit, Emitter<SelectOptionCellEditorState> emit,
) { ) {
final _MakeOptionResult result = _makeOptions( filter = filterText;
optionName, final _MakeOptionResult result = _getVisibleOptions(
state.allOptions, allOptions,
); );
final focusedOptionId = result.options.isEmpty final focusedOptionId = result.options.isEmpty
? result.createSelectOptionSuggestion == null ? result.createSelectOptionSuggestion == null
? null ? null
: createSelectOptionSuggestionId : createSelectOptionSuggestionId
: result.options.length != state.options.length : result.options.any((option) => option.id == state.focusedOptionId)
? result.options.first.id ? state.focusedOptionId
: state.focusedOptionId; : result.options.first.id;
emit( emit(
state.copyWith( state.copyWith(
filter: optionName,
options: result.options, options: result.options,
createSelectOptionSuggestion: result.createSelectOptionSuggestion, createSelectOptionSuggestion: result.createSelectOptionSuggestion,
focusedOptionId: focusedOptionId, focusedOptionId: focusedOptionId,
@ -314,39 +272,66 @@ class SelectOptionCellEditorBloc
); );
} }
_MakeOptionResult _makeOptions( _MakeOptionResult _getVisibleOptions(
String? filter,
List<SelectOptionPB> allOptions, List<SelectOptionPB> allOptions,
) { ) {
final List<SelectOptionPB> options = List.from(allOptions); final List<SelectOptionPB> options = List.from(allOptions);
String? newOptionName = filter; String newOptionName = filter;
if (filter != null && filter.isNotEmpty) { if (filter.isNotEmpty) {
options.retainWhere((option) { options.retainWhere((option) {
final name = option.name.toLowerCase(); final name = option.name.toLowerCase();
final lFilter = filter.toLowerCase(); final lFilter = filter.toLowerCase();
if (name == lFilter) { if (name == lFilter) {
newOptionName = null; newOptionName = "";
} }
return name.contains(lFilter); return name.contains(lFilter);
}); });
} else {
newOptionName = null;
} }
return _MakeOptionResult( return _MakeOptionResult(
options: options, options: options,
createSelectOptionSuggestion: newOptionName != null createSelectOptionSuggestion: newOptionName.isEmpty
? CreateSelectOptionSuggestion( ? null
name: newOptionName!, : CreateSelectOptionSuggestion(
name: newOptionName,
color: newSelectOptionColor(allOptions), color: newSelectOptionColor(allOptions),
) ),
: null,
); );
} }
void _focusOption(bool previous, Emitter<SelectOptionCellEditorState> 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() { void _startListening() {
_onCellChangedFn = cellController.addListener( _onCellChangedFn = cellController.addListener(
onCellChanged: (selectOptionContext) { onCellChanged: (selectOptionContext) {
@ -370,6 +355,8 @@ class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent {
_SelectOption; _SelectOption;
const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) = const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) =
_UnSelectOption; _UnSelectOption;
const factory SelectOptionCellEditorEvent.unSelectLastOption() =
_UnSelectLastOption;
const factory SelectOptionCellEditorEvent.updateOption( const factory SelectOptionCellEditorEvent.updateOption(
SelectOptionPB option, SelectOptionPB option,
) = _UpdateOption; ) = _UpdateOption;
@ -382,7 +369,7 @@ class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent {
String fromOptionId, String fromOptionId,
String toOptionId, String toOptionId,
) = _ReorderOption; ) = _ReorderOption;
const factory SelectOptionCellEditorEvent.filterOption(String optionName) = const factory SelectOptionCellEditorEvent.filterOption(String filterText) =
_SelectOptionFilter; _SelectOptionFilter;
const factory SelectOptionCellEditorEvent.submitTextField() = const factory SelectOptionCellEditorEvent.submitTextField() =
_SubmitTextField; _SubmitTextField;
@ -397,17 +384,18 @@ class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent {
const factory SelectOptionCellEditorEvent.updateFocusedOption( const factory SelectOptionCellEditorEvent.updateFocusedOption(
String? optionId, String? optionId,
) = _UpdateFocusedOption; ) = _UpdateFocusedOption;
const factory SelectOptionCellEditorEvent.resetClearFilterFlag() =
_ResetClearFilterFlag;
} }
@freezed @freezed
class SelectOptionCellEditorState with _$SelectOptionCellEditorState { class SelectOptionCellEditorState with _$SelectOptionCellEditorState {
const factory SelectOptionCellEditorState({ const factory SelectOptionCellEditorState({
required List<SelectOptionPB> options, required List<SelectOptionPB> options,
required List<SelectOptionPB> allOptions,
required List<SelectOptionPB> selectedOptions, required List<SelectOptionPB> selectedOptions,
required CreateSelectOptionSuggestion? createSelectOptionSuggestion, required CreateSelectOptionSuggestion? createSelectOptionSuggestion,
required String? filter,
required String? focusedOptionId, required String? focusedOptionId,
required bool clearFilter,
}) = _SelectOptionEditorState; }) = _SelectOptionEditorState;
factory SelectOptionCellEditorState.initial( factory SelectOptionCellEditorState.initial(
@ -416,11 +404,10 @@ class SelectOptionCellEditorState with _$SelectOptionCellEditorState {
final data = context.getCellData(loadIfNotExist: false); final data = context.getCellData(loadIfNotExist: false);
return SelectOptionCellEditorState( return SelectOptionCellEditorState(
options: data?.options ?? [], options: data?.options ?? [],
allOptions: data?.options ?? [],
selectedOptions: data?.selectOptions ?? [], selectedOptions: data?.selectOptions ?? [],
createSelectOptionSuggestion: null, createSelectOptionSuggestion: null,
filter: null,
focusedOptionId: null, focusedOptionId: null,
clearFilter: false,
); );
} }
} }

View File

@ -36,7 +36,7 @@ class SelectOptionFilterListBloc<T>
emit: emit, emit: emit,
); );
}, },
unselectOption: (option) { unSelectOption: (option) {
final selectedOptionIds = Set<String>.from(state.selectedOptionIds); final selectedOptionIds = Set<String>.from(state.selectedOptionIds);
selectedOptionIds.remove(option.id); selectedOptionIds.remove(option.id);
@ -121,7 +121,7 @@ class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent {
SelectOptionPB option, SelectOptionPB option,
SelectOptionFilterConditionPB condition, SelectOptionFilterConditionPB condition,
) = _SelectOption; ) = _SelectOption;
const factory SelectOptionFilterListEvent.unselectOption( const factory SelectOptionFilterListEvent.unSelectOption(
SelectOptionPB option, SelectOptionPB option,
) = _UnSelectOption; ) = _UnSelectOption;
const factory SelectOptionFilterListEvent.didReceiveOptions( const factory SelectOptionFilterListEvent.didReceiveOptions(

View File

@ -89,7 +89,7 @@ class _SelectOptionFilterCellState extends State<SelectOptionFilterCell> {
if (widget.isSelected) { if (widget.isSelected) {
context context
.read<SelectOptionFilterListBloc>() .read<SelectOptionFilterListBloc>()
.add(SelectOptionFilterListEvent.unselectOption(widget.option)); .add(SelectOptionFilterListEvent.unSelectOption(widget.option));
} else { } else {
context.read<SelectOptionFilterListBloc>().add( context.read<SelectOptionFilterListBloc>().add(
SelectOptionFilterListEvent.selectOption( SelectOptionFilterListEvent.selectOption(

View File

@ -45,21 +45,20 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
super.initState(); super.initState();
focusNode = FocusNode( focusNode = FocusNode(
onKeyEvent: (node, event) { onKeyEvent: (node, event) {
if (event is KeyUpEvent) {
return KeyEventResult.ignored;
}
switch (event.logicalKey) { switch (event.logicalKey) {
case LogicalKeyboardKey.arrowUp: case LogicalKeyboardKey.arrowUp when event is! KeyUpEvent:
if (textEditingController.value.composing.isCollapsed) { if (textEditingController.value.composing.isCollapsed) {
bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption()); bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption());
return KeyEventResult.handled; return KeyEventResult.handled;
} }
case LogicalKeyboardKey.arrowDown: break;
case LogicalKeyboardKey.arrowDown when event is! KeyUpEvent:
if (textEditingController.value.composing.isCollapsed) { if (textEditingController.value.composing.isCollapsed) {
bloc.add(const SelectOptionCellEditorEvent.focusNextOption()); bloc.add(const SelectOptionCellEditorEvent.focusNextOption());
return KeyEventResult.handled; return KeyEventResult.handled;
} }
case LogicalKeyboardKey.escape: break;
case LogicalKeyboardKey.escape when event is! KeyUpEvent:
if (!textEditingController.value.composing.isCollapsed) { if (!textEditingController.value.composing.isCollapsed) {
final end = textEditingController.value.composing.end; final end = textEditingController.value.composing.end;
final text = textEditingController.text; final text = textEditingController.text;
@ -70,6 +69,13 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
); );
return KeyEventResult.handled; 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; return KeyEventResult.ignored;
}, },
@ -126,7 +132,18 @@ class _OptionList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>( return BlocConsumer<SelectOptionCellEditorBloc,
SelectOptionCellEditorState>(
listenWhen: (previous, current) =>
previous.clearFilter != current.clearFilter,
listener: (context, state) {
if (state.clearFilter) {
textEditingController.clear();
context
.read<SelectOptionCellEditorBloc>()
.add(const SelectOptionCellEditorEvent.resetClearFilterFlag());
}
},
buildWhen: (previous, current) => buildWhen: (previous, current) =>
!listEquals(previous.options, current.options) || !listEquals(previous.options, current.options) ||
previous.createSelectOptionSuggestion != previous.createSelectOptionSuggestion !=
@ -231,7 +248,6 @@ class _TextField extends StatelessWidget {
context context
.read<SelectOptionCellEditorBloc>() .read<SelectOptionCellEditorBloc>()
.add(const SelectOptionCellEditorEvent.submitTextField()); .add(const SelectOptionCellEditorEvent.submitTextField());
textEditingController.clear();
focusNode.requestFocus(); focusNode.requestFocus();
}, },
onPaste: (tagNames, remainder) { onPaste: (tagNames, remainder) {

View File

@ -184,7 +184,7 @@ void main() {
assert(bloc.state.selectedOptions.length == 1); assert(bloc.state.selectedOptions.length == 1);
expect(bloc.state.selectedOptions[0].name, "A"); expect(bloc.state.selectedOptions[0].name, "A");
expect(bloc.state.filter, "x"); expect(bloc.filter, "x");
}); });
test('filter options', () async { test('filter options', () async {
@ -234,12 +234,12 @@ void main() {
reason: "Options: ${bloc.state.options}", reason: "Options: ${bloc.state.options}",
); );
expect( expect(
bloc.state.allOptions.length, bloc.allOptions.length,
3, 3,
reason: "Options: ${bloc.state.options}", reason: "Options: ${bloc.state.options}",
); );
expect(bloc.state.createSelectOptionSuggestion!.name, "a"); expect(bloc.state.createSelectOptionSuggestion!.name, "a");
expect(bloc.state.filter, "a"); expect(bloc.filter, "a");
}); });
}); });
} }