mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
fix: add new select option tag from textfield
This commit is contained in:
parent
ce85050114
commit
ed10ebac7a
@ -110,7 +110,7 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
|
||||
void _loadOptions() async {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(
|
||||
const Duration(milliseconds: 300),
|
||||
const Duration(milliseconds: 1),
|
||||
() async {
|
||||
final result = await _selectOptionService.getOpitonContext(
|
||||
gridId: state.gridId,
|
||||
@ -119,10 +119,14 @@ class SelectOptionEditorBloc extends Bloc<SelectOptionEditorEvent, SelectOptionE
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(selectOptionContext) => add(SelectOptionEditorEvent.didReceiveOptions(
|
||||
selectOptionContext.options,
|
||||
selectOptionContext.selectOptions,
|
||||
)),
|
||||
(selectOptionContext) {
|
||||
if (!isClosed) {
|
||||
add(SelectOptionEditorEvent.didReceiveOptions(
|
||||
selectOptionContext.options,
|
||||
selectOptionContext.selectOptions,
|
||||
));
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
@ -6,7 +7,6 @@ import 'package:flowy_sdk/protobuf/flowy-folder-data-model/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/protobuf.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'grid_block_service.dart';
|
||||
import 'field/grid_listenr.dart';
|
||||
import 'grid_service.dart';
|
||||
@ -154,46 +154,3 @@ class GridLoadingState with _$GridLoadingState {
|
||||
const factory GridLoadingState.loading() = _Loading;
|
||||
const factory GridLoadingState.finish(Either<Unit, FlowyError> successOrFail) = _Finish;
|
||||
}
|
||||
|
||||
class GridBlockRow {
|
||||
final String gridId;
|
||||
final String rowId;
|
||||
final String blockId;
|
||||
final double height;
|
||||
|
||||
const GridBlockRow({
|
||||
required this.gridId,
|
||||
required this.rowId,
|
||||
required this.blockId,
|
||||
required this.height,
|
||||
});
|
||||
}
|
||||
|
||||
class RowData extends Equatable {
|
||||
final String gridId;
|
||||
final String rowId;
|
||||
final String blockId;
|
||||
final List<Field> fields;
|
||||
final double height;
|
||||
|
||||
const RowData({
|
||||
required this.gridId,
|
||||
required this.rowId,
|
||||
required this.blockId,
|
||||
required this.fields,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
factory RowData.fromBlockRow(GridBlockRow row, List<Field> fields) {
|
||||
return RowData(
|
||||
gridId: row.gridId,
|
||||
rowId: row.rowId,
|
||||
blockId: row.blockId,
|
||||
fields: fields,
|
||||
height: row.height,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [rowId, fields];
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid-data-model/grid.pb.dart';
|
||||
@ -41,3 +40,34 @@ class CellData with _$CellData {
|
||||
Cell? cell,
|
||||
}) = _CellData;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class RowData with _$RowData {
|
||||
const factory RowData({
|
||||
required String gridId,
|
||||
required String rowId,
|
||||
required String blockId,
|
||||
required List<Field> fields,
|
||||
required double height,
|
||||
}) = _RowData;
|
||||
|
||||
factory RowData.fromBlockRow(GridBlockRow row, List<Field> fields) {
|
||||
return RowData(
|
||||
gridId: row.gridId,
|
||||
rowId: row.rowId,
|
||||
blockId: row.blockId,
|
||||
fields: fields,
|
||||
height: row.height,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class GridBlockRow with _$GridBlockRow {
|
||||
const factory GridBlockRow({
|
||||
required String gridId,
|
||||
required String rowId,
|
||||
required String blockId,
|
||||
required double height,
|
||||
}) = _GridBlockRow;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/grid_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||
|
@ -1,6 +1,3 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
@ -8,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
|
||||
extension SelectOptionColorExtension on SelectOptionColor {
|
||||
Color make(BuildContext context) {
|
||||
@ -63,98 +59,6 @@ extension SelectOptionColorExtension on SelectOptionColor {
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionTextField extends StatelessWidget {
|
||||
final FocusNode _focusNode;
|
||||
final TextEditingController _controller;
|
||||
final TextfieldTagsController tagController;
|
||||
final List<SelectOption> options;
|
||||
final LinkedHashMap<String, SelectOption> selectedOptionMap;
|
||||
|
||||
final double distanceToText;
|
||||
|
||||
final Function(String) onNewTag;
|
||||
|
||||
SelectOptionTextField({
|
||||
required this.options,
|
||||
required this.selectedOptionMap,
|
||||
required this.distanceToText,
|
||||
required this.tagController,
|
||||
required this.onNewTag,
|
||||
TextEditingController? controller,
|
||||
FocusNode? focusNode,
|
||||
Key? key,
|
||||
}) : _controller = controller ?? TextEditingController(),
|
||||
_focusNode = focusNode ?? FocusNode(),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
|
||||
return TextFieldTags(
|
||||
textEditingController: _controller,
|
||||
textfieldTagsController: tagController,
|
||||
initialTags: selectedOptionMap.keys.toList(),
|
||||
focusNode: _focusNode,
|
||||
textSeparators: const [' ', ','],
|
||||
inputfieldBuilder: (BuildContext context, editController, focusNode, error, onChanged, onSubmitted) {
|
||||
return ((context, sc, tags, onTagDelegate) {
|
||||
tags.retainWhere((name) {
|
||||
return options.where((option) => option.name == name).isEmpty;
|
||||
});
|
||||
if (tags.isNotEmpty) {
|
||||
assert(tags.length == 1);
|
||||
onNewTag(tags.first);
|
||||
}
|
||||
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
controller: editController,
|
||||
focusNode: focusNode,
|
||||
onChanged: onChanged,
|
||||
onSubmitted: onSubmitted,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: theme.main1, width: 1.0),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
isDense: true,
|
||||
prefixIcon: _renderTags(sc),
|
||||
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
|
||||
prefixIconConstraints: BoxConstraints(maxWidth: distanceToText),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: theme.main1,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _renderTags(ScrollController sc) {
|
||||
if (selectedOptionMap.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final children = selectedOptionMap.values.map((option) => SelectOptionTag(option: option)).toList();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SingleChildScrollView(
|
||||
controller: sc,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: children),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SelectOptionTag extends StatelessWidget {
|
||||
final SelectOption option;
|
||||
final bool isSelected;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:app_flowy/workspace/application/grid/cell_bloc/selection_editor_bloc.dart';
|
||||
import 'package:app_flowy/workspace/application/grid/row/row_service.dart';
|
||||
import 'package:app_flowy/workspace/presentation/plugins/grid/src/layout/sizes.dart';
|
||||
@ -21,6 +20,7 @@ import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
|
||||
import 'extension.dart';
|
||||
import 'text_field.dart';
|
||||
|
||||
const double _editorPannelWidth = 300;
|
||||
|
||||
@ -135,8 +135,7 @@ class _TextField extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<SelectOptionEditorBloc, SelectOptionEditorState>(
|
||||
listener: (context, state) {},
|
||||
return BlocBuilder<SelectOptionEditorBloc, SelectOptionEditorState>(
|
||||
builder: (context, state) {
|
||||
final optionMap = LinkedHashMap<String, SelectOption>.fromIterable(state.selectedOptions,
|
||||
key: (option) => option.name, value: (option) => option);
|
||||
|
@ -0,0 +1,101 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/selection_type_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:textfield_tags/textfield_tags.dart';
|
||||
|
||||
import 'extension.dart';
|
||||
|
||||
class SelectOptionTextField extends StatelessWidget {
|
||||
final FocusNode _focusNode;
|
||||
final TextEditingController _controller;
|
||||
final TextfieldTagsController tagController;
|
||||
final List<SelectOption> options;
|
||||
final LinkedHashMap<String, SelectOption> selectedOptionMap;
|
||||
|
||||
final double distanceToText;
|
||||
|
||||
final Function(String) onNewTag;
|
||||
|
||||
SelectOptionTextField({
|
||||
required this.options,
|
||||
required this.selectedOptionMap,
|
||||
required this.distanceToText,
|
||||
required this.tagController,
|
||||
required this.onNewTag,
|
||||
TextEditingController? controller,
|
||||
FocusNode? focusNode,
|
||||
Key? key,
|
||||
}) : _controller = controller ?? TextEditingController(),
|
||||
_focusNode = focusNode ?? FocusNode(),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = context.watch<AppTheme>();
|
||||
|
||||
return TextFieldTags(
|
||||
textEditingController: _controller,
|
||||
textfieldTagsController: tagController,
|
||||
initialTags: selectedOptionMap.keys.toList(),
|
||||
focusNode: _focusNode,
|
||||
textSeparators: const [' ', ','],
|
||||
inputfieldBuilder: (BuildContext context, editController, focusNode, error, onChanged, onSubmitted) {
|
||||
return ((context, sc, tags, onTagDelegate) {
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
controller: editController,
|
||||
focusNode: focusNode,
|
||||
onChanged: onChanged,
|
||||
onSubmitted: (text) {
|
||||
if (onSubmitted != null) {
|
||||
onSubmitted(text);
|
||||
}
|
||||
|
||||
if (text.isNotEmpty) {
|
||||
onNewTag(text);
|
||||
}
|
||||
},
|
||||
maxLines: 1,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: theme.main1, width: 1.0),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
isDense: true,
|
||||
prefixIcon: _renderTags(sc),
|
||||
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
|
||||
prefixIconConstraints: BoxConstraints(maxWidth: distanceToText),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: theme.main1, width: 1.0),
|
||||
borderRadius: Corners.s10Border,
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _renderTags(ScrollController sc) {
|
||||
if (selectedOptionMap.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final children = selectedOptionMap.values.map((option) => SelectOptionTag(option: option)).toList();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SingleChildScrollView(
|
||||
controller: sc,
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(children: children),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -16,9 +16,28 @@ use std::str::FromStr;
|
||||
pub const SELECTION_IDS_SEPARATOR: &str = ",";
|
||||
|
||||
pub trait SelectOptionOperation: TypeOptionDataEntry + Send + Sync {
|
||||
fn insert_option(&mut self, new_option: SelectOption);
|
||||
fn delete_option(&mut self, delete_option: SelectOption);
|
||||
fn insert_option(&mut self, new_option: SelectOption) {
|
||||
let options = self.mut_options();
|
||||
if let Some(index) = options
|
||||
.iter()
|
||||
.position(|option| option.id == new_option.id || option.name == new_option.name)
|
||||
{
|
||||
options.remove(index);
|
||||
options.insert(index, new_option);
|
||||
} else {
|
||||
options.insert(0, new_option);
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_option(&mut self, delete_option: SelectOption) {
|
||||
let options = self.mut_options();
|
||||
if let Some(index) = options.iter().position(|option| option.id == delete_option.id) {
|
||||
options.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn option_context(&self, cell_meta: &Option<CellMeta>) -> SelectOptionContext;
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption>;
|
||||
}
|
||||
|
||||
// Single select
|
||||
@ -33,21 +52,6 @@ pub struct SingleSelectTypeOption {
|
||||
impl_type_option!(SingleSelectTypeOption, FieldType::SingleSelect);
|
||||
|
||||
impl SelectOptionOperation for SingleSelectTypeOption {
|
||||
fn insert_option(&mut self, new_option: SelectOption) {
|
||||
if let Some(index) = self.options.iter().position(|option| option.id == new_option.id) {
|
||||
self.options.remove(index);
|
||||
self.options.insert(index, new_option);
|
||||
} else {
|
||||
self.options.insert(0, new_option);
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_option(&mut self, delete_option: SelectOption) {
|
||||
if let Some(index) = self.options.iter().position(|option| option.id == delete_option.id) {
|
||||
self.options.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn option_context(&self, cell_meta: &Option<CellMeta>) -> SelectOptionContext {
|
||||
let select_options = make_select_context_from(cell_meta, &self.options);
|
||||
SelectOptionContext {
|
||||
@ -55,6 +59,10 @@ impl SelectOptionOperation for SingleSelectTypeOption {
|
||||
select_options,
|
||||
}
|
||||
}
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
|
||||
&mut self.options
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation for SingleSelectTypeOption {
|
||||
@ -139,21 +147,6 @@ impl MultiSelectTypeOption {
|
||||
}
|
||||
|
||||
impl SelectOptionOperation for MultiSelectTypeOption {
|
||||
fn insert_option(&mut self, new_option: SelectOption) {
|
||||
if let Some(index) = self.options.iter().position(|option| option.id == new_option.id) {
|
||||
self.options.remove(index);
|
||||
self.options.insert(index, new_option);
|
||||
} else {
|
||||
self.options.insert(0, new_option);
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_option(&mut self, delete_option: SelectOption) {
|
||||
if let Some(index) = self.options.iter().position(|option| option.id == delete_option.id) {
|
||||
self.options.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
fn option_context(&self, cell_meta: &Option<CellMeta>) -> SelectOptionContext {
|
||||
let select_options = make_select_context_from(cell_meta, &self.options);
|
||||
SelectOptionContext {
|
||||
@ -161,6 +154,10 @@ impl SelectOptionOperation for MultiSelectTypeOption {
|
||||
select_options,
|
||||
}
|
||||
}
|
||||
|
||||
fn mut_options(&mut self) -> &mut Vec<SelectOption> {
|
||||
&mut self.options
|
||||
}
|
||||
}
|
||||
|
||||
impl CellDataOperation for MultiSelectTypeOption {
|
||||
|
Loading…
Reference in New Issue
Block a user