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() {
|
void _dispatch() {
|
||||||
on<SelectOptionTypeOptionEvent>(
|
on<SelectOptionTypeOptionEvent>(
|
||||||
(event, emit) async {
|
(event, emit) async {
|
||||||
await event.when(
|
event.when(
|
||||||
createOption: (optionName) async {
|
createOption: (optionName) {
|
||||||
final List<SelectOptionPB> options =
|
final List<SelectOptionPB> options =
|
||||||
await typeOptionAction.insertOption(state.options, optionName);
|
typeOptionAction.insertOption(state.options, optionName);
|
||||||
emit(state.copyWith(options: options));
|
emit(state.copyWith(options: options));
|
||||||
},
|
},
|
||||||
addingOption: () {
|
addingOption: () {
|
||||||
@ -33,15 +33,23 @@ class SelectOptionTypeOptionBloc
|
|||||||
emit(state.copyWith(isEditingOption: false, newOptionName: null));
|
emit(state.copyWith(isEditingOption: false, newOptionName: null));
|
||||||
},
|
},
|
||||||
updateOption: (option) {
|
updateOption: (option) {
|
||||||
final List<SelectOptionPB> options =
|
final options =
|
||||||
typeOptionAction.updateOption(state.options, option);
|
typeOptionAction.updateOption(state.options, option);
|
||||||
emit(state.copyWith(options: options));
|
emit(state.copyWith(options: options));
|
||||||
},
|
},
|
||||||
deleteOption: (option) {
|
deleteOption: (option) {
|
||||||
final List<SelectOptionPB> options =
|
final options =
|
||||||
typeOptionAction.deleteOption(state.options, option);
|
typeOptionAction.deleteOption(state.options, option);
|
||||||
emit(state.copyWith(options: options));
|
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(
|
const factory SelectOptionTypeOptionEvent.deleteOption(
|
||||||
SelectOptionPB option,
|
SelectOptionPB option,
|
||||||
) = _DeleteOption;
|
) = _DeleteOption;
|
||||||
|
const factory SelectOptionTypeOptionEvent.reorderOption(
|
||||||
|
String fromOptionId,
|
||||||
|
String toOptionId,
|
||||||
|
) = _ReorderOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:appflowy/plugins/database/domain/type_option_service.dart';
|
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/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:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||||
|
import 'package:nanoid/nanoid.dart';
|
||||||
|
|
||||||
abstract class ISelectOptionAction {
|
abstract class ISelectOptionAction {
|
||||||
ISelectOptionAction({
|
ISelectOptionAction({
|
||||||
@ -20,29 +18,25 @@ abstract class ISelectOptionAction {
|
|||||||
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SelectOptionPB>> insertOption(
|
List<SelectOptionPB> insertOption(
|
||||||
List<SelectOptionPB> options,
|
List<SelectOptionPB> options,
|
||||||
String optionName,
|
String optionName,
|
||||||
) {
|
) {
|
||||||
final newOptions = List<SelectOptionPB>.from(options);
|
if (options.any((element) => element.name == optionName)) {
|
||||||
return service.newOption(name: optionName).then((result) {
|
return options;
|
||||||
return result.fold(
|
|
||||||
(option) {
|
|
||||||
final exists =
|
|
||||||
newOptions.any((element) => element.name == option.name);
|
|
||||||
if (!exists) {
|
|
||||||
newOptions.insert(0, option);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final newOptions = List<SelectOptionPB>.from(options);
|
||||||
|
|
||||||
|
final newSelectOption = SelectOptionPB()
|
||||||
|
..id = nanoid(4)
|
||||||
|
..color = newSelectOptionColor(options)
|
||||||
|
..name = optionName;
|
||||||
|
|
||||||
|
newOptions.insert(0, newSelectOption);
|
||||||
|
|
||||||
updateTypeOption(newOptions);
|
updateTypeOption(newOptions);
|
||||||
return newOptions;
|
return newOptions;
|
||||||
},
|
|
||||||
(err) {
|
|
||||||
Log.error(err);
|
|
||||||
return newOptions;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<SelectOptionPB> deleteOption(
|
List<SelectOptionPB> deleteOption(
|
||||||
@ -73,6 +67,25 @@ abstract class ISelectOptionAction {
|
|||||||
updateTypeOption(newOptions);
|
updateTypeOption(newOptions);
|
||||||
return 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 {
|
class MultiSelectAction extends ISelectOptionAction {
|
||||||
@ -102,3 +115,19 @@ class SingleSelectAction extends ISelectOptionAction {
|
|||||||
onTypeOptionUpdated(newTypeOption.writeToBuffer());
|
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-database2/protobuf.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||||
import 'package:appflowy_result/appflowy_result.dart';
|
import 'package:appflowy_result/appflowy_result.dart';
|
||||||
|
import 'package:nanoid/nanoid.dart';
|
||||||
import 'type_option_service.dart';
|
|
||||||
|
|
||||||
class SelectOptionCellBackendService {
|
class SelectOptionCellBackendService {
|
||||||
SelectOptionCellBackendService({
|
SelectOptionCellBackendService({
|
||||||
@ -18,14 +17,16 @@ class SelectOptionCellBackendService {
|
|||||||
|
|
||||||
Future<FlowyResult<void, FlowyError>> create({
|
Future<FlowyResult<void, FlowyError>> create({
|
||||||
required String name,
|
required String name,
|
||||||
|
SelectOptionColorPB? color,
|
||||||
bool isSelected = true,
|
bool isSelected = true,
|
||||||
}) {
|
}) {
|
||||||
return TypeOptionBackendService(viewId: viewId, fieldId: fieldId)
|
final option = SelectOptionPB()
|
||||||
.newOption(name: name)
|
..id = nanoid(4)
|
||||||
.then(
|
..name = name;
|
||||||
(result) {
|
if (color != null) {
|
||||||
return result.fold(
|
option.color = color;
|
||||||
(option) {
|
}
|
||||||
|
|
||||||
final payload = RepeatedSelectOptionPayload()
|
final payload = RepeatedSelectOptionPayload()
|
||||||
..viewId = viewId
|
..viewId = viewId
|
||||||
..fieldId = fieldId
|
..fieldId = fieldId
|
||||||
@ -33,11 +34,6 @@ class SelectOptionCellBackendService {
|
|||||||
..items.add(option);
|
..items.add(option);
|
||||||
|
|
||||||
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
|
return DatabaseEventInsertOrUpdateSelectOption(payload).send();
|
||||||
},
|
|
||||||
(r) => FlowyResult.failure(r),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<FlowyResult<void, FlowyError>> update({
|
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/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/layout/sizes.dart';
|
||||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.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/field_entities.pbenum.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||||
import 'package:flowy_infra_ui/widget/spacing.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:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../editable_cell_skeleton/select_option.dart';
|
import '../editable_cell_skeleton/select_option.dart';
|
||||||
|
|
||||||
@ -16,29 +17,29 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
CellContainerNotifier cellContainerNotifier,
|
CellContainerNotifier cellContainerNotifier,
|
||||||
SelectOptionCellBloc bloc,
|
SelectOptionCellBloc bloc,
|
||||||
SelectOptionCellState state,
|
|
||||||
PopoverController popoverController,
|
PopoverController popoverController,
|
||||||
) {
|
) {
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
constraints: BoxConstraints.loose(const Size.square(300)),
|
constraints: const BoxConstraints.tightFor(width: 300),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
cellContainerNotifier.isFocus = true;
|
|
||||||
});
|
|
||||||
return SelectOptionCellEditor(
|
return SelectOptionCellEditor(
|
||||||
cellController: bloc.cellController,
|
cellController: bloc.cellController,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onClose: () => cellContainerNotifier.isFocus = false,
|
onClose: () => cellContainerNotifier.isFocus = false,
|
||||||
child: Container(
|
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Container(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
padding: GridSize.cellContentInsets,
|
padding: GridSize.cellContentInsets,
|
||||||
child: state.selectedOptions.isEmpty
|
child: state.selectedOptions.isEmpty
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: _buildOptions(context, state.selectedOptions),
|
: _buildOptions(context, state.selectedOptions),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../editable_cell_skeleton/select_option.dart';
|
import '../editable_cell_skeleton/select_option.dart';
|
||||||
|
|
||||||
@ -18,12 +19,11 @@ class DesktopRowDetailSelectOptionCellSkin
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
CellContainerNotifier cellContainerNotifier,
|
CellContainerNotifier cellContainerNotifier,
|
||||||
SelectOptionCellBloc bloc,
|
SelectOptionCellBloc bloc,
|
||||||
SelectOptionCellState state,
|
|
||||||
PopoverController popoverController,
|
PopoverController popoverController,
|
||||||
) {
|
) {
|
||||||
return AppFlowyPopover(
|
return AppFlowyPopover(
|
||||||
controller: popoverController,
|
controller: popoverController,
|
||||||
constraints: BoxConstraints.loose(const Size.square(300)),
|
constraints: const BoxConstraints.tightFor(width: 300),
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
@ -35,7 +35,9 @@ class DesktopRowDetailSelectOptionCellSkin
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onClose: () => cellContainerNotifier.isFocus = false,
|
onClose: () => cellContainerNotifier.isFocus = false,
|
||||||
child: Container(
|
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
return Container(
|
||||||
alignment: AlignmentDirectional.centerStart,
|
alignment: AlignmentDirectional.centerStart,
|
||||||
padding: state.selectedOptions.isEmpty
|
padding: state.selectedOptions.isEmpty
|
||||||
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)
|
? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0)
|
||||||
@ -43,6 +45,8 @@ class DesktopRowDetailSelectOptionCellSkin
|
|||||||
child: state.selectedOptions.isEmpty
|
child: state.selectedOptions.isEmpty
|
||||||
? _buildPlaceholder(context)
|
? _buildPlaceholder(context)
|
||||||
: _buildOptions(context, state.selectedOptions),
|
: _buildOptions(context, state.selectedOptions),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ abstract class IEditableSelectOptionCellSkin {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
CellContainerNotifier cellContainerNotifier,
|
CellContainerNotifier cellContainerNotifier,
|
||||||
SelectOptionCellBloc bloc,
|
SelectOptionCellBloc bloc,
|
||||||
SelectOptionCellState state,
|
|
||||||
PopoverController popoverController,
|
PopoverController popoverController,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -77,16 +76,11 @@ class _SelectOptionCellState extends GridCellState<EditableSelectOptionCell> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: cellBloc,
|
value: cellBloc,
|
||||||
child: BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
child: widget.skin.build(
|
||||||
builder: (context, state) {
|
|
||||||
return widget.skin.build(
|
|
||||||
context,
|
context,
|
||||||
widget.cellContainerNotifier,
|
widget.cellContainerNotifier,
|
||||||
cellBloc,
|
cellBloc,
|
||||||
state,
|
|
||||||
_popover,
|
_popover,
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../editable_cell_skeleton/select_option.dart';
|
import '../editable_cell_skeleton/select_option.dart';
|
||||||
|
|
||||||
@ -17,9 +18,10 @@ class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
CellContainerNotifier cellContainerNotifier,
|
CellContainerNotifier cellContainerNotifier,
|
||||||
SelectOptionCellBloc bloc,
|
SelectOptionCellBloc bloc,
|
||||||
SelectOptionCellState state,
|
|
||||||
PopoverController popoverController,
|
PopoverController popoverController,
|
||||||
) {
|
) {
|
||||||
|
return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
return FlowyButton(
|
return FlowyButton(
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
radius: BorderRadius.zero,
|
radius: BorderRadius.zero,
|
||||||
@ -41,6 +43,8 @@ class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {
|
Widget _buildOptions(BuildContext context, List<SelectOptionPB> options) {
|
||||||
|
@ -10,6 +10,7 @@ import 'package:collection/collection.dart';
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../editable_cell_skeleton/select_option.dart';
|
import '../editable_cell_skeleton/select_option.dart';
|
||||||
|
|
||||||
@ -20,9 +21,10 @@ class MobileRowDetailSelectOptionCellSkin
|
|||||||
BuildContext context,
|
BuildContext context,
|
||||||
CellContainerNotifier cellContainerNotifier,
|
CellContainerNotifier cellContainerNotifier,
|
||||||
SelectOptionCellBloc bloc,
|
SelectOptionCellBloc bloc,
|
||||||
SelectOptionCellState state,
|
|
||||||
PopoverController popoverController,
|
PopoverController popoverController,
|
||||||
) {
|
) {
|
||||||
|
return BlocBuilder<SelectOptionCellBloc, SelectOptionCellState>(
|
||||||
|
builder: (context, state) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
borderRadius: const BorderRadius.all(Radius.circular(14)),
|
||||||
onTap: () => showMobileBottomSheet(
|
onTap: () => showMobileBottomSheet(
|
||||||
@ -68,6 +70,8 @@ class MobileRowDetailSelectOptionCellSkin
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlaceholder(BuildContext context) {
|
Widget _buildPlaceholder(BuildContext context) {
|
||||||
|
@ -33,7 +33,7 @@ extension SelectOptionColorExtension on SelectOptionColorPB {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String optionName() {
|
String colorName() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case SelectOptionColorPB.Purple:
|
case SelectOptionColorPB.Purple:
|
||||||
return LocaleKeys.grid_selectOption_purpleColor.tr();
|
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/flowy_mobile_search_text_field.dart';
|
||||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||||
import 'package:appflowy/plugins/base/drag_handler.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/application/cell/cell_controller_builder.dart';
|
||||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||||
@ -55,8 +55,9 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) => SelectOptionCellEditorBloc(
|
create: (context) => SelectOptionCellEditorBloc(
|
||||||
cellController: widget.cellController,
|
cellController: widget.cellController,
|
||||||
)..add(const SelectOptionEditorEvent.initial()),
|
),
|
||||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
child: BlocBuilder<SelectOptionCellEditorBloc,
|
||||||
|
SelectOptionCellEditorState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -110,7 +111,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
onDelete: () {
|
onDelete: () {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.deleteOption(option!));
|
.add(SelectOptionCellEditorEvent.deleteOption(option!));
|
||||||
_popOrBack();
|
_popOrBack();
|
||||||
},
|
},
|
||||||
onUpdate: (name, color) {
|
onUpdate: (name, color) {
|
||||||
@ -120,7 +121,7 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
}
|
}
|
||||||
option.freeze();
|
option.freeze();
|
||||||
context.read<SelectOptionCellEditorBloc>().add(
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
SelectOptionEditorEvent.updateOption(
|
SelectOptionCellEditorEvent.updateOption(
|
||||||
option.rebuild((p0) {
|
option.rebuild((p0) {
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
p0.name = name;
|
p0.name = name;
|
||||||
@ -142,16 +143,16 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
_SearchField(
|
_SearchField(
|
||||||
controller: searchController,
|
controller: searchController,
|
||||||
hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(),
|
hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(),
|
||||||
onSubmitted: (option) {
|
onSubmitted: (_) {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.trySelectOption(option));
|
.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||||
searchController.clear();
|
searchController.clear();
|
||||||
},
|
},
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
typingOption = value;
|
typingOption = value;
|
||||||
context.read<SelectOptionCellEditorBloc>().add(
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
SelectOptionEditorEvent.selectMultipleOptions(
|
SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||||
[],
|
[],
|
||||||
value,
|
value,
|
||||||
),
|
),
|
||||||
@ -164,18 +165,18 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
|
|||||||
onCreateOption: (optionName) {
|
onCreateOption: (optionName) {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.newOption(optionName));
|
.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
searchController.clear();
|
searchController.clear();
|
||||||
},
|
},
|
||||||
onCheck: (option, value) {
|
onCheck: (option, value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.selectOption(option.id));
|
.add(SelectOptionCellEditorEvent.selectOption(option.id));
|
||||||
} else {
|
} else {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.unSelectOption(option.id));
|
.add(SelectOptionCellEditorEvent.unSelectOption(option.id));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMoreOptions: (option) {
|
onMoreOptions: (option) {
|
||||||
@ -253,18 +254,20 @@ class _OptionList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// existing options
|
// existing options
|
||||||
final List<Widget> cells = [];
|
final List<Widget> cells = [];
|
||||||
|
|
||||||
// create an option cell
|
// create an option cell
|
||||||
final createOption = state.createOption;
|
if (state.createSelectOptionSuggestion != null) {
|
||||||
if (createOption != null) {
|
|
||||||
cells.add(
|
cells.add(
|
||||||
_CreateOptionCell(
|
_CreateOptionCell(
|
||||||
optionName: createOption,
|
name: state.createSelectOptionSuggestion!.name,
|
||||||
onTap: () => onCreateOption(createOption),
|
color: state.createSelectOptionSuggestion!.color,
|
||||||
|
onTap: () => onCreateOption(
|
||||||
|
state.createSelectOptionSuggestion!.name,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -332,14 +335,17 @@ class _SelectOption extends StatelessWidget {
|
|||||||
const HSpace(12),
|
const HSpace(12),
|
||||||
// option tag
|
// option tag
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
child: SelectOptionTag(
|
child: SelectOptionTag(
|
||||||
option: option,
|
option: option,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10,
|
vertical: 10,
|
||||||
|
horizontal: 14,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
fontSize: 15.0,
|
fontSize: 15.0,
|
||||||
isExpanded: true,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const HSpace(24),
|
const HSpace(24),
|
||||||
@ -359,11 +365,13 @@ class _SelectOption extends StatelessWidget {
|
|||||||
|
|
||||||
class _CreateOptionCell extends StatelessWidget {
|
class _CreateOptionCell extends StatelessWidget {
|
||||||
const _CreateOptionCell({
|
const _CreateOptionCell({
|
||||||
required this.optionName,
|
required this.name,
|
||||||
|
required this.color,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String optionName;
|
final String name;
|
||||||
|
final SelectOptionColorPB color;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -381,13 +389,16 @@ class _CreateOptionCell extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const HSpace(8),
|
const HSpace(8),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
child: SelectOptionTag(
|
child: SelectOptionTag(
|
||||||
isExpanded: true,
|
name: name,
|
||||||
name: optionName,
|
color: color.toColor(context),
|
||||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 10,
|
vertical: 10,
|
||||||
|
horizontal: 14,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/plugins/database/application/cell/cell_controller_builder.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra/theme_extension.dart';
|
import 'package:flowy_infra/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.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/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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/layout/sizes.dart';
|
||||||
import '../../grid/presentation/widgets/common/type_option_separator.dart';
|
import '../../grid/presentation/widgets/common/type_option_separator.dart';
|
||||||
import '../field/type_option_editor/select/select_option_editor.dart';
|
import '../field/type_option_editor/select/select_option_editor.dart';
|
||||||
@ -33,39 +35,81 @@ class SelectOptionCellEditor extends StatefulWidget {
|
|||||||
class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||||
final TextEditingController textEditingController = TextEditingController();
|
final TextEditingController textEditingController = TextEditingController();
|
||||||
final popoverMutex = PopoverMutex();
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
popoverMutex.dispose();
|
popoverMutex.dispose();
|
||||||
textEditingController.dispose();
|
textEditingController.dispose();
|
||||||
|
bloc.close();
|
||||||
|
focusNode.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider.value(
|
||||||
create: (context) => SelectOptionCellEditorBloc(
|
value: bloc,
|
||||||
cellController: widget.cellController,
|
child: TextFieldTapRegion(
|
||||||
)..add(const SelectOptionEditorEvent.initial()),
|
child: Column(
|
||||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
_TextField(
|
_TextField(
|
||||||
textEditingController: textEditingController,
|
textEditingController: textEditingController,
|
||||||
|
focusNode: focusNode,
|
||||||
popoverMutex: popoverMutex,
|
popoverMutex: popoverMutex,
|
||||||
),
|
),
|
||||||
const TypeOptionSeparator(spacing: 0.0),
|
const TypeOptionSeparator(spacing: 0.0),
|
||||||
Flexible(
|
Flexible(
|
||||||
|
child: Focus(
|
||||||
|
descendantsAreFocusable: false,
|
||||||
child: _OptionList(
|
child: _OptionList(
|
||||||
textEditingController: textEditingController,
|
textEditingController: textEditingController,
|
||||||
popoverMutex: popoverMutex,
|
popoverMutex: popoverMutex,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -82,60 +126,83 @@ class _OptionList extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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) {
|
builder: (context, state) {
|
||||||
final cells = [
|
return ReorderableListView.builder(
|
||||||
_Title(onPressedAddButton: () => onPressedAddButton(context)),
|
|
||||||
...state.options.map(
|
|
||||||
(option) => _SelectOptionCell(
|
|
||||||
option: option,
|
|
||||||
isSelected: state.selectedOptions.contains(option),
|
|
||||||
popoverMutex: popoverMutex,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
final createOption = state.createOption;
|
|
||||||
if (createOption != null) {
|
|
||||||
cells.add(_CreateOptionCell(name: createOption));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.separated(
|
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: cells.length,
|
proxyDecorator: (child, index, _) => Material(
|
||||||
separatorBuilder: (_, __) =>
|
color: Colors.transparent,
|
||||||
VSpace(GridSize.typeOptionSeparatorHeight),
|
child: Stack(
|
||||||
physics: StyledScrollPhysics(),
|
children: [
|
||||||
itemBuilder: (_, int index) => cells[index],
|
BlocProvider.value(
|
||||||
|
value: context.read<SelectOptionCellEditorBloc>(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
MouseRegion(
|
||||||
|
cursor: Platform.isWindows
|
||||||
|
? SystemMouseCursors.click
|
||||||
|
: SystemMouseCursors.grabbing,
|
||||||
|
child: const SizedBox.expand(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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),
|
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 {
|
class _TextField extends StatelessWidget {
|
||||||
const _TextField({
|
const _TextField({
|
||||||
required this.textEditingController,
|
required this.textEditingController,
|
||||||
|
required this.focusNode,
|
||||||
required this.popoverMutex,
|
required this.popoverMutex,
|
||||||
});
|
});
|
||||||
|
|
||||||
final TextEditingController textEditingController;
|
final TextEditingController textEditingController;
|
||||||
|
final FocusNode focusNode;
|
||||||
final PopoverMutex popoverMutex;
|
final PopoverMutex popoverMutex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
return BlocBuilder<SelectOptionCellEditorBloc, SelectOptionCellEditorState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final optionMap = LinkedHashMap<String, SelectOptionPB>.fromIterable(
|
final optionMap = LinkedHashMap<String, SelectOptionPB>.fromIterable(
|
||||||
state.selectedOptions,
|
state.selectedOptions,
|
||||||
@ -143,10 +210,13 @@ class _TextField extends StatelessWidget {
|
|||||||
value: (option) => option,
|
value: (option) => option,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Padding(
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
child: SelectOptionTextField(
|
child: SelectOptionTextField(
|
||||||
options: state.options,
|
options: state.options,
|
||||||
|
focusNode: focusNode,
|
||||||
selectedOptionMap: optionMap,
|
selectedOptionMap: optionMap,
|
||||||
distanceToText: _editorPanelWidth * 0.7,
|
distanceToText: _editorPanelWidth * 0.7,
|
||||||
textController: textEditingController,
|
textController: textEditingController,
|
||||||
@ -155,16 +225,18 @@ class _TextField extends StatelessWidget {
|
|||||||
newText: (text) {
|
newText: (text) {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.filterOption(text));
|
.add(SelectOptionCellEditorEvent.filterOption(text));
|
||||||
},
|
},
|
||||||
onSubmitted: (tagName) {
|
onSubmitted: () {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.trySelectOption(tagName));
|
.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||||
|
textEditingController.clear();
|
||||||
|
focusNode.requestFocus();
|
||||||
},
|
},
|
||||||
onPaste: (tagNames, remainder) {
|
onPaste: (tagNames, remainder) {
|
||||||
context.read<SelectOptionCellEditorBloc>().add(
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
SelectOptionEditorEvent.selectMultipleOptions(
|
SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||||
tagNames,
|
tagNames,
|
||||||
remainder,
|
remainder,
|
||||||
),
|
),
|
||||||
@ -172,12 +244,13 @@ class _TextField extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
onRemove: (optionName) {
|
onRemove: (optionName) {
|
||||||
context.read<SelectOptionCellEditorBloc>().add(
|
context.read<SelectOptionCellEditorBloc>().add(
|
||||||
SelectOptionEditorEvent.unSelectOption(
|
SelectOptionCellEditorEvent.unSelectOption(
|
||||||
optionMap[optionName]!.id,
|
optionMap[optionName]!.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -185,11 +258,7 @@ class _TextField extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _Title extends StatelessWidget {
|
class _Title extends StatelessWidget {
|
||||||
const _Title({
|
const _Title();
|
||||||
required this.onPressedAddButton,
|
|
||||||
});
|
|
||||||
|
|
||||||
final VoidCallback onPressedAddButton;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -197,77 +266,25 @@ class _Title extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: GridSize.popoverItemHeight,
|
height: GridSize.popoverItemHeight,
|
||||||
child: Row(
|
child: FlowyText.regular(
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: FlowyText.medium(
|
|
||||||
LocaleKeys.grid_selectOption_panelTitle.tr(),
|
LocaleKeys.grid_selectOption_panelTitle.tr(),
|
||||||
color: Theme.of(context).hintColor,
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SelectOptionCell extends StatefulWidget {
|
class _SelectOptionCell extends StatefulWidget {
|
||||||
const _SelectOptionCell({
|
const _SelectOptionCell({
|
||||||
|
super.key,
|
||||||
required this.option,
|
required this.option,
|
||||||
required this.isSelected,
|
required this.index,
|
||||||
required this.popoverMutex,
|
required this.popoverMutex,
|
||||||
});
|
});
|
||||||
|
|
||||||
final SelectOptionPB option;
|
final SelectOptionPB option;
|
||||||
final bool isSelected;
|
final int index;
|
||||||
final PopoverMutex popoverMutex;
|
final PopoverMutex popoverMutex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -285,13 +302,46 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final child = SizedBox(
|
return AppFlowyPopover(
|
||||||
|
controller: _popoverController,
|
||||||
|
offset: const Offset(8, 0),
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
asBarrier: true,
|
||||||
|
constraints: BoxConstraints.loose(const Size(200, 470)),
|
||||||
|
mutex: widget.popoverMutex,
|
||||||
|
clickHandler: PopoverClickHandler.gestureDetector,
|
||||||
|
child: Padding(
|
||||||
|
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,
|
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(
|
child: SelectOptionTagCell(
|
||||||
option: widget.option,
|
option: widget.option,
|
||||||
|
index: widget.index,
|
||||||
onSelected: _onTap,
|
onSelected: _onTap,
|
||||||
children: [
|
children: [
|
||||||
if (widget.isSelected)
|
if (context
|
||||||
|
.watch<SelectOptionCellEditorBloc>()
|
||||||
|
.state
|
||||||
|
.selectedOptions
|
||||||
|
.contains(widget.option))
|
||||||
FlowyIconButton(
|
FlowyIconButton(
|
||||||
width: 20,
|
width: 20,
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
@ -306,29 +356,14 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
|||||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
icon: FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.details_s,
|
FlowySvgs.three_dots_s,
|
||||||
color: Theme.of(context).iconTheme.color,
|
size: const Size.square(16),
|
||||||
|
color: Theme.of(context).colorScheme.onBackground,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
return AppFlowyPopover(
|
|
||||||
controller: _popoverController,
|
|
||||||
offset: const Offset(8, 0),
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
asBarrier: true,
|
|
||||||
constraints: BoxConstraints.loose(const Size(200, 470)),
|
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
child: child,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
popupBuilder: (BuildContext popoverContext) {
|
popupBuilder: (BuildContext popoverContext) {
|
||||||
@ -337,13 +372,13 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
|||||||
onDeleted: () {
|
onDeleted: () {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.deleteOption(widget.option));
|
.add(SelectOptionCellEditorEvent.deleteOption(widget.option));
|
||||||
PopoverContainer.of(popoverContext).close();
|
PopoverContainer.of(popoverContext).close();
|
||||||
},
|
},
|
||||||
onUpdated: (updatedOption) {
|
onUpdated: (updatedOption) {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.updateOption(updatedOption));
|
.add(SelectOptionCellEditorEvent.updateOption(updatedOption));
|
||||||
},
|
},
|
||||||
key: ValueKey(
|
key: ValueKey(
|
||||||
widget.option.id,
|
widget.option.id,
|
||||||
@ -355,14 +390,149 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
|||||||
|
|
||||||
void _onTap() {
|
void _onTap() {
|
||||||
widget.popoverMutex.close();
|
widget.popoverMutex.close();
|
||||||
if (widget.isSelected) {
|
if (context
|
||||||
|
.read<SelectOptionCellEditorBloc>()
|
||||||
|
.state
|
||||||
|
.selectedOptions
|
||||||
|
.contains(widget.option)) {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.read<SelectOptionCellEditorBloc>()
|
||||||
.add(SelectOptionEditorEvent.unSelectOption(widget.option.id));
|
.add(SelectOptionCellEditorEvent.unSelectOption(widget.option.id));
|
||||||
} else {
|
} else {
|
||||||
context
|
context
|
||||||
.read<SelectOptionCellEditorBloc>()
|
.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:flowy_infra/size.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.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';
|
import 'extension.dart';
|
||||||
|
|
||||||
@ -18,6 +15,7 @@ class SelectOptionTextField extends StatefulWidget {
|
|||||||
required this.distanceToText,
|
required this.distanceToText,
|
||||||
required this.textSeparators,
|
required this.textSeparators,
|
||||||
required this.textController,
|
required this.textController,
|
||||||
|
required this.focusNode,
|
||||||
required this.onSubmitted,
|
required this.onSubmitted,
|
||||||
required this.newText,
|
required this.newText,
|
||||||
required this.onPaste,
|
required this.onPaste,
|
||||||
@ -30,8 +28,9 @@ class SelectOptionTextField extends StatefulWidget {
|
|||||||
final double distanceToText;
|
final double distanceToText;
|
||||||
final List<String> textSeparators;
|
final List<String> textSeparators;
|
||||||
final TextEditingController textController;
|
final TextEditingController textController;
|
||||||
|
final FocusNode focusNode;
|
||||||
|
|
||||||
final Function(String) onSubmitted;
|
final Function() onSubmitted;
|
||||||
final Function(String) newText;
|
final Function(String) newText;
|
||||||
final Function(List<String>, String) onPaste;
|
final Function(List<String>, String) onPaste;
|
||||||
final Function(String) onRemove;
|
final Function(String) onRemove;
|
||||||
@ -42,32 +41,11 @@ class SelectOptionTextField extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||||
late final FocusNode focusNode;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
focusNode.requestFocus();
|
widget.focusNode.requestFocus();
|
||||||
});
|
});
|
||||||
widget.textController.addListener(_onChanged);
|
widget.textController.addListener(_onChanged);
|
||||||
}
|
}
|
||||||
@ -75,7 +53,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.textController.removeListener(_onChanged);
|
widget.textController.removeListener(_onChanged);
|
||||||
focusNode.dispose();
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,15 +60,9 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return TextField(
|
return TextField(
|
||||||
controller: widget.textController,
|
controller: widget.textController,
|
||||||
focusNode: focusNode,
|
focusNode: widget.focusNode,
|
||||||
onTap: widget.onClick,
|
onTap: widget.onClick,
|
||||||
onSubmitted: (text) {
|
onSubmitted: (_) => widget.onSubmitted(),
|
||||||
if (text.isNotEmpty) {
|
|
||||||
widget.onSubmitted(text.trim());
|
|
||||||
focusNode.requestFocus();
|
|
||||||
widget.textController.clear();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
@ -100,11 +71,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
|||||||
),
|
),
|
||||||
isDense: true,
|
isDense: true,
|
||||||
prefixIcon: _renderTags(context),
|
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),
|
prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||||
@ -148,7 +114,9 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return MouseRegion(
|
return Focus(
|
||||||
|
descendantsAreFocusable: false,
|
||||||
|
child: MouseRegion(
|
||||||
cursor: SystemMouseCursors.basic,
|
cursor: SystemMouseCursors.basic,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
@ -168,6 +136,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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_option_type_option_bloc.dart';
|
||||||
import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.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/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_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
@ -48,16 +50,15 @@ class SelectOptionTypeOptionWidget extends StatelessWidget {
|
|||||||
] else
|
] else
|
||||||
const _AddOptionButton(),
|
const _AddOptionButton(),
|
||||||
const VSpace(4),
|
const VSpace(4),
|
||||||
...state.options.map((option) {
|
Flexible(
|
||||||
return _OptionCell(
|
child: _OptionList(
|
||||||
option: option,
|
|
||||||
popoverMutex: popoverMutex,
|
popoverMutex: popoverMutex,
|
||||||
);
|
),
|
||||||
}),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
return ListView(
|
return Column(
|
||||||
shrinkWrap: true,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: children,
|
children: children,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -90,9 +91,15 @@ class _OptionTitle extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _OptionCell extends StatefulWidget {
|
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 SelectOptionPB option;
|
||||||
|
final int index;
|
||||||
final PopoverMutex? popoverMutex;
|
final PopoverMutex? popoverMutex;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -108,6 +115,7 @@ class _OptionCellState extends State<_OptionCell> {
|
|||||||
height: 28,
|
height: 28,
|
||||||
child: SelectOptionTagCell(
|
child: SelectOptionTagCell(
|
||||||
option: widget.option,
|
option: widget.option,
|
||||||
|
index: widget.index,
|
||||||
onSelected: () => _popoverController.show(),
|
onSelected: () => _popoverController.show(),
|
||||||
children: [
|
children: [
|
||||||
FlowyIconButton(
|
FlowyIconButton(
|
||||||
@ -115,8 +123,9 @@ class _OptionCellState extends State<_OptionCell> {
|
|||||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
icon: FlowySvg(
|
icon: FlowySvg(
|
||||||
FlowySvgs.details_s,
|
FlowySvgs.three_dots_s,
|
||||||
color: Theme.of(context).iconTheme.color,
|
color: Theme.of(context).iconTheme.color,
|
||||||
|
size: const Size.square(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -253,3 +262,61 @@ class _CreateOptionTextFieldState extends State<CreateOptionTextField> {
|
|||||||
super.dispose();
|
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(
|
child: FlowyButton(
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||||
text: FlowyText.medium(
|
text: FlowyText.medium(
|
||||||
color.optionName(),
|
color.colorName(),
|
||||||
color: AFThemeExtension.of(context).textColor,
|
color: AFThemeExtension.of(context).textColor,
|
||||||
),
|
),
|
||||||
leftIcon: colorIcon,
|
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/database_controller.dart';
|
||||||
import 'package:appflowy/plugins/database/application/setting/group_bloc.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/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:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
@ -65,13 +65,13 @@ void main() {
|
|||||||
context.makeCellControllerFromFieldId(multiSelectField.id)
|
context.makeCellControllerFromFieldId(multiSelectField.id)
|
||||||
as SelectOptionCellController;
|
as SelectOptionCellController;
|
||||||
|
|
||||||
final multiSelectOptionBloc =
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
SelectOptionCellEditorBloc(cellController: cellController);
|
|
||||||
multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("B"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await boardResponseFuture();
|
await boardResponseFuture();
|
||||||
|
|
||||||
// set grouped by the new multi-select field"
|
// 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/field_entities.pbenum.dart';
|
||||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@ -21,10 +21,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
bloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
expect(bloc.state.options.length, 1);
|
expect(bloc.state.options.length, 1);
|
||||||
@ -40,16 +40,16 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
bloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
final SelectOptionPB optionUpdate = bloc.state.options[0]
|
final SelectOptionPB optionUpdate = bloc.state.options[0]
|
||||||
..color = SelectOptionColorPB.Aqua
|
..color = SelectOptionColorPB.Aqua
|
||||||
..name = "B";
|
..name = "B";
|
||||||
bloc.add(SelectOptionEditorEvent.updateOption(optionUpdate));
|
bloc.add(SelectOptionCellEditorEvent.updateOption(optionUpdate));
|
||||||
|
|
||||||
expect(bloc.state.options.length, 1);
|
expect(bloc.state.options.length, 1);
|
||||||
expect(bloc.state.options[0].name, "B");
|
expect(bloc.state.options[0].name, "B");
|
||||||
@ -65,31 +65,33 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
bloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
assert(
|
assert(
|
||||||
bloc.state.options.length == 1,
|
bloc.state.options.length == 1,
|
||||||
"Expect 1 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}",
|
"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();
|
await gridResponseFuture();
|
||||||
assert(
|
assert(
|
||||||
bloc.state.options.length == 2,
|
bloc.state.options.length == 2,
|
||||||
"Expect 2 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}",
|
"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();
|
await gridResponseFuture();
|
||||||
assert(
|
assert(
|
||||||
bloc.state.options.length == 3,
|
bloc.state.options.length == 3,
|
||||||
"Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}",
|
"Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}",
|
||||||
);
|
);
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.deleteAllOptions());
|
bloc.add(const SelectOptionCellEditorEvent.deleteAllOptions());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
@ -107,18 +109,18 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
bloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
final optionId = bloc.state.options[0].id;
|
final optionId = bloc.state.options[0].id;
|
||||||
bloc.add(SelectOptionEditorEvent.unSelectOption(optionId));
|
bloc.add(SelectOptionCellEditorEvent.unSelectOption(optionId));
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
assert(bloc.state.selectedOptions.isEmpty);
|
assert(bloc.state.selectedOptions.isEmpty);
|
||||||
|
|
||||||
bloc.add(SelectOptionEditorEvent.selectOption(optionId));
|
bloc.add(SelectOptionCellEditorEvent.selectOption(optionId));
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
assert(bloc.state.selectedOptions.length == 1);
|
assert(bloc.state.selectedOptions.length == 1);
|
||||||
@ -134,20 +136,22 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
bloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.trySelectOption("B"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("B"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.trySelectOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.submitTextField());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
assert(bloc.state.selectedOptions.length == 1);
|
expect(bloc.state.selectedOptions.length, 1);
|
||||||
assert(bloc.state.options.length == 2);
|
expect(bloc.state.options.length, 1);
|
||||||
expect(bloc.state.selectedOptions[0].name, "A");
|
expect(bloc.state.selectedOptions[0].name, "A");
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,17 +164,18 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
bloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("A"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("A"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("B"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("B"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(
|
bloc.add(
|
||||||
const SelectOptionEditorEvent.selectMultipleOptions(
|
const SelectOptionCellEditorEvent.selectMultipleOptions(
|
||||||
["A", "B", "C"],
|
["A", "B", "C"],
|
||||||
"x",
|
"x",
|
||||||
),
|
),
|
||||||
@ -191,10 +196,10 @@ void main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
final bloc = SelectOptionCellEditorBloc(cellController: cellController);
|
||||||
bloc.add(const SelectOptionEditorEvent.initial());
|
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("abcd"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("abcd"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
expect(
|
expect(
|
||||||
bloc.state.options.length,
|
bloc.state.options.length,
|
||||||
@ -202,7 +207,8 @@ void main() {
|
|||||||
reason: "Options: ${bloc.state.options}",
|
reason: "Options: ${bloc.state.options}",
|
||||||
);
|
);
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("aaaa"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("aaaa"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
expect(
|
expect(
|
||||||
bloc.state.options.length,
|
bloc.state.options.length,
|
||||||
@ -210,7 +216,8 @@ void main() {
|
|||||||
reason: "Options: ${bloc.state.options}",
|
reason: "Options: ${bloc.state.options}",
|
||||||
);
|
);
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.newOption("defg"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("defg"));
|
||||||
|
bloc.add(const SelectOptionCellEditorEvent.createOption());
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
expect(
|
expect(
|
||||||
bloc.state.options.length,
|
bloc.state.options.length,
|
||||||
@ -218,7 +225,7 @@ void main() {
|
|||||||
reason: "Options: ${bloc.state.options}",
|
reason: "Options: ${bloc.state.options}",
|
||||||
);
|
);
|
||||||
|
|
||||||
bloc.add(const SelectOptionEditorEvent.filterOption("a"));
|
bloc.add(const SelectOptionCellEditorEvent.filterOption("a"));
|
||||||
await gridResponseFuture();
|
await gridResponseFuture();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
@ -231,7 +238,7 @@ void main() {
|
|||||||
3,
|
3,
|
||||||
reason: "Options: ${bloc.state.options}",
|
reason: "Options: ${bloc.state.options}",
|
||||||
);
|
);
|
||||||
expect(bloc.state.createOption, "a");
|
expect(bloc.state.createSelectOptionSuggestion!.name, "a");
|
||||||
expect(bloc.state.filter, "a");
|
expect(bloc.state.filter, "a");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -17,11 +17,13 @@ void main() {
|
|||||||
String remainder = '';
|
String remainder = '';
|
||||||
List<String> select = [];
|
List<String> select = [];
|
||||||
|
|
||||||
|
final textController = TextEditingController();
|
||||||
|
|
||||||
final textField = SelectOptionTextField(
|
final textField = SelectOptionTextField(
|
||||||
options: const [],
|
options: const [],
|
||||||
selectedOptionMap: LinkedHashMap<String, SelectOptionPB>(),
|
selectedOptionMap: LinkedHashMap<String, SelectOptionPB>(),
|
||||||
distanceToText: 0.0,
|
distanceToText: 0.0,
|
||||||
onSubmitted: (text) => submit = text,
|
onSubmitted: () => submit = textController.text,
|
||||||
onPaste: (options, remaining) {
|
onPaste: (options, remaining) {
|
||||||
remainder = remaining;
|
remainder = remaining;
|
||||||
select = options;
|
select = options;
|
||||||
@ -29,7 +31,8 @@ void main() {
|
|||||||
onRemove: (_) {},
|
onRemove: (_) {},
|
||||||
newText: (text) => remainder = text,
|
newText: (text) => remainder = text,
|
||||||
textSeparators: const [','],
|
textSeparators: const [','],
|
||||||
textController: TextEditingController(),
|
textController: textController,
|
||||||
|
focusNode: FocusNode(),
|
||||||
);
|
);
|
||||||
|
|
||||||
testWidgets('SelectOptionTextField callback outputs',
|
testWidgets('SelectOptionTextField callback outputs',
|
||||||
@ -57,11 +60,6 @@ void main() {
|
|||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||||
expect(submit, 'an option');
|
expect(submit, 'an option');
|
||||||
|
|
||||||
submit = '';
|
|
||||||
await tester.enterText(find.byType(TextField), ' ');
|
|
||||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
|
||||||
expect(submit, '');
|
|
||||||
|
|
||||||
// test inputs containing commas
|
// test inputs containing commas
|
||||||
await tester.enterText(find.byType(TextField), 'a a, bbbb , c');
|
await tester.enterText(find.byType(TextField), 'a a, bbbb , c');
|
||||||
expect(remainder, 'c');
|
expect(remainder, 'c');
|
||||||
|
@ -736,9 +736,9 @@
|
|||||||
"blueColor": "Blue",
|
"blueColor": "Blue",
|
||||||
"deleteTag": "Delete tag",
|
"deleteTag": "Delete tag",
|
||||||
"colorPanelTitle": "Color",
|
"colorPanelTitle": "Color",
|
||||||
"panelTitle": "Select an option",
|
"panelTitle": "Select an option or create one",
|
||||||
"searchOption": "Search for an option",
|
"searchOption": "Search for an option",
|
||||||
"searchOrCreateOption": "Search...",
|
"searchOrCreateOption": "Search for an option or create one",
|
||||||
"createNew": "Create a new",
|
"createNew": "Create a new",
|
||||||
"orSelectOne": "Or select an option",
|
"orSelectOne": "Or select an option",
|
||||||
"typeANewOption": "Type a new option",
|
"typeANewOption": "Type a new option",
|
||||||
|
Loading…
Reference in New Issue
Block a user