mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: select option cell editor revamp (#5011)
* chore: gen new select option color on frontend * chore: reorder select options * chore: fix performance regression * chore: add text field tap region * chore: implement hover focus * chore: implement keyboard focus * chore: fix tests * chore: reorder options in field editor * chore: fix tests
This commit is contained in:
parent
adc2ee755e
commit
419464c175
@ -0,0 +1,446 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
|
||||
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:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'select_option_cell_editor_bloc.freezed.dart';
|
||||
|
||||
const String createSelectOptionSuggestionId =
|
||||
"create_select_option_suggestion_id";
|
||||
|
||||
class SelectOptionCellEditorBloc
|
||||
extends Bloc<SelectOptionCellEditorEvent, SelectOptionCellEditorState> {
|
||||
SelectOptionCellEditorBloc({required this.cellController})
|
||||
: _selectOptionService = SelectOptionCellBackendService(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
),
|
||||
_typeOptionAction = cellController.fieldType == FieldType.SingleSelect
|
||||
? SingleSelectAction(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
onTypeOptionUpdated: (typeOptionData) =>
|
||||
FieldBackendService.updateFieldTypeOption(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
typeOptionData: typeOptionData,
|
||||
),
|
||||
)
|
||||
: MultiSelectAction(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
onTypeOptionUpdated: (typeOptionData) =>
|
||||
FieldBackendService.updateFieldTypeOption(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
typeOptionData: typeOptionData,
|
||||
),
|
||||
),
|
||||
super(SelectOptionCellEditorState.initial(cellController)) {
|
||||
_dispatch();
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
}
|
||||
|
||||
final SelectOptionCellBackendService _selectOptionService;
|
||||
final ISelectOptionAction _typeOptionAction;
|
||||
final SelectOptionCellController cellController;
|
||||
|
||||
VoidCallback? _onCellChangedFn;
|
||||
|
||||
void _dispatch() {
|
||||
on<SelectOptionCellEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
didReceiveOptions: (options, selectedOptions) {
|
||||
final result = _makeOptions(state.filter, options);
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: options,
|
||||
options: result.options,
|
||||
createSelectOptionSuggestion:
|
||||
result.createSelectOptionSuggestion,
|
||||
selectedOptions: selectedOptions,
|
||||
),
|
||||
);
|
||||
},
|
||||
createOption: () async {
|
||||
if (state.createSelectOptionSuggestion == null) {
|
||||
return;
|
||||
}
|
||||
await _createOption(
|
||||
name: state.createSelectOptionSuggestion!.name,
|
||||
color: state.createSelectOptionSuggestion!.color,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
filter: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
deleteOption: (option) async {
|
||||
await _deleteOption([option]);
|
||||
},
|
||||
deleteAllOptions: () async {
|
||||
if (state.allOptions.isNotEmpty) {
|
||||
await _deleteOption(state.allOptions);
|
||||
}
|
||||
},
|
||||
updateOption: (option) async {
|
||||
await _updateOption(option);
|
||||
},
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
submitTextField: () {
|
||||
_submitTextFieldValue(emit);
|
||||
},
|
||||
selectMultipleOptions: (optionNames, remainder) {
|
||||
if (optionNames.isNotEmpty) {
|
||||
_selectMultipleOptions(optionNames);
|
||||
}
|
||||
_filterOption(remainder, emit);
|
||||
},
|
||||
reorderOption: (fromOptionId, toOptionId) {
|
||||
final options = _typeOptionAction.reorderOption(
|
||||
state.allOptions,
|
||||
fromOptionId,
|
||||
toOptionId,
|
||||
);
|
||||
final result = _makeOptions(state.filter, options);
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: options,
|
||||
options: result.options,
|
||||
),
|
||||
);
|
||||
},
|
||||
filterOption: (optionName) {
|
||||
_filterOption(optionName, 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateFocusedOption: (optionId) {
|
||||
emit(state.copyWith(focusedOptionId: optionId));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _createOption({
|
||||
required String name,
|
||||
required SelectOptionColorPB color,
|
||||
}) async {
|
||||
final result = await _selectOptionService.create(
|
||||
name: name,
|
||||
color: color,
|
||||
);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<void> _deleteOption(List<SelectOptionPB> options) async {
|
||||
final result = await _selectOptionService.delete(options: options);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<void> _updateOption(SelectOptionPB option) async {
|
||||
final result = await _selectOptionService.update(
|
||||
option: option,
|
||||
);
|
||||
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _submitTextFieldValue(Emitter<SelectOptionCellEditorState> emit) {
|
||||
if (state.focusedOptionId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final optionId = state.focusedOptionId!;
|
||||
|
||||
if (optionId == createSelectOptionSuggestionId) {
|
||||
_createOption(
|
||||
name: state.createSelectOptionSuggestion!.name,
|
||||
color: state.createSelectOptionSuggestion!.color,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
filter: null,
|
||||
createSelectOptionSuggestion: null,
|
||||
),
|
||||
);
|
||||
} else if (!state.selectedOptions.any((option) => option.id == optionId)) {
|
||||
_selectOptionService.select(optionIds: [optionId]);
|
||||
}
|
||||
}
|
||||
|
||||
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]!)
|
||||
.toList();
|
||||
|
||||
_selectOptionService.select(optionIds: optionIds);
|
||||
}
|
||||
|
||||
void _filterOption(
|
||||
String optionName,
|
||||
Emitter<SelectOptionCellEditorState> emit,
|
||||
) {
|
||||
final _MakeOptionResult result = _makeOptions(
|
||||
optionName,
|
||||
state.allOptions,
|
||||
);
|
||||
final focusedOptionId = result.options.isEmpty
|
||||
? result.createSelectOptionSuggestion == null
|
||||
? null
|
||||
: createSelectOptionSuggestionId
|
||||
: result.options.length != state.options.length
|
||||
? result.options.first.id
|
||||
: state.focusedOptionId;
|
||||
emit(
|
||||
state.copyWith(
|
||||
filter: optionName,
|
||||
options: result.options,
|
||||
createSelectOptionSuggestion: result.createSelectOptionSuggestion,
|
||||
focusedOptionId: focusedOptionId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadOptions() async {
|
||||
final result = await _selectOptionService.getCellData();
|
||||
if (isClosed) {
|
||||
Log.warn("Unexpecteded closing the bloc");
|
||||
return;
|
||||
}
|
||||
|
||||
return result.fold(
|
||||
(data) => add(
|
||||
SelectOptionCellEditorEvent.didReceiveOptions(
|
||||
data.options,
|
||||
data.selectOptions,
|
||||
),
|
||||
),
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_MakeOptionResult _makeOptions(
|
||||
String? filter,
|
||||
List<SelectOptionPB> allOptions,
|
||||
) {
|
||||
final List<SelectOptionPB> options = List.from(allOptions);
|
||||
String? newOptionName = filter;
|
||||
|
||||
if (filter != null && filter.isNotEmpty) {
|
||||
options.retainWhere((option) {
|
||||
final name = option.name.toLowerCase();
|
||||
final lFilter = filter.toLowerCase();
|
||||
|
||||
if (name == lFilter) {
|
||||
newOptionName = null;
|
||||
}
|
||||
|
||||
return name.contains(lFilter);
|
||||
});
|
||||
} else {
|
||||
newOptionName = null;
|
||||
}
|
||||
|
||||
return _MakeOptionResult(
|
||||
options: options,
|
||||
createSelectOptionSuggestion: newOptionName != null
|
||||
? CreateSelectOptionSuggestion(
|
||||
name: newOptionName!,
|
||||
color: newSelectOptionColor(allOptions),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (selectOptionContext) {
|
||||
_loadOptions();
|
||||
},
|
||||
onCellFieldChanged: (field) {
|
||||
_loadOptions();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent {
|
||||
const factory SelectOptionCellEditorEvent.didReceiveOptions(
|
||||
List<SelectOptionPB> options,
|
||||
List<SelectOptionPB> selectedOptions,
|
||||
) = _DidReceiveOptions;
|
||||
const factory SelectOptionCellEditorEvent.createOption() = _CreateOption;
|
||||
const factory SelectOptionCellEditorEvent.selectOption(String optionId) =
|
||||
_SelectOption;
|
||||
const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) =
|
||||
_UnSelectOption;
|
||||
const factory SelectOptionCellEditorEvent.updateOption(
|
||||
SelectOptionPB option,
|
||||
) = _UpdateOption;
|
||||
const factory SelectOptionCellEditorEvent.deleteOption(
|
||||
SelectOptionPB option,
|
||||
) = _DeleteOption;
|
||||
const factory SelectOptionCellEditorEvent.deleteAllOptions() =
|
||||
_DeleteAllOptions;
|
||||
const factory SelectOptionCellEditorEvent.reorderOption(
|
||||
String fromOptionId,
|
||||
String toOptionId,
|
||||
) = _ReorderOption;
|
||||
const factory SelectOptionCellEditorEvent.filterOption(String optionName) =
|
||||
_SelectOptionFilter;
|
||||
const factory SelectOptionCellEditorEvent.submitTextField() =
|
||||
_SubmitTextField;
|
||||
const factory SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||
List<String> optionNames,
|
||||
String remainder,
|
||||
) = _SelectMultipleOptions;
|
||||
const factory SelectOptionCellEditorEvent.focusPreviousOption() =
|
||||
_FocusPreviousOption;
|
||||
const factory SelectOptionCellEditorEvent.focusNextOption() =
|
||||
_FocusNextOption;
|
||||
const factory SelectOptionCellEditorEvent.updateFocusedOption(
|
||||
String? optionId,
|
||||
) = _UpdateFocusedOption;
|
||||
}
|
||||
|
||||
@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,
|
||||
}) = _SelectOptionEditorState;
|
||||
|
||||
factory SelectOptionCellEditorState.initial(
|
||||
SelectOptionCellController context,
|
||||
) {
|
||||
final data = context.getCellData(loadIfNotExist: false);
|
||||
return SelectOptionCellEditorState(
|
||||
options: data?.options ?? [],
|
||||
allOptions: data?.options ?? [],
|
||||
selectedOptions: data?.selectOptions ?? [],
|
||||
createSelectOptionSuggestion: null,
|
||||
filter: null,
|
||||
focusedOptionId: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MakeOptionResult {
|
||||
_MakeOptionResult({
|
||||
required this.options,
|
||||
required this.createSelectOptionSuggestion,
|
||||
});
|
||||
|
||||
List<SelectOptionPB> options;
|
||||
CreateSelectOptionSuggestion? createSelectOptionSuggestion;
|
||||
}
|
||||
|
||||
class CreateSelectOptionSuggestion {
|
||||
CreateSelectOptionSuggestion({
|
||||
required this.name,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final SelectOptionColorPB color;
|
||||
}
|
@ -1,318 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.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/select_option_entities.pb.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'select_option_editor_bloc.freezed.dart';
|
||||
|
||||
class SelectOptionCellEditorBloc
|
||||
extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
|
||||
SelectOptionCellEditorBloc({required this.cellController})
|
||||
: _selectOptionService = SelectOptionCellBackendService(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
),
|
||||
super(SelectOptionEditorState.initial(cellController)) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final SelectOptionCellBackendService _selectOptionService;
|
||||
final SelectOptionCellController cellController;
|
||||
|
||||
VoidCallback? _onCellChangedFn;
|
||||
|
||||
void _dispatch() {
|
||||
on<SelectOptionEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
await _loadOptions();
|
||||
},
|
||||
didReceiveOptions: (options, selectedOptions) {
|
||||
final result = _makeOptions(state.filter, options);
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: options,
|
||||
options: result.options,
|
||||
createOption: result.createOption,
|
||||
selectedOptions: selectedOptions,
|
||||
),
|
||||
);
|
||||
},
|
||||
newOption: (optionName) async {
|
||||
await _createOption(optionName);
|
||||
emit(
|
||||
state.copyWith(
|
||||
filter: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
deleteOption: (option) async {
|
||||
await _deleteOption([option]);
|
||||
},
|
||||
deleteAllOptions: () async {
|
||||
if (state.allOptions.isNotEmpty) {
|
||||
await _deleteOption(state.allOptions);
|
||||
}
|
||||
},
|
||||
updateOption: (option) async {
|
||||
await _updateOption(option);
|
||||
},
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
trySelectOption: (optionName) {
|
||||
_trySelectOption(optionName, emit);
|
||||
},
|
||||
selectMultipleOptions: (optionNames, remainder) {
|
||||
if (optionNames.isNotEmpty) {
|
||||
_selectMultipleOptions(optionNames);
|
||||
}
|
||||
_filterOption(remainder, emit);
|
||||
},
|
||||
filterOption: (optionName) {
|
||||
_filterOption(optionName, emit);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _createOption(String name) async {
|
||||
final result = await _selectOptionService.create(name: name);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<void> _deleteOption(List<SelectOptionPB> options) async {
|
||||
final result = await _selectOptionService.delete(options: options);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<void> _updateOption(SelectOptionPB option) async {
|
||||
final result = await _selectOptionService.update(
|
||||
option: option,
|
||||
);
|
||||
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _trySelectOption(
|
||||
String optionName,
|
||||
Emitter<SelectOptionEditorState> emit,
|
||||
) {
|
||||
SelectOptionPB? matchingOption;
|
||||
bool optionExistsButSelected = false;
|
||||
|
||||
for (final option in state.options) {
|
||||
if (option.name.toLowerCase() == optionName.toLowerCase()) {
|
||||
if (!state.selectedOptions.contains(option)) {
|
||||
matchingOption = option;
|
||||
break;
|
||||
} else {
|
||||
optionExistsButSelected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there isn't a matching option at all, then create it
|
||||
if (matchingOption == null && !optionExistsButSelected) {
|
||||
_createOption(optionName);
|
||||
}
|
||||
|
||||
// if there is an unselected matching option, select it
|
||||
if (matchingOption != null) {
|
||||
_selectOptionService.select(optionIds: [matchingOption.id]);
|
||||
}
|
||||
|
||||
// clear the filter
|
||||
emit(state.copyWith(filter: null));
|
||||
}
|
||||
|
||||
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]!)
|
||||
.toList();
|
||||
|
||||
_selectOptionService.select(optionIds: optionIds);
|
||||
}
|
||||
|
||||
void _filterOption(String optionName, Emitter<SelectOptionEditorState> emit) {
|
||||
final _MakeOptionResult result = _makeOptions(
|
||||
optionName,
|
||||
state.allOptions,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
filter: optionName,
|
||||
options: result.options,
|
||||
createOption: result.createOption,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadOptions() async {
|
||||
final result = await _selectOptionService.getCellData();
|
||||
if (isClosed) {
|
||||
Log.warn("Unexpecteded closing the bloc");
|
||||
return;
|
||||
}
|
||||
|
||||
return result.fold(
|
||||
(data) => add(
|
||||
SelectOptionEditorEvent.didReceiveOptions(
|
||||
data.options,
|
||||
data.selectOptions,
|
||||
),
|
||||
),
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
_MakeOptionResult _makeOptions(
|
||||
String? filter,
|
||||
List<SelectOptionPB> allOptions,
|
||||
) {
|
||||
final List<SelectOptionPB> options = List.from(allOptions);
|
||||
String? createOption = filter;
|
||||
|
||||
if (filter != null && filter.isNotEmpty) {
|
||||
options.retainWhere((option) {
|
||||
final name = option.name.toLowerCase();
|
||||
final lFilter = filter.toLowerCase();
|
||||
|
||||
if (name == lFilter) {
|
||||
createOption = null;
|
||||
}
|
||||
|
||||
return name.contains(lFilter);
|
||||
});
|
||||
} else {
|
||||
createOption = null;
|
||||
}
|
||||
|
||||
return _MakeOptionResult(
|
||||
options: options,
|
||||
createOption: createOption,
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.addListener(
|
||||
onCellChanged: (selectOptionContext) {
|
||||
_loadOptions();
|
||||
},
|
||||
onCellFieldChanged: (field) {
|
||||
_loadOptions();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
|
||||
const factory SelectOptionEditorEvent.initial() = _Initial;
|
||||
const factory SelectOptionEditorEvent.didReceiveOptions(
|
||||
List<SelectOptionPB> options,
|
||||
List<SelectOptionPB> selectedOptions,
|
||||
) = _DidReceiveOptions;
|
||||
const factory SelectOptionEditorEvent.newOption(String optionName) =
|
||||
_NewOption;
|
||||
const factory SelectOptionEditorEvent.selectOption(String optionId) =
|
||||
_SelectOption;
|
||||
const factory SelectOptionEditorEvent.unSelectOption(String optionId) =
|
||||
_UnSelectOption;
|
||||
const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) =
|
||||
_UpdateOption;
|
||||
const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) =
|
||||
_DeleteOption;
|
||||
const factory SelectOptionEditorEvent.deleteAllOptions() = _DeleteAllOptions;
|
||||
const factory SelectOptionEditorEvent.filterOption(String optionName) =
|
||||
_SelectOptionFilter;
|
||||
const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
|
||||
_TrySelectOption;
|
||||
const factory SelectOptionEditorEvent.selectMultipleOptions(
|
||||
List<String> optionNames,
|
||||
String remainder,
|
||||
) = _SelectMultipleOptions;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SelectOptionEditorState with _$SelectOptionEditorState {
|
||||
const factory SelectOptionEditorState({
|
||||
required List<SelectOptionPB> options,
|
||||
required List<SelectOptionPB> allOptions,
|
||||
required List<SelectOptionPB> selectedOptions,
|
||||
required String? createOption,
|
||||
required String? filter,
|
||||
}) = _SelectOptionEditorState;
|
||||
|
||||
factory SelectOptionEditorState.initial(SelectOptionCellController context) {
|
||||
final data = context.getCellData(loadIfNotExist: false);
|
||||
return SelectOptionEditorState(
|
||||
options: data?.options ?? [],
|
||||
allOptions: data?.options ?? [],
|
||||
selectedOptions: data?.selectOptions ?? [],
|
||||
createOption: null,
|
||||
filter: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MakeOptionResult {
|
||||
_MakeOptionResult({
|
||||
required this.options,
|
||||
required this.createOption,
|
||||
});
|
||||
|
||||
List<SelectOptionPB> options;
|
||||
String? createOption;
|
||||
}
|
@ -20,10 +20,10 @@ class SelectOptionTypeOptionBloc
|
||||
void _dispatch() {
|
||||
on<SelectOptionTypeOptionEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
createOption: (optionName) async {
|
||||
event.when(
|
||||
createOption: (optionName) {
|
||||
final List<SelectOptionPB> options =
|
||||
await typeOptionAction.insertOption(state.options, optionName);
|
||||
typeOptionAction.insertOption(state.options, optionName);
|
||||
emit(state.copyWith(options: options));
|
||||
},
|
||||
addingOption: () {
|
||||
@ -33,15 +33,23 @@ class SelectOptionTypeOptionBloc
|
||||
emit(state.copyWith(isEditingOption: false, newOptionName: null));
|
||||
},
|
||||
updateOption: (option) {
|
||||
final List<SelectOptionPB> options =
|
||||
final options =
|
||||
typeOptionAction.updateOption(state.options, option);
|
||||
emit(state.copyWith(options: options));
|
||||
},
|
||||
deleteOption: (option) {
|
||||
final List<SelectOptionPB> options =
|
||||
final options =
|
||||
typeOptionAction.deleteOption(state.options, option);
|
||||
emit(state.copyWith(options: options));
|
||||
},
|
||||
reorderOption: (fromOptionId, toOptionId) {
|
||||
final options = typeOptionAction.reorderOption(
|
||||
state.options,
|
||||
fromOptionId,
|
||||
toOptionId,
|
||||
);
|
||||
emit(state.copyWith(options: options));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -61,6 +69,10 @@ class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {
|
||||
const factory SelectOptionTypeOptionEvent.deleteOption(
|
||||
SelectOptionPB option,
|
||||
) = _DeleteOption;
|
||||
const factory SelectOptionTypeOptionEvent.reorderOption(
|
||||
String fromOptionId,
|
||||
String toOptionId,
|
||||
) = _ReorderOption;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
@ -1,9 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/database/domain/type_option_service.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
abstract class ISelectOptionAction {
|
||||
ISelectOptionAction({
|
||||
@ -20,29 +18,25 @@ abstract class ISelectOptionAction {
|
||||
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
||||
}
|
||||
|
||||
Future<List<SelectOptionPB>> insertOption(
|
||||
List<SelectOptionPB> insertOption(
|
||||
List<SelectOptionPB> options,
|
||||
String optionName,
|
||||
) {
|
||||
final newOptions = List<SelectOptionPB>.from(options);
|
||||
return service.newOption(name: optionName).then((result) {
|
||||
return result.fold(
|
||||
(option) {
|
||||
final exists =
|
||||
newOptions.any((element) => element.name == option.name);
|
||||
if (!exists) {
|
||||
newOptions.insert(0, option);
|
||||
}
|
||||
if (options.any((element) => element.name == optionName)) {
|
||||
return options;
|
||||
}
|
||||
|
||||
updateTypeOption(newOptions);
|
||||
return newOptions;
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
return newOptions;
|
||||
},
|
||||
);
|
||||
});
|
||||
final newOptions = List<SelectOptionPB>.from(options);
|
||||
|
||||
final newSelectOption = SelectOptionPB()
|
||||
..id = nanoid(4)
|
||||
..color = newSelectOptionColor(options)
|
||||
..name = optionName;
|
||||
|
||||
newOptions.insert(0, newSelectOption);
|
||||
|
||||
updateTypeOption(newOptions);
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
List<SelectOptionPB> deleteOption(
|
||||
@ -73,6 +67,25 @@ abstract class ISelectOptionAction {
|
||||
updateTypeOption(newOptions);
|
||||
return newOptions;
|
||||
}
|
||||
|
||||
List<SelectOptionPB> reorderOption(
|
||||
List<SelectOptionPB> options,
|
||||
String fromOptionId,
|
||||
String toOptionId,
|
||||
) {
|
||||
final newOptions = List<SelectOptionPB>.from(options);
|
||||
final fromIndex =
|
||||
newOptions.indexWhere((element) => element.id == fromOptionId);
|
||||
final toIndex =
|
||||
newOptions.indexWhere((element) => element.id == toOptionId);
|
||||
|
||||
if (fromIndex != -1 && toIndex != -1) {
|
||||
newOptions.insert(toIndex, newOptions.removeAt(fromIndex));
|
||||
}
|
||||
|
||||
updateTypeOption(newOptions);
|
||||
return newOptions;
|
||||
}
|
||||
}
|
||||
|
||||
class MultiSelectAction extends ISelectOptionAction {
|
||||
@ -102,3 +115,19 @@ class SingleSelectAction extends ISelectOptionAction {
|
||||
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
SelectOptionColorPB newSelectOptionColor(List<SelectOptionPB> options) {
|
||||
final colorFrequency = List.filled(SelectOptionColorPB.values.length, 0);
|
||||
|
||||
for (final option in options) {
|
||||
colorFrequency[option.color.value]++;
|
||||
}
|
||||
|
||||
final minIndex = colorFrequency
|
||||
.asMap()
|
||||
.entries
|
||||
.reduce((a, b) => a.value <= b.value ? a : b)
|
||||
.key;
|
||||
|
||||
return SelectOptionColorPB.valueOf(minIndex) ?? SelectOptionColorPB.Purple;
|
||||
}
|
||||
|
@ -2,8 +2,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import 'type_option_service.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
class SelectOptionCellBackendService {
|
||||
SelectOptionCellBackendService({
|
||||
@ -18,26 +17,23 @@ class SelectOptionCellBackendService {
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> create({
|
||||
required String name,
|
||||
SelectOptionColorPB? color,
|
||||
bool isSelected = true,
|
||||
}) {
|
||||
return TypeOptionBackendService(viewId: viewId, fieldId: fieldId)
|
||||
.newOption(name: name)
|
||||
.then(
|
||||
(result) {
|
||||
return result.fold(
|
||||
(option) {
|
||||
final payload = RepeatedSelectOptionPayload()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..items.add(option);
|
||||
final option = SelectOptionPB()
|
||||
..id = nanoid(4)
|
||||
..name = name;
|
||||
if (color != null) {
|
||||
option.color = color;
|
||||
}
|
||||
|
||||
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
|
||||
},
|
||||
(r) => FlowyResult.failure(r),
|
||||
);
|
||||
},
|
||||
);
|
||||
final payload = RepeatedSelectOptionPayload()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..items.add(option);
|
||||
|
||||
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> update({
|
||||
|
@ -3,7 +3,7 @@ import 'package:appflowy/plugins/database/grid/application/filter/select_option_
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
|
@ -7,6 +7,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -16,29 +17,29 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size.square(300)),
|
||||
constraints: const BoxConstraints.tightFor(width: 300),
|
||||
margin: EdgeInsets.zero,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
cellContainerNotifier.isFocus = true;
|
||||
});
|
||||
return SelectOptionCellEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -18,12 +19,11 @@ class DesktopRowDetailSelectOptionCellSkin
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return AppFlowyPopover(
|
||||
controller: popoverController,
|
||||
constraints: BoxConstraints.loose(const Size.square(300)),
|
||||
constraints: const BoxConstraints.tightFor(width: 300),
|
||||
margin: EdgeInsets.zero,
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
@ -35,14 +35,18 @@ class DesktopRowDetailSelectOptionCellSkin
|
||||
);
|
||||
},
|
||||
onClose: () => cellContainerNotifier.isFocus = false,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: state.selectedOptions.isEmpty
|
||||
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)
|
||||
: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: state.selectedOptions.isEmpty
|
||||
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)
|
||||
: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -32,7 +32,6 @@ abstract class IEditableSelectOptionCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
);
|
||||
}
|
||||
@ -77,16 +76,11 @@ class _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: cellBloc,
|
||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
state,
|
||||
_popover,
|
||||
);
|
||||
},
|
||||
child: widget.skin.build(
|
||||
context,
|
||||
widget.cellContainerNotifier,
|
||||
cellBloc,
|
||||
_popover,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -17,25 +18,28 @@ class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return FlowyButton(
|
||||
hoverColor: Colors.transparent,
|
||||
radius: BorderRadius.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
text: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return FlowyButton(
|
||||
hoverColor: Colors.transparent,
|
||||
radius: BorderRadius.zero,
|
||||
margin: EdgeInsets.zero,
|
||||
text: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: state.selectedOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../editable_cell_skeleton/select_option.dart';
|
||||
|
||||
@ -20,53 +21,56 @@ class MobileRowDetailSelectOptionCellSkin
|
||||
BuildContext context,
|
||||
CellContainerNotifier cellContainerNotifier,
|
||||
SelectOptionCellBloc bloc,
|
||||
SelectOptionCellState state,
|
||||
PopoverController popoverController,
|
||||
) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 48,
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: state.selectedOptions.isEmpty ? 13 : 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||
builder: (context, state) {
|
||||
return InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
builder: (context) {
|
||||
return MobileSelectOptionEditor(
|
||||
cellController: bloc.cellController,
|
||||
);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 48,
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
const HSpace(6),
|
||||
RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Icon(
|
||||
Icons.chevron_left,
|
||||
color: Theme.of(context).hintColor,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: state.selectedOptions.isEmpty ? 13 : 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.fromBorderSide(
|
||||
BorderSide(color: Theme.of(context).colorScheme.outline),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
const HSpace(2),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: state.selectedOptions.isEmpty
|
||||
? _buildPlaceholder(context)
|
||||
: _buildOptions(context, state.selectedOptions),
|
||||
),
|
||||
const HSpace(6),
|
||||
RotatedBox(
|
||||
quarterTurns: 3,
|
||||
child: Icon(
|
||||
Icons.chevron_left,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
const HSpace(2),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@ extension SelectOptionColorExtension on SelectOptionColorPB {
|
||||
}
|
||||
}
|
||||
|
||||
String optionName() {
|
||||
String colorName() {
|
||||
switch (this) {
|
||||
case SelectOptionColorPB.Purple:
|
||||
return LocaleKeys.grid_selectOption_purpleColor.tr();
|
||||
@ -123,44 +123,3 @@ class SelectOptionTag extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionTagCell extends StatelessWidget {
|
||||
const SelectOptionTagCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.onSelected,
|
||||
this.children = const [],
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final VoidCallback onSelected;
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onSelected,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import 'package:appflowy/mobile/presentation/base/option_color_list.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/base/drag_handler.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
@ -55,8 +55,9 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
child: BlocProvider(
|
||||
create: (context) => SelectOptionCellEditorBloc(
|
||||
cellController: widget.cellController,
|
||||
)..add(const SelectOptionEditorEvent.initial()),
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
),
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc,
|
||||
SelectOptionCellEditorState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -110,7 +111,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
onDelete: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.deleteOption(option!));
|
||||
.add(SelectOptionCellEditorEvent.deleteOption(option!));
|
||||
_popOrBack();
|
||||
},
|
||||
onUpdate: (name, color) {
|
||||
@ -120,7 +121,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
}
|
||||
option.freeze();
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.updateOption(
|
||||
SelectOptionCellEditorEvent.updateOption(
|
||||
option.rebuild((p0) {
|
||||
if (name != null) {
|
||||
p0.name = name;
|
||||
@ -142,16 +143,16 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
_SearchField(
|
||||
controller: searchController,
|
||||
hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(),
|
||||
onSubmitted: (option) {
|
||||
onSubmitted: (_) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(option));
|
||||
.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||
searchController.clear();
|
||||
},
|
||||
onChanged: (value) {
|
||||
typingOption = value;
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.selectMultipleOptions(
|
||||
SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||
[],
|
||||
value,
|
||||
),
|
||||
@ -164,18 +165,18 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
||||
onCreateOption: (optionName) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.newOption(optionName));
|
||||
.add(const SelectOptionCellEditorEvent.createOption());
|
||||
searchController.clear();
|
||||
},
|
||||
onCheck: (option, value) {
|
||||
if (value) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.selectOption(option.id));
|
||||
.add(SelectOptionCellEditorEvent.selectOption(option.id));
|
||||
} else {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.unSelectOption(option.id));
|
||||
.add(SelectOptionCellEditorEvent.unSelectOption(option.id));
|
||||
}
|
||||
},
|
||||
onMoreOptions: (option) {
|
||||
@ -253,18 +254,20 @@ class _OptionList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||
builder: (context, state) {
|
||||
// existing options
|
||||
final List<Widget> cells = [];
|
||||
|
||||
// create an option cell
|
||||
final createOption = state.createOption;
|
||||
if (createOption != null) {
|
||||
if (state.createSelectOptionSuggestion != null) {
|
||||
cells.add(
|
||||
_CreateOptionCell(
|
||||
optionName: createOption,
|
||||
onTap: () => onCreateOption(createOption),
|
||||
name: state.createSelectOptionSuggestion!.name,
|
||||
color: state.createSelectOptionSuggestion!.color,
|
||||
onTap: () => onCreateOption(
|
||||
state.createSelectOptionSuggestion!.name,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -332,14 +335,17 @@ class _SelectOption extends StatelessWidget {
|
||||
const HSpace(12),
|
||||
// option tag
|
||||
Expanded(
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 14,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: 15.0,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
fontSize: 15.0,
|
||||
isExpanded: true,
|
||||
),
|
||||
),
|
||||
const HSpace(24),
|
||||
@ -359,11 +365,13 @@ class _SelectOption extends StatelessWidget {
|
||||
|
||||
class _CreateOptionCell extends StatelessWidget {
|
||||
const _CreateOptionCell({
|
||||
required this.optionName,
|
||||
required this.name,
|
||||
required this.color,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String optionName;
|
||||
final String name;
|
||||
final SelectOptionColorPB color;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
@ -381,13 +389,16 @@ class _CreateOptionCell extends StatelessWidget {
|
||||
),
|
||||
const HSpace(8),
|
||||
Expanded(
|
||||
child: SelectOptionTag(
|
||||
isExpanded: true,
|
||||
name: optionName,
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
textAlign: TextAlign.center,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: SelectOptionTag(
|
||||
name: name,
|
||||
color: color.toColor(context),
|
||||
textAlign: TextAlign.center,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,18 +1,20 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../application/cell/bloc/select_option_editor_bloc.dart';
|
||||
import '../../grid/presentation/layout/sizes.dart';
|
||||
import '../../grid/presentation/widgets/common/type_option_separator.dart';
|
||||
import '../field/type_option_editor/select/select_option_editor.dart';
|
||||
@ -33,39 +35,81 @@ class SelectOptionCellEditor extends StatefulWidget {
|
||||
class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
final TextEditingController textEditingController = TextEditingController();
|
||||
final popoverMutex = PopoverMutex();
|
||||
late final bloc = SelectOptionCellEditorBloc(
|
||||
cellController: widget.cellController,
|
||||
);
|
||||
late final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyUpEvent) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
switch (event.logicalKey) {
|
||||
case LogicalKeyboardKey.arrowUp:
|
||||
if (textEditingController.value.composing.isCollapsed) {
|
||||
bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption());
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
case LogicalKeyboardKey.arrowDown:
|
||||
if (textEditingController.value.composing.isCollapsed) {
|
||||
bloc.add(const SelectOptionCellEditorEvent.focusNextOption());
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
case LogicalKeyboardKey.escape:
|
||||
if (!textEditingController.value.composing.isCollapsed) {
|
||||
final end = textEditingController.value.composing.end;
|
||||
final text = textEditingController.text;
|
||||
|
||||
textEditingController.value = TextEditingValue(
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: end),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
textEditingController.dispose();
|
||||
bloc.close();
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SelectOptionCellEditorBloc(
|
||||
cellController: widget.cellController,
|
||||
)..add(const SelectOptionEditorEvent.initial()),
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_TextField(
|
||||
textEditingController: textEditingController,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
Flexible(
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: TextFieldTapRegion(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_TextField(
|
||||
textEditingController: textEditingController,
|
||||
focusNode: focusNode,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
Flexible(
|
||||
child: Focus(
|
||||
descendantsAreFocusable: false,
|
||||
child: _OptionList(
|
||||
textEditingController: textEditingController,
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -82,60 +126,83 @@ class _OptionList extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||
buildWhen: (previous, current) =>
|
||||
!listEquals(previous.options, current.options) ||
|
||||
previous.createSelectOptionSuggestion !=
|
||||
current.createSelectOptionSuggestion,
|
||||
builder: (context, state) {
|
||||
final cells = [
|
||||
_Title(onPressedAddButton: () => onPressedAddButton(context)),
|
||||
...state.options.map(
|
||||
(option) => _SelectOptionCell(
|
||||
option: option,
|
||||
isSelected: state.selectedOptions.contains(option),
|
||||
popoverMutex: popoverMutex,
|
||||
return ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
proxyDecorator: (child, index, _) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
BlocProvider.value(
|
||||
value: context.read<SelectOptionCellEditorBloc>(),
|
||||
child: child,
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final createOption = state.createOption;
|
||||
if (createOption != null) {
|
||||
cells.add(_CreateOptionCell(name: createOption));
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
shrinkWrap: true,
|
||||
itemCount: cells.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
||||
physics: StyledScrollPhysics(),
|
||||
itemBuilder: (_, int index) => cells[index],
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: state.options.length,
|
||||
onReorderStart: (_) => popoverMutex.close(),
|
||||
itemBuilder: (_, int index) {
|
||||
final option = state.options[index];
|
||||
return _SelectOptionCell(
|
||||
key: ValueKey("select_cell_option_list_${option.id}"),
|
||||
index: index,
|
||||
option: option,
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
},
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromOptionId = state.options[oldIndex].id;
|
||||
final toOptionId = state.options[newIndex].id;
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.reorderOption(
|
||||
fromOptionId,
|
||||
toOptionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
header: const _Title(),
|
||||
footer: state.createSelectOptionSuggestion == null
|
||||
? null
|
||||
: _CreateOptionCell(
|
||||
suggestion: state.createSelectOptionSuggestion!,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void onPressedAddButton(BuildContext context) {
|
||||
final text = textEditingController.text;
|
||||
if (text.isNotEmpty) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(text));
|
||||
}
|
||||
textEditingController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
class _TextField extends StatelessWidget {
|
||||
const _TextField({
|
||||
required this.textEditingController,
|
||||
required this.focusNode,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final TextEditingController textEditingController;
|
||||
final FocusNode focusNode;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||
builder: (context, state) {
|
||||
final optionMap = LinkedHashMap<String, SelectOptionPB>.fromIterable(
|
||||
state.selectedOptions,
|
||||
@ -143,40 +210,46 @@ class _TextField extends StatelessWidget {
|
||||
value: (option) => option,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SelectOptionTextField(
|
||||
options: state.options,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
textController: textEditingController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
newText: (text) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: (tagName) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(tagName));
|
||||
},
|
||||
onPaste: (tagNames, remainder) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.selectMultipleOptions(
|
||||
tagNames,
|
||||
remainder,
|
||||
),
|
||||
);
|
||||
},
|
||||
onRemove: (optionName) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionEditorEvent.unSelectOption(
|
||||
optionMap[optionName]!.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: SelectOptionTextField(
|
||||
options: state.options,
|
||||
focusNode: focusNode,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
textController: textEditingController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
newText: (text) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionCellEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||
textEditingController.clear();
|
||||
focusNode.requestFocus();
|
||||
},
|
||||
onPaste: (tagNames, remainder) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||
tagNames,
|
||||
remainder,
|
||||
),
|
||||
);
|
||||
},
|
||||
onRemove: (optionName) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.unSelectOption(
|
||||
optionMap[optionName]!.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -185,11 +258,7 @@ class _TextField extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _Title extends StatelessWidget {
|
||||
const _Title({
|
||||
required this.onPressedAddButton,
|
||||
});
|
||||
|
||||
final VoidCallback onPressedAddButton;
|
||||
const _Title();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -197,62 +266,9 @@ class _Title extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: FlowyText.medium(
|
||||
LocaleKeys.grid_selectOption_panelTitle.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateOptionCell extends StatelessWidget {
|
||||
const _CreateOptionCell({
|
||||
required this.name,
|
||||
});
|
||||
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: SizedBox(
|
||||
height: 28,
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
onTap: () => context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.newOption(name)),
|
||||
text: Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.grid_selectOption_create.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(10),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectOptionTag(
|
||||
name: name,
|
||||
fontSize: 11,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 1,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.grid_selectOption_panelTitle.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -261,13 +277,14 @@ class _CreateOptionCell extends StatelessWidget {
|
||||
|
||||
class _SelectOptionCell extends StatefulWidget {
|
||||
const _SelectOptionCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.isSelected,
|
||||
required this.index,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final bool isSelected;
|
||||
final int index;
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
@ -285,34 +302,6 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final child = SizedBox(
|
||||
height: 28,
|
||||
child: SelectOptionTagCell(
|
||||
option: widget.option,
|
||||
onSelected: _onTap,
|
||||
children: [
|
||||
if (widget.isSelected)
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: _onTap,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
FlowyIconButton(
|
||||
onPressed: () => _popoverController.show(),
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
hoverColor: Colors.transparent,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return AppFlowyPopover(
|
||||
controller: _popoverController,
|
||||
offset: const Offset(8, 0),
|
||||
@ -322,13 +311,59 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
mutex: widget.popoverMutex,
|
||||
clickHandler: PopoverClickHandler.gestureDetector,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: FlowyHover(
|
||||
resetHoverOnRebuild: false,
|
||||
style: HoverStyle(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
SelectOptionCellEditorEvent.updateFocusedOption(
|
||||
widget.option.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: context
|
||||
.watch<SelectOptionCellEditorBloc>()
|
||||
.state
|
||||
.focusedOptionId ==
|
||||
widget.option.id
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: null,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: SelectOptionTagCell(
|
||||
option: widget.option,
|
||||
index: widget.index,
|
||||
onSelected: _onTap,
|
||||
children: [
|
||||
if (context
|
||||
.watch<SelectOptionCellEditorBloc>()
|
||||
.state
|
||||
.selectedOptions
|
||||
.contains(widget.option))
|
||||
FlowyIconButton(
|
||||
width: 20,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: _onTap,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.check_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
),
|
||||
),
|
||||
FlowyIconButton(
|
||||
onPressed: () => _popoverController.show(),
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
hoverColor: Colors.transparent,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.three_dots_s,
|
||||
size: const Size.square(16),
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
@ -337,13 +372,13 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
onDeleted: () {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.deleteOption(widget.option));
|
||||
.add(SelectOptionCellEditorEvent.deleteOption(widget.option));
|
||||
PopoverContainer.of(popoverContext).close();
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.updateOption(updatedOption));
|
||||
.add(SelectOptionCellEditorEvent.updateOption(updatedOption));
|
||||
},
|
||||
key: ValueKey(
|
||||
widget.option.id,
|
||||
@ -355,14 +390,149 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
|
||||
void _onTap() {
|
||||
widget.popoverMutex.close();
|
||||
if (widget.isSelected) {
|
||||
if (context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.state
|
||||
.selectedOptions
|
||||
.contains(widget.option)) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.unSelectOption(widget.option.id));
|
||||
.add(SelectOptionCellEditorEvent.unSelectOption(widget.option.id));
|
||||
} else {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.selectOption(widget.option.id));
|
||||
.add(SelectOptionCellEditorEvent.selectOption(widget.option.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionTagCell extends StatelessWidget {
|
||||
const SelectOptionTagCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.onSelected,
|
||||
this.children = const [],
|
||||
this.index,
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final VoidCallback onSelected;
|
||||
final List<Widget> children;
|
||||
final int? index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (index != null)
|
||||
ReorderableDragStartListener(
|
||||
index: index!,
|
||||
child: MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grab,
|
||||
child: GestureDetector(
|
||||
onTap: onSelected,
|
||||
child: SizedBox(
|
||||
width: 26,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
FlowySvgs.drag_element_s,
|
||||
size: const Size.square(14),
|
||||
color: Theme.of(context).colorScheme.onBackground,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onSelected,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 5.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...children,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CreateOptionCell extends StatelessWidget {
|
||||
const _CreateOptionCell({
|
||||
required this.suggestion,
|
||||
});
|
||||
|
||||
final CreateSelectOptionSuggestion suggestion;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 28,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
context.watch<SelectOptionCellEditorBloc>().state.focusedOptionId ==
|
||||
createSelectOptionSuggestionId
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: null,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(const SelectOptionCellEditorEvent.createOption()),
|
||||
child: MouseRegion(
|
||||
onEnter: (_) {
|
||||
context.read<SelectOptionCellEditorBloc>().add(
|
||||
const SelectOptionCellEditorEvent.updateFocusedOption(
|
||||
createSelectOptionSuggestionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.grid_selectOption_create.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(10),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectOptionTag(
|
||||
name: suggestion.name,
|
||||
color: suggestion.color.toColor(context),
|
||||
fontSize: 11,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'extension.dart';
|
||||
|
||||
@ -18,6 +15,7 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
required this.distanceToText,
|
||||
required this.textSeparators,
|
||||
required this.textController,
|
||||
required this.focusNode,
|
||||
required this.onSubmitted,
|
||||
required this.newText,
|
||||
required this.onPaste,
|
||||
@ -30,8 +28,9 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
final double distanceToText;
|
||||
final List<String> textSeparators;
|
||||
final TextEditingController textController;
|
||||
final FocusNode focusNode;
|
||||
|
||||
final Function(String) onSubmitted;
|
||||
final Function() onSubmitted;
|
||||
final Function(String) newText;
|
||||
final Function(List<String>, String) onPaste;
|
||||
final Function(String) onRemove;
|
||||
@ -42,32 +41,11 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
late final FocusNode focusNode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
if (!widget.textController.value.composing.isCollapsed) {
|
||||
final TextRange(:start, :end) =
|
||||
widget.textController.value.composing;
|
||||
final text = widget.textController.text;
|
||||
|
||||
widget.textController.value = TextEditingValue(
|
||||
text: "${text.substring(0, start)}${text.substring(end)}",
|
||||
selection: TextSelection(baseOffset: start, extentOffset: start),
|
||||
);
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
widget.focusNode.requestFocus();
|
||||
});
|
||||
widget.textController.addListener(_onChanged);
|
||||
}
|
||||
@ -75,7 +53,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
@override
|
||||
void dispose() {
|
||||
widget.textController.removeListener(_onChanged);
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -83,15 +60,9 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
controller: widget.textController,
|
||||
focusNode: focusNode,
|
||||
focusNode: widget.focusNode,
|
||||
onTap: widget.onClick,
|
||||
onSubmitted: (text) {
|
||||
if (text.isNotEmpty) {
|
||||
widget.onSubmitted(text.trim());
|
||||
focusNode.requestFocus();
|
||||
widget.textController.clear();
|
||||
}
|
||||
},
|
||||
onSubmitted: (_) => widget.onSubmitted(),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
@ -100,11 +71,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
),
|
||||
isDense: true,
|
||||
prefixIcon: _renderTags(context),
|
||||
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.copyWith(color: Theme.of(context).hintColor),
|
||||
prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
@ -148,23 +114,26 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
)
|
||||
.toList();
|
||||
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(spacing: 4, children: children),
|
||||
return Focus(
|
||||
descendantsAreFocusable: false,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.basic,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
dragDevices: {
|
||||
PointerDeviceKind.mouse,
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.trackpad,
|
||||
PointerDeviceKind.stylus,
|
||||
PointerDeviceKind.invertedStylus,
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Wrap(spacing: 4, children: children),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -1,9 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/select_option_type_option_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -48,16 +50,15 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
|
||||
] else
|
||||
const _AddOptionButton(),
|
||||
const VSpace(4),
|
||||
...state.options.map((option) {
|
||||
return _OptionCell(
|
||||
option: option,
|
||||
Flexible(
|
||||
child: _OptionList(
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return ListView(
|
||||
shrinkWrap: true,
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
@ -90,9 +91,15 @@ class _OptionTitle extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _OptionCell extends StatefulWidget {
|
||||
const _OptionCell({required this.option, this.popoverMutex});
|
||||
const _OptionCell({
|
||||
super.key,
|
||||
required this.option,
|
||||
required this.index,
|
||||
this.popoverMutex,
|
||||
});
|
||||
|
||||
final SelectOptionPB option;
|
||||
final int index;
|
||||
final PopoverMutex? popoverMutex;
|
||||
|
||||
@override
|
||||
@ -108,6 +115,7 @@ class _OptionCellState extends State<_OptionCell> {
|
||||
height: 28,
|
||||
child: SelectOptionTagCell(
|
||||
option: widget.option,
|
||||
index: widget.index,
|
||||
onSelected: () => _popoverController.show(),
|
||||
children: [
|
||||
FlowyIconButton(
|
||||
@ -115,8 +123,9 @@ class _OptionCellState extends State<_OptionCell> {
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
hoverColor: Colors.transparent,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
FlowySvgs.three_dots_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -253,3 +262,61 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _OptionList extends StatelessWidget {
|
||||
const _OptionList({
|
||||
this.popoverMutex,
|
||||
});
|
||||
|
||||
final PopoverMutex? popoverMutex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SelectOptionTypeOptionBloc, SelectOptionTypeOptionState>(
|
||||
builder: (context, state) {
|
||||
return ReorderableListView.builder(
|
||||
shrinkWrap: true,
|
||||
onReorderStart: (_) => popoverMutex?.close(),
|
||||
proxyDecorator: (child, index, _) => Material(
|
||||
color: Colors.transparent,
|
||||
child: Stack(
|
||||
children: [
|
||||
BlocProvider.value(
|
||||
value: context.read<SelectOptionTypeOptionBloc>(),
|
||||
child: child,
|
||||
),
|
||||
MouseRegion(
|
||||
cursor: Platform.isWindows
|
||||
? SystemMouseCursors.click
|
||||
: SystemMouseCursors.grabbing,
|
||||
child: const SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) => _OptionCell(
|
||||
key: ValueKey("select_type_option_list_${state.options[index].id}"),
|
||||
index: index,
|
||||
option: state.options[index],
|
||||
popoverMutex: popoverMutex,
|
||||
),
|
||||
itemCount: state.options.length,
|
||||
onReorder: (oldIndex, newIndex) {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex--;
|
||||
}
|
||||
final fromOptionId = state.options[oldIndex].id;
|
||||
final toOptionId = state.options[newIndex].id;
|
||||
context.read<SelectOptionTypeOptionBloc>().add(
|
||||
SelectOptionTypeOptionEvent.reorderOption(
|
||||
fromOptionId,
|
||||
toOptionId,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -230,7 +230,7 @@ class _SelectOptionColorCell extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
color.optionName(),
|
||||
color.colorName(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: colorIcon,
|
||||
|
@ -2,7 +2,7 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/setting/group_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -65,13 +65,13 @@ void main() {
|
||||
context.makeCellControllerFromFieldId(multiSelectField.id)
|
||||
as SelectOptionCellController;
|
||||
|
||||
final multiSelectOptionBloc =
|
||||
SelectOptionCellEditorBloc(cellController: cellController);
|
||||
multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial());
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
await boardResponseFuture();
|
||||
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await boardResponseFuture();
|
||||
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await boardResponseFuture();
|
||||
|
||||
// set grouped by the new multi-select field"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -21,10 +21,10 @@ void main() {
|
||||
);
|
||||
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
bloc.add(const SelectOptionEditorEvent.initial());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
|
||||
expect(bloc.state.options.length, 1);
|
||||
@ -40,16 +40,16 @@ void main() {
|
||||
);
|
||||
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
bloc.add(const SelectOptionEditorEvent.initial());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
|
||||
final SelectOptionPB optionUpdate = bloc.state.options[0]
|
||||
..color = SelectOptionColorPB.Aqua
|
||||
..name = "B";
|
||||
bloc.add(SelectOptionEditorEvent.updateOption(optionUpdate));
|
||||
bloc.add(SelectOptionCellEditorEvent.updateOption(optionUpdate));
|
||||
|
||||
expect(bloc.state.options.length, 1);
|
||||
expect(bloc.state.options[0].name, "B");
|
||||
@ -65,31 +65,33 @@ void main() {
|
||||
);
|
||||
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
bloc.add(const SelectOptionEditorEvent.initial());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
assert(
|
||||
bloc.state.options.length == 1,
|
||||
"Expect 1 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}",
|
||||
);
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
assert(
|
||||
bloc.state.options.length == 2,
|
||||
"Expect 2 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}",
|
||||
);
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("C"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("C"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
assert(
|
||||
bloc.state.options.length == 3,
|
||||
"Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}",
|
||||
);
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.deleteAllOptions());
|
||||
bloc.add(const SelectOptionCellEditorEvent.deleteAllOptions());
|
||||
await gridResponseFuture();
|
||||
|
||||
assert(
|
||||
@ -107,18 +109,18 @@ void main() {
|
||||
);
|
||||
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
bloc.add(const SelectOptionEditorEvent.initial());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
|
||||
final optionId = bloc.state.options[0].id;
|
||||
bloc.add(SelectOptionEditorEvent.unSelectOption(optionId));
|
||||
bloc.add(SelectOptionCellEditorEvent.unSelectOption(optionId));
|
||||
await gridResponseFuture();
|
||||
assert(bloc.state.selectedOptions.isEmpty);
|
||||
|
||||
bloc.add(SelectOptionEditorEvent.selectOption(optionId));
|
||||
bloc.add(SelectOptionCellEditorEvent.selectOption(optionId));
|
||||
await gridResponseFuture();
|
||||
|
||||
assert(bloc.state.selectedOptions.length == 1);
|
||||
@ -134,20 +136,22 @@ void main() {
|
||||
);
|
||||
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
bloc.add(const SelectOptionEditorEvent.initial());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.trySelectOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.trySelectOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||
await gridResponseFuture();
|
||||
|
||||
assert(bloc.state.selectedOptions.length == 1);
|
||||
assert(bloc.state.options.length == 2);
|
||||
expect(bloc.state.selectedOptions.length, 1);
|
||||
expect(bloc.state.options.length, 1);
|
||||
expect(bloc.state.selectedOptions[0].name, "A");
|
||||
});
|
||||
|
||||
@ -160,17 +164,18 @@ void main() {
|
||||
);
|
||||
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
bloc.add(const SelectOptionEditorEvent.initial());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("B"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(
|
||||
const SelectOptionEditorEvent.selectMultipleOptions(
|
||||
const SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||
["A", "B", "C"],
|
||||
"x",
|
||||
),
|
||||
@ -191,10 +196,10 @@ void main() {
|
||||
);
|
||||
|
||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||
bloc.add(const SelectOptionEditorEvent.initial());
|
||||
await gridResponseFuture();
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("abcd"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("abcd"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
expect(
|
||||
bloc.state.options.length,
|
||||
@ -202,7 +207,8 @@ void main() {
|
||||
reason: "Options: ${bloc.state.options}",
|
||||
);
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("aaaa"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("aaaa"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
expect(
|
||||
bloc.state.options.length,
|
||||
@ -210,7 +216,8 @@ void main() {
|
||||
reason: "Options: ${bloc.state.options}",
|
||||
);
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.newOption("defg"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("defg"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||
await gridResponseFuture();
|
||||
expect(
|
||||
bloc.state.options.length,
|
||||
@ -218,7 +225,7 @@ void main() {
|
||||
reason: "Options: ${bloc.state.options}",
|
||||
);
|
||||
|
||||
bloc.add(const SelectOptionEditorEvent.filterOption("a"));
|
||||
bloc.add(const SelectOptionCellEditorEvent.filterOption("a"));
|
||||
await gridResponseFuture();
|
||||
|
||||
expect(
|
||||
@ -231,7 +238,7 @@ void main() {
|
||||
3,
|
||||
reason: "Options: ${bloc.state.options}",
|
||||
);
|
||||
expect(bloc.state.createOption, "a");
|
||||
expect(bloc.state.createSelectOptionSuggestion!.name, "a");
|
||||
expect(bloc.state.filter, "a");
|
||||
});
|
||||
});
|
||||
|
@ -17,11 +17,13 @@ void main() {
|
||||
String remainder = '';
|
||||
List<String> select = [];
|
||||
|
||||
final textController = TextEditingController();
|
||||
|
||||
final textField = SelectOptionTextField(
|
||||
options: const [],
|
||||
selectedOptionMap: LinkedHashMap<String, SelectOptionPB>(),
|
||||
distanceToText: 0.0,
|
||||
onSubmitted: (text) => submit = text,
|
||||
onSubmitted: () => submit = textController.text,
|
||||
onPaste: (options, remaining) {
|
||||
remainder = remaining;
|
||||
select = options;
|
||||
@ -29,7 +31,8 @@ void main() {
|
||||
onRemove: (_) {},
|
||||
newText: (text) => remainder = text,
|
||||
textSeparators: const [','],
|
||||
textController: TextEditingController(),
|
||||
textController: textController,
|
||||
focusNode: FocusNode(),
|
||||
);
|
||||
|
||||
testWidgets('SelectOptionTextField callback outputs',
|
||||
@ -57,11 +60,6 @@ void main() {
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
expect(submit, 'an option');
|
||||
|
||||
submit = '';
|
||||
await tester.enterText(find.byType(TextField), ' ');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
expect(submit, '');
|
||||
|
||||
// test inputs containing commas
|
||||
await tester.enterText(find.byType(TextField), 'a a, bbbb , c');
|
||||
expect(remainder, 'c');
|
||||
|
@ -736,9 +736,9 @@
|
||||
"blueColor": "Blue",
|
||||
"deleteTag": "Delete tag",
|
||||
"colorPanelTitle": "Color",
|
||||
"panelTitle": "Select an option",
|
||||
"panelTitle": "Select an option or create one",
|
||||
"searchOption": "Search for an option",
|
||||
"searchOrCreateOption": "Search...",
|
||||
"searchOrCreateOption": "Search for an option or create one",
|
||||
"createNew": "Create a new",
|
||||
"orSelectOne": "Or select an option",
|
||||
"typeANewOption": "Type a new option",
|
||||
|
Loading…
Reference in New Issue
Block a user