mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: more select option cell editor improvements (#5019)
This commit is contained in:
parent
129d56e494
commit
53dbef25ab
@ -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<SelectOptionPB> allOptions = [];
|
||||
String filter = "";
|
||||
|
||||
void _dispatch() {
|
||||
on<SelectOptionCellEditorEvent>(
|
||||
(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<String> 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<String, String> 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<SelectOptionCellEditorState> 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<SelectOptionPB> 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) {
|
||||
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<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() {
|
||||
_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<SelectOptionPB> options,
|
||||
required List<SelectOptionPB> allOptions,
|
||||
required List<SelectOptionPB> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ class SelectOptionFilterListBloc<T>
|
||||
emit: emit,
|
||||
);
|
||||
},
|
||||
unselectOption: (option) {
|
||||
unSelectOption: (option) {
|
||||
final selectedOptionIds = Set<String>.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(
|
||||
|
@ -89,7 +89,7 @@ class _SelectOptionFilterCellState extends State<SelectOptionFilterCell> {
|
||||
if (widget.isSelected) {
|
||||
context
|
||||
.read<SelectOptionFilterListBloc>()
|
||||
.add(SelectOptionFilterListEvent.unselectOption(widget.option));
|
||||
.add(SelectOptionFilterListEvent.unSelectOption(widget.option));
|
||||
} else {
|
||||
context.read<SelectOptionFilterListBloc>().add(
|
||||
SelectOptionFilterListEvent.selectOption(
|
||||
|
@ -45,21 +45,20 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
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<SelectOptionCellEditor> {
|
||||
);
|
||||
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<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) =>
|
||||
!listEquals(previous.options, current.options) ||
|
||||
previous.createSelectOptionSuggestion !=
|
||||
@ -231,7 +248,6 @@ class _TextField extends StatelessWidget {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||
textEditingController.clear();
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onPaste: (tagNames, remainder) {
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user