fix: checklist cell did get notified after the cell content change

This commit is contained in:
nathan 2022-11-30 14:24:26 +08:00
parent 3cdd6665b3
commit 29e07089ca
12 changed files with 195 additions and 90 deletions

View file

@ -51,7 +51,11 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
onCellFieldChanged: () { onCellFieldChanged: () {
_loadOptions(); _loadOptions();
}, },
onCellChanged: (_) {}, onCellChanged: (data) {
if (!isClosed && data != null) {
add(ChecklistCellEvent.didReceiveOptions(data));
}
},
); );
} }

View file

@ -15,7 +15,6 @@ class ChecklistCellEditorBloc
extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> { extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
final SelectOptionFFIService _selectOptionService; final SelectOptionFFIService _selectOptionService;
final GridChecklistCellController cellController; final GridChecklistCellController cellController;
Timer? _delayOperation;
ChecklistCellEditorBloc({ ChecklistCellEditorBloc({
required this.cellController, required this.cellController,
@ -27,7 +26,6 @@ class ChecklistCellEditorBloc
await event.when( await event.when(
initial: () async { initial: () async {
_startListening(); _startListening();
_loadOptions();
}, },
didReceiveOptions: (data) { didReceiveOptions: (data) {
emit(state.copyWith( emit(state.copyWith(
@ -47,11 +45,12 @@ class ChecklistCellEditorBloc
updateOption: (option) { updateOption: (option) {
_updateOption(option); _updateOption(option);
}, },
selectOption: (optionId) { selectOption: (option) async {
_selectOptionService.select(optionIds: [optionId]); if (option.isSelected) {
}, await _selectOptionService.unSelect(optionIds: [option.data.id]);
unSelectOption: (optionId) { } else {
_selectOptionService.unSelect(optionIds: [optionId]); await _selectOptionService.select(optionIds: [option.data.id]);
}
}, },
filterOption: (String predicate) {}, filterOption: (String predicate) {},
); );
@ -61,7 +60,6 @@ class ChecklistCellEditorBloc
@override @override
Future<void> close() async { Future<void> close() async {
_delayOperation?.cancel();
await cellController.dispose(); await cellController.dispose();
return super.close(); return super.close();
} }
@ -119,10 +117,8 @@ class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
SelectOptionCellDataPB data) = _DidReceiveOptions; SelectOptionCellDataPB data) = _DidReceiveOptions;
const factory ChecklistCellEditorEvent.newOption(String optionName) = const factory ChecklistCellEditorEvent.newOption(String optionName) =
_NewOption; _NewOption;
const factory ChecklistCellEditorEvent.selectOption(String optionId) = const factory ChecklistCellEditorEvent.selectOption(
_SelectOption; ChecklistSelectOption option) = _SelectOption;
const factory ChecklistCellEditorEvent.unSelectOption(String optionId) =
_UnSelectOption;
const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) = const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) =
_UpdateOption; _UpdateOption;
const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) = const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) =

View file

@ -522,6 +522,7 @@ class FieldInfo {
case FieldType.MultiSelect: case FieldType.MultiSelect:
case FieldType.RichText: case FieldType.RichText:
case FieldType.SingleSelect: case FieldType.SingleSelect:
// case FieldType.Checklist:
return true; return true;
default: default:
return false; return false;

View file

@ -36,20 +36,16 @@ class GridChecklistCellState extends State<GridChecklistCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>( child: Stack(
builder: (context, state) { alignment: AlignmentDirectional.center,
return Stack( fit: StackFit.expand,
alignment: AlignmentDirectional.center, children: [
fit: StackFit.expand, Padding(
children: [ padding: GridSize.cellContentInsets,
Padding( child: _wrapPopover(const ChecklistProgressBar()),
padding: GridSize.cellContentInsets, ),
child: _wrapPopover(const ChecklistProgressBar()), InkWell(onTap: () => _popover.show()),
), ],
InkWell(onTap: () => _popover.show()),
],
);
},
), ),
); );
} }

View file

@ -6,6 +6,7 @@ import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/s
import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra/image.dart'; import 'package:flowy_infra/image.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/button.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
@ -24,9 +25,11 @@ class GridChecklistCellEditor extends StatefulWidget {
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> { class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
late ChecklistCellEditorBloc bloc; late ChecklistCellEditorBloc bloc;
late PopoverMutex popoverMutex;
@override @override
void initState() { void initState() {
popoverMutex = PopoverMutex();
bloc = ChecklistCellEditorBloc(cellController: widget.cellController); bloc = ChecklistCellEditorBloc(cellController: widget.cellController);
bloc.add(const ChecklistCellEditorEvent.initial()); bloc.add(const ChecklistCellEditorEvent.initial());
super.initState(); super.initState();
@ -47,23 +50,28 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
final List<Widget> slivers = [ final List<Widget> slivers = [
const SliverChecklistPrograssBar(), const SliverChecklistPrograssBar(),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Container(color: Colors.red, height: 2, width: 2100)), child: Padding(
SliverToBoxAdapter( padding: GridSize.typeOptionContentInsets,
child: ListView.separated( child: ListView.separated(
controller: ScrollController(), controller: ScrollController(),
shrinkWrap: true, shrinkWrap: true,
itemCount: state.allOptions.length, itemCount: state.allOptions.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return _ChecklistOptionCell(option: state.allOptions[index]); return _ChecklistOptionCell(
}, option: state.allOptions[index],
separatorBuilder: (BuildContext context, int index) { popoverMutex: popoverMutex,
return VSpace(GridSize.typeOptionSeparatorHeight); );
}, },
separatorBuilder: (BuildContext context, int index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
),
), ),
), ),
]; ];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8), return ScrollConfiguration(
behavior: const ScrollBehavior().copyWith(scrollbars: false),
child: CustomScrollView( child: CustomScrollView(
shrinkWrap: true, shrinkWrap: true,
slivers: slivers, slivers: slivers,
@ -79,8 +87,10 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
class _ChecklistOptionCell extends StatefulWidget { class _ChecklistOptionCell extends StatefulWidget {
final ChecklistSelectOption option; final ChecklistSelectOption option;
final PopoverMutex popoverMutex;
const _ChecklistOptionCell({ const _ChecklistOptionCell({
required this.option, required this.option,
required this.popoverMutex,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -107,10 +117,15 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
height: GridSize.typeOptionItemHeight, height: GridSize.typeOptionItemHeight,
child: Row( child: Row(
children: [ children: [
icon, Expanded(
const HSpace(6), child: FlowyButton(
FlowyText(widget.option.data.name), text: FlowyText(widget.option.data.name),
const Spacer(), leftIcon: icon,
onTap: () => context
.read<ChecklistCellEditorBloc>()
.add(ChecklistCellEditorEvent.selectOption(widget.option)),
),
),
_disclosureButton(), _disclosureButton(),
], ],
), ),
@ -122,8 +137,7 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
return FlowyIconButton( return FlowyIconButton(
width: 20, width: 20,
onPressed: () => _popoverController.show(), onPressed: () => _popoverController.show(),
hoverColor: Colors.transparent, iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
icon: svgWidget( icon: svgWidget(
"editor/details", "editor/details",
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
@ -137,15 +151,30 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
offset: const Offset(20, 0), offset: const Offset(20, 0),
asBarrier: true, asBarrier: true,
constraints: BoxConstraints.loose(const Size(200, 300)), constraints: BoxConstraints.loose(const Size(200, 300)),
mutex: widget.popoverMutex,
triggerActions: PopoverTriggerFlags.none,
child: child, child: child,
popupBuilder: (BuildContext popoverContext) { popupBuilder: (BuildContext popoverContext) {
return SelectOptionTypeOptionEditor( return SelectOptionTypeOptionEditor(
option: widget.option.data, option: widget.option.data,
onDeleted: () {}, onDeleted: () {
onUpdated: (updatedOption) {}, context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.deleteOption(widget.option.data),
);
_popoverController.close();
},
onUpdated: (updatedOption) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.updateOption(widget.option.data),
);
},
showOptions: false,
autoFocus: false,
// Use ValueKey to refresh the UI, otherwise, it will remain the old value.
key: ValueKey( key: ValueKey(
widget.option.data.id, widget.option.data.id,
), // Use ValueKey to refresh the UI, otherwise, it will remain the old value. ),
); );
}, },
); );

View file

@ -1,5 +1,6 @@
import 'package:app_flowy/generated/locale_keys.g.dart'; import 'package:app_flowy/generated/locale_keys.g.dart';
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_editor_bloc.dart'; import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_editor_bloc.dart';
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/color_extension.dart'; import 'package:flowy_infra/color_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -41,34 +42,39 @@ class _SliverChecklistPrograssBarDelegate
extends SliverPersistentHeaderDelegate { extends SliverPersistentHeaderDelegate {
_SliverChecklistPrograssBarDelegate(); _SliverChecklistPrograssBarDelegate();
double fixHeight = 80; double fixHeight = 60;
@override @override
Widget build( Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) { BuildContext context, double shrinkOffset, bool overlapsContent) {
return BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>( return BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
builder: (context, state) { builder: (context, state) {
return Column( return Container(
children: [ color: Theme.of(context).colorScheme.background,
if (state.percent != 0) padding: GridSize.typeOptionContentInsets,
Padding( child: Column(
padding: const EdgeInsets.symmetric(vertical: 8.0), children: [
child: ChecklistPrograssBar(percent: state.percent), FlowyTextField(
autoClearWhenDone: true,
hintText: LocaleKeys.grid_checklist_panelTitle.tr(),
onChanged: (text) {
context
.read<ChecklistCellEditorBloc>()
.add(ChecklistCellEditorEvent.filterOption(text));
},
onSubmitted: (text) {
context
.read<ChecklistCellEditorBloc>()
.add(ChecklistCellEditorEvent.newOption(text));
},
), ),
FlowyTextField( if (state.percent != 0)
hintText: LocaleKeys.grid_checklist_panelTitle.tr(), Padding(
onChanged: (text) { padding: const EdgeInsets.symmetric(vertical: 8.0),
context child: ChecklistPrograssBar(percent: state.percent),
.read<ChecklistCellEditorBloc>() ),
.add(ChecklistCellEditorEvent.filterOption(text)); ],
}, ),
onSubmitted: (text) {
context
.read<ChecklistCellEditorBloc>()
.add(ChecklistCellEditorEvent.newOption(text));
},
)
],
); );
}, },
); );

View file

@ -4,6 +4,7 @@ import 'package:flowy_infra/image.dart';
import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart'; import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -18,10 +19,14 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
final SelectOptionPB option; final SelectOptionPB option;
final VoidCallback onDeleted; final VoidCallback onDeleted;
final Function(SelectOptionPB) onUpdated; final Function(SelectOptionPB) onUpdated;
final bool showOptions;
final bool autoFocus;
const SelectOptionTypeOptionEditor({ const SelectOptionTypeOptionEditor({
required this.option, required this.option,
required this.onDeleted, required this.onDeleted,
required this.onUpdated, required this.onUpdated,
this.showOptions = true,
this.autoFocus = true,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -50,21 +55,29 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
builder: (context, state) { builder: (context, state) {
List<Widget> slivers = [ List<Widget> slivers = [
SliverToBoxAdapter( SliverToBoxAdapter(
child: _OptionNameTextField(state.option.name)), child: _OptionNameTextField(
name: state.option.name,
autoFocus: autoFocus,
)),
const SliverToBoxAdapter(child: VSpace(10)), const SliverToBoxAdapter(child: VSpace(10)),
const SliverToBoxAdapter(child: _DeleteTag()), const SliverToBoxAdapter(child: _DeleteTag()),
const SliverToBoxAdapter(child: TypeOptionSeparator()),
SliverToBoxAdapter(
child:
SelectOptionColorList(selectedColor: state.option.color)),
]; ];
if (showOptions) {
slivers
.add(const SliverToBoxAdapter(child: TypeOptionSeparator()));
slivers.add(SliverToBoxAdapter(
child: SelectOptionColorList(
selectedColor: state.option.color)));
}
return SizedBox( return SizedBox(
width: 160, width: 160,
child: Padding( child: Padding(
padding: const EdgeInsets.all(6.0), padding: const EdgeInsets.all(6.0),
child: CustomScrollView( child: CustomScrollView(
slivers: slivers, slivers: slivers,
shrinkWrap: true,
controller: ScrollController(), controller: ScrollController(),
physics: StyledScrollPhysics(), physics: StyledScrollPhysics(),
), ),
@ -102,19 +115,21 @@ class _DeleteTag extends StatelessWidget {
class _OptionNameTextField extends StatelessWidget { class _OptionNameTextField extends StatelessWidget {
final String name; final String name;
const _OptionNameTextField(this.name, {Key? key}) : super(key: key); final bool autoFocus;
const _OptionNameTextField(
{required this.name, required this.autoFocus, Key? key})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return InputTextField( return FlowyTextField(
autoFucous: autoFocus,
text: name, text: name,
maxLength: 30, onSubmitted: (newName) {
onCanceled: () {}, if (name != newName) {
onDone: (optionName) {
if (name != optionName) {
context context
.read<EditSelectOptionBloc>() .read<EditSelectOptionBloc>()
.add(EditSelectOptionEvent.updateName(optionName)); .add(EditSelectOptionEvent.updateName(newName));
} }
}, },
); );

View file

@ -88,7 +88,8 @@ class _PopoverMaskState extends State<PopoverMask> {
} }
bool _handleGlobalKeyEvent(KeyEvent event) { bool _handleGlobalKeyEvent(KeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.escape) { if (event.logicalKey == LogicalKeyboardKey.escape &&
event is KeyDownEvent) {
if (widget.onExit != null) { if (widget.onExit != null) {
widget.onExit!(); widget.onExit!();
} }

View file

@ -1,18 +1,28 @@
import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/size.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:textstyle_extensions/textstyle_extensions.dart';
class FlowyTextField extends StatefulWidget { class FlowyTextField extends StatefulWidget {
final String hintText; final String hintText;
final String text; final String text;
final void Function(String)? onChanged; final void Function(String)? onChanged;
final void Function(String)? onSubmitted; final void Function(String)? onSubmitted;
final void Function()? onCanceled;
final bool autoFucous; final bool autoFucous;
final int? maxLength;
final TextEditingController? controller;
final bool autoClearWhenDone;
const FlowyTextField({ const FlowyTextField({
this.hintText = "", this.hintText = "",
this.text = "", this.text = "",
this.onChanged, this.onChanged,
this.onSubmitted, this.onSubmitted,
this.onCanceled,
this.autoFucous = true, this.autoFucous = true,
this.maxLength,
this.controller,
this.autoClearWhenDone = false,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@ -23,12 +33,19 @@ class FlowyTextField extends StatefulWidget {
class FlowyTextFieldState extends State<FlowyTextField> { class FlowyTextFieldState extends State<FlowyTextField> {
late FocusNode focusNode; late FocusNode focusNode;
late TextEditingController controller; late TextEditingController controller;
var textLength = 0;
@override @override
void initState() { void initState() {
focusNode = FocusNode(); focusNode = FocusNode();
controller = TextEditingController(); focusNode.addListener(notifyDidEndEditing);
controller.text = widget.text;
if (widget.controller != null) {
controller = widget.controller!;
} else {
controller = TextEditingController();
controller.text = widget.text;
}
if (widget.autoFucous) { if (widget.autoFucous) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus(); focusNode.requestFocus();
@ -47,9 +64,15 @@ class FlowyTextFieldState extends State<FlowyTextField> {
}, },
onSubmitted: (text) { onSubmitted: (text) {
widget.onSubmitted?.call(text); widget.onSubmitted?.call(text);
if (widget.autoClearWhenDone) {
controller.text = "";
}
}, },
maxLines: 1, maxLines: 1,
style: Theme.of(context).textTheme.bodyMedium, maxLength: widget.maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
style: TextStyles.body1.size(FontSizes.s12),
decoration: InputDecoration( decoration: InputDecoration(
contentPadding: const EdgeInsets.all(10), contentPadding: const EdgeInsets.all(10),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
@ -61,6 +84,8 @@ class FlowyTextFieldState extends State<FlowyTextField> {
), ),
isDense: true, isDense: true,
hintText: widget.hintText, hintText: widget.hintText,
suffixText: _suffixText(),
counterText: "",
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary, color: Theme.of(context).colorScheme.primary,
@ -71,4 +96,29 @@ class FlowyTextFieldState extends State<FlowyTextField> {
), ),
); );
} }
@override
void dispose() {
focusNode.removeListener(notifyDidEndEditing);
focusNode.dispose();
super.dispose();
}
void notifyDidEndEditing() {
if (!focusNode.hasFocus) {
if (controller.text.isEmpty) {
widget.onCanceled?.call();
} else {
widget.onSubmitted?.call(controller.text);
}
}
}
String? _suffixText() {
if (widget.maxLength != null) {
return '${textLength.toString()}/${widget.maxLength.toString()}';
} else {
return null;
}
}
} }

View file

@ -80,10 +80,10 @@ impl CellDataOperation<SelectOptionIds, SelectOptionCellChangeset> for Checklist
} }
new_cell_data = select_ids.to_string(); new_cell_data = select_ids.to_string();
tracing::trace!("checklist's cell data: {}", &new_cell_data);
} }
} }
tracing::trace!("checklist's cell data: {}", &new_cell_data);
Ok(new_cell_data) Ok(new_cell_data)
} }
} }

View file

@ -48,7 +48,7 @@ impl SelectOptionTypeOptionTransformer {
T: SelectTypeOptionSharedAction, T: SelectTypeOptionSharedAction,
{ {
match decoded_field_type { match decoded_field_type {
FieldType::SingleSelect | FieldType::MultiSelect => { FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => {
// //
CellBytes::from(shared.get_selected_options(cell_data)) CellBytes::from(shared.get_selected_options(cell_data))
} }

View file

@ -390,7 +390,14 @@ fn make_test_board() -> BuildGridContext {
let url_field = FieldBuilder::new(url).name("link").visibility(true).build(); let url_field = FieldBuilder::new(url).name("link").visibility(true).build();
grid_builder.add_field(url_field); grid_builder.add_field(url_field);
} }
FieldType::Checklist => {} FieldType::Checklist => {
let checklist = ChecklistTypeOptionBuilder::default()
.add_option(SelectOptionPB::new(FIRST_THING))
.add_option(SelectOptionPB::new(SECOND_THING))
.add_option(SelectOptionPB::new(THIRD_THING));
let checklist_field = FieldBuilder::new(checklist).name("TODO").visibility(true).build();
grid_builder.add_field(checklist_field);
}
} }
} }