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: () {
_loadOptions();
},
onCellChanged: (_) {},
onCellChanged: (data) {
if (!isClosed && data != null) {
add(ChecklistCellEvent.didReceiveOptions(data));
}
},
);
}

View File

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

View File

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

View File

@ -36,20 +36,16 @@ class GridChecklistCellState extends State<GridChecklistCell> {
Widget build(BuildContext context) {
return BlocProvider.value(
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) {
return Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: [
Padding(
padding: GridSize.cellContentInsets,
child: _wrapPopover(const ChecklistProgressBar()),
),
InkWell(onTap: () => _popover.show()),
],
);
},
child: Stack(
alignment: AlignmentDirectional.center,
fit: StackFit.expand,
children: [
Padding(
padding: GridSize.cellContentInsets,
child: _wrapPopover(const ChecklistProgressBar()),
),
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:flowy_infra/image.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/scrolling/styled_list.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@ -24,9 +25,11 @@ class GridChecklistCellEditor extends StatefulWidget {
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
late ChecklistCellEditorBloc bloc;
late PopoverMutex popoverMutex;
@override
void initState() {
popoverMutex = PopoverMutex();
bloc = ChecklistCellEditorBloc(cellController: widget.cellController);
bloc.add(const ChecklistCellEditorEvent.initial());
super.initState();
@ -47,23 +50,28 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
final List<Widget> slivers = [
const SliverChecklistPrograssBar(),
SliverToBoxAdapter(
child: Container(color: Colors.red, height: 2, width: 2100)),
SliverToBoxAdapter(
child: ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: state.allOptions.length,
itemBuilder: (BuildContext context, int index) {
return _ChecklistOptionCell(option: state.allOptions[index]);
},
separatorBuilder: (BuildContext context, int index) {
return VSpace(GridSize.typeOptionSeparatorHeight);
},
child: Padding(
padding: GridSize.typeOptionContentInsets,
child: ListView.separated(
controller: ScrollController(),
shrinkWrap: true,
itemCount: state.allOptions.length,
itemBuilder: (BuildContext context, int index) {
return _ChecklistOptionCell(
option: state.allOptions[index],
popoverMutex: popoverMutex,
);
},
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(
shrinkWrap: true,
slivers: slivers,
@ -79,8 +87,10 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
class _ChecklistOptionCell extends StatefulWidget {
final ChecklistSelectOption option;
final PopoverMutex popoverMutex;
const _ChecklistOptionCell({
required this.option,
required this.popoverMutex,
Key? key,
}) : super(key: key);
@ -107,10 +117,15 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
height: GridSize.typeOptionItemHeight,
child: Row(
children: [
icon,
const HSpace(6),
FlowyText(widget.option.data.name),
const Spacer(),
Expanded(
child: FlowyButton(
text: FlowyText(widget.option.data.name),
leftIcon: icon,
onTap: () => context
.read<ChecklistCellEditorBloc>()
.add(ChecklistCellEditorEvent.selectOption(widget.option)),
),
),
_disclosureButton(),
],
),
@ -122,8 +137,7 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
return FlowyIconButton(
width: 20,
onPressed: () => _popoverController.show(),
hoverColor: Colors.transparent,
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
icon: svgWidget(
"editor/details",
color: Theme.of(context).colorScheme.onSurface,
@ -137,15 +151,30 @@ class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
offset: const Offset(20, 0),
asBarrier: true,
constraints: BoxConstraints.loose(const Size(200, 300)),
mutex: widget.popoverMutex,
triggerActions: PopoverTriggerFlags.none,
child: child,
popupBuilder: (BuildContext popoverContext) {
return SelectOptionTypeOptionEditor(
option: widget.option.data,
onDeleted: () {},
onUpdated: (updatedOption) {},
onDeleted: () {
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(
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/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:flowy_infra/color_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@ -41,34 +42,39 @@ class _SliverChecklistPrograssBarDelegate
extends SliverPersistentHeaderDelegate {
_SliverChecklistPrograssBarDelegate();
double fixHeight = 80;
double fixHeight = 60;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
builder: (context, state) {
return Column(
children: [
if (state.percent != 0)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ChecklistPrograssBar(percent: state.percent),
return Container(
color: Theme.of(context).colorScheme.background,
padding: GridSize.typeOptionContentInsets,
child: Column(
children: [
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(
hintText: LocaleKeys.grid_checklist_panelTitle.tr(),
onChanged: (text) {
context
.read<ChecklistCellEditorBloc>()
.add(ChecklistCellEditorEvent.filterOption(text));
},
onSubmitted: (text) {
context
.read<ChecklistCellEditorBloc>()
.add(ChecklistCellEditorEvent.newOption(text));
},
)
],
if (state.percent != 0)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: ChecklistPrograssBar(percent: state.percent),
),
],
),
);
},
);

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/scrolling/styled_list.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_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
import 'package:flutter/material.dart';
@ -18,10 +19,14 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
final SelectOptionPB option;
final VoidCallback onDeleted;
final Function(SelectOptionPB) onUpdated;
final bool showOptions;
final bool autoFocus;
const SelectOptionTypeOptionEditor({
required this.option,
required this.onDeleted,
required this.onUpdated,
this.showOptions = true,
this.autoFocus = true,
Key? key,
}) : super(key: key);
@ -50,21 +55,29 @@ class SelectOptionTypeOptionEditor extends StatelessWidget {
builder: (context, state) {
List<Widget> slivers = [
SliverToBoxAdapter(
child: _OptionNameTextField(state.option.name)),
child: _OptionNameTextField(
name: state.option.name,
autoFocus: autoFocus,
)),
const SliverToBoxAdapter(child: VSpace(10)),
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(
width: 160,
child: Padding(
padding: const EdgeInsets.all(6.0),
child: CustomScrollView(
slivers: slivers,
shrinkWrap: true,
controller: ScrollController(),
physics: StyledScrollPhysics(),
),
@ -102,19 +115,21 @@ class _DeleteTag extends StatelessWidget {
class _OptionNameTextField extends StatelessWidget {
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
Widget build(BuildContext context) {
return InputTextField(
return FlowyTextField(
autoFucous: autoFocus,
text: name,
maxLength: 30,
onCanceled: () {},
onDone: (optionName) {
if (name != optionName) {
onSubmitted: (newName) {
if (name != newName) {
context
.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) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
if (event.logicalKey == LogicalKeyboardKey.escape &&
event is KeyDownEvent) {
if (widget.onExit != null) {
widget.onExit!();
}

View File

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

View File

@ -48,7 +48,7 @@ impl SelectOptionTypeOptionTransformer {
T: SelectTypeOptionSharedAction,
{
match decoded_field_type {
FieldType::SingleSelect | FieldType::MultiSelect => {
FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => {
//
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();
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);
}
}
}