|
@@ -1,3 +1,5 @@
|
|
|
+import 'dart:async';
|
|
|
+
|
|
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
|
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
|
|
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
|
@@ -30,13 +32,19 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
|
|
/// Focus node for the new task text field
|
|
|
late final FocusNode newTaskFocusNode;
|
|
|
|
|
|
- /// A flag that determines whether the new task text field is visible
|
|
|
- bool _isAddingNewTask = false;
|
|
|
-
|
|
|
@override
|
|
|
void initState() {
|
|
|
super.initState();
|
|
|
- newTaskFocusNode = FocusNode();
|
|
|
+ newTaskFocusNode = FocusNode(
|
|
|
+ onKey: (node, event) {
|
|
|
+ if (event is RawKeyDownEvent &&
|
|
|
+ event.logicalKey == LogicalKeyboardKey.escape) {
|
|
|
+ node.unfocus();
|
|
|
+ return KeyEventResult.handled;
|
|
|
+ }
|
|
|
+ return KeyEventResult.ignored;
|
|
|
+ },
|
|
|
+ );
|
|
|
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
|
|
|
..add(const ChecklistCellEditorEvent.initial());
|
|
|
}
|
|
@@ -48,59 +56,35 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
|
|
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
|
|
listener: (context, state) {
|
|
|
if (state.allOptions.isEmpty) {
|
|
|
- setState(() => _isAddingNewTask = true);
|
|
|
+ newTaskFocusNode.requestFocus();
|
|
|
}
|
|
|
},
|
|
|
builder: (context, state) {
|
|
|
- return Focus(
|
|
|
- onKey: (node, event) {
|
|
|
- // don't hide new task text field if there are no tasks at all
|
|
|
- if (state.allOptions.isNotEmpty &&
|
|
|
- event is RawKeyDownEvent &&
|
|
|
- event.logicalKey == LogicalKeyboardKey.escape) {
|
|
|
- setState(() {
|
|
|
- _isAddingNewTask = false;
|
|
|
- });
|
|
|
- return KeyEventResult.handled;
|
|
|
- }
|
|
|
- return KeyEventResult.ignored;
|
|
|
- },
|
|
|
- child: CustomScrollView(
|
|
|
- shrinkWrap: true,
|
|
|
- physics: StyledScrollPhysics(),
|
|
|
- slivers: [
|
|
|
- SliverToBoxAdapter(
|
|
|
- child: AnimatedSwitcher(
|
|
|
- duration: const Duration(milliseconds: 300),
|
|
|
- child: state.allOptions.isEmpty
|
|
|
- ? const SizedBox.shrink()
|
|
|
- : Padding(
|
|
|
- padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
|
|
- child: ChecklistProgressBar(
|
|
|
- percent: state.percent,
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ChecklistItemList(
|
|
|
- options: state.allOptions,
|
|
|
- newTaskFocusNode: newTaskFocusNode,
|
|
|
- isAddingNewTask: _isAddingNewTask,
|
|
|
- onUpdateTask: () => setState(() {
|
|
|
- _isAddingNewTask = true;
|
|
|
- newTaskFocusNode.requestFocus();
|
|
|
- }),
|
|
|
- ),
|
|
|
- const SliverToBoxAdapter(
|
|
|
- child: TypeOptionSeparator(spacing: 0.0),
|
|
|
- ),
|
|
|
- SliverToBoxAdapter(
|
|
|
- child: ChecklistNewTaskButton(
|
|
|
- onTap: () => setState(() => _isAddingNewTask = true),
|
|
|
- ),
|
|
|
- ),
|
|
|
- ],
|
|
|
- ),
|
|
|
+ return Column(
|
|
|
+ mainAxisSize: MainAxisSize.min,
|
|
|
+ children: [
|
|
|
+ AnimatedSwitcher(
|
|
|
+ duration: const Duration(milliseconds: 300),
|
|
|
+ child: state.allOptions.isEmpty
|
|
|
+ ? const SizedBox.shrink()
|
|
|
+ : Padding(
|
|
|
+ padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
|
|
+ child: ChecklistProgressBar(
|
|
|
+ percent: state.percent,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ChecklistItemList(
|
|
|
+ options: state.allOptions,
|
|
|
+ onUpdateTask: () => newTaskFocusNode.requestFocus(),
|
|
|
+ ),
|
|
|
+ if (state.allOptions.isNotEmpty)
|
|
|
+ const TypeOptionSeparator(spacing: 0.0),
|
|
|
+ Padding(
|
|
|
+ padding: const EdgeInsets.symmetric(vertical: 8),
|
|
|
+ child: NewTaskItem(focusNode: newTaskFocusNode),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
);
|
|
|
},
|
|
|
),
|
|
@@ -118,16 +102,12 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
|
|
/// a new task if `isAddingNewTask` is true
|
|
|
class ChecklistItemList extends StatefulWidget {
|
|
|
final List<ChecklistSelectOption> options;
|
|
|
- final FocusNode newTaskFocusNode;
|
|
|
- final bool isAddingNewTask;
|
|
|
final VoidCallback onUpdateTask;
|
|
|
|
|
|
const ChecklistItemList({
|
|
|
super.key,
|
|
|
required this.options,
|
|
|
required this.onUpdateTask,
|
|
|
- required this.isAddingNewTask,
|
|
|
- required this.newTaskFocusNode,
|
|
|
});
|
|
|
|
|
|
@override
|
|
@@ -137,32 +117,28 @@ class ChecklistItemList extends StatefulWidget {
|
|
|
class _ChecklistItemListState extends State<ChecklistItemList> {
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
- final itemList = [
|
|
|
- const VSpace(6.0),
|
|
|
- ...widget.options.mapIndexed(
|
|
|
- (index, option) => Padding(
|
|
|
- padding: const EdgeInsets.symmetric(vertical: 2),
|
|
|
- child: ChecklistItem(
|
|
|
+ if (widget.options.isEmpty) {
|
|
|
+ return const SizedBox.shrink();
|
|
|
+ }
|
|
|
+
|
|
|
+ final itemList = widget.options
|
|
|
+ .mapIndexed(
|
|
|
+ (index, option) => ChecklistItem(
|
|
|
option: option,
|
|
|
onSubmitted:
|
|
|
index == widget.options.length - 1 ? widget.onUpdateTask : null,
|
|
|
key: ValueKey(option.data.id),
|
|
|
- // only allow calling the callback for the last task in the list
|
|
|
),
|
|
|
- ),
|
|
|
- ),
|
|
|
- AnimatedSwitcher(
|
|
|
- duration: const Duration(milliseconds: 300),
|
|
|
- child: widget.isAddingNewTask
|
|
|
- ? NewTaskItem(focusNode: widget.newTaskFocusNode)
|
|
|
- : const SizedBox.shrink(),
|
|
|
- ),
|
|
|
- const VSpace(6.0),
|
|
|
- ];
|
|
|
- return SliverList(
|
|
|
- delegate: SliverChildBuilderDelegate(
|
|
|
- (BuildContext context, int index) => itemList[index],
|
|
|
- childCount: itemList.length,
|
|
|
+ )
|
|
|
+ .toList();
|
|
|
+
|
|
|
+ return Flexible(
|
|
|
+ child: ListView.separated(
|
|
|
+ itemBuilder: (context, index) => itemList[index],
|
|
|
+ separatorBuilder: (context, index) => const VSpace(4),
|
|
|
+ itemCount: itemList.length,
|
|
|
+ shrinkWrap: true,
|
|
|
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
|
),
|
|
|
);
|
|
|
}
|
|
@@ -187,6 +163,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
|
|
late final TextEditingController _textController;
|
|
|
late final FocusNode _focusNode;
|
|
|
bool _isHovered = false;
|
|
|
+ Timer? _debounceOnChanged;
|
|
|
|
|
|
@override
|
|
|
void initState() {
|
|
@@ -249,13 +226,9 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
|
|
),
|
|
|
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
|
|
),
|
|
|
- onSubmitted: (taskDescription) {
|
|
|
- context.read<ChecklistCellEditorBloc>().add(
|
|
|
- ChecklistCellEditorEvent.updateTaskName(
|
|
|
- widget.option.data,
|
|
|
- taskDescription,
|
|
|
- ),
|
|
|
- );
|
|
|
+ onChanged: _debounceOnChangedText,
|
|
|
+ onSubmitted: (description) {
|
|
|
+ _submitUpdateTaskDescription(description);
|
|
|
widget.onSubmitted?.call();
|
|
|
},
|
|
|
),
|
|
@@ -276,6 +249,22 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
+
|
|
|
+ void _debounceOnChangedText(String text) {
|
|
|
+ _debounceOnChanged?.cancel();
|
|
|
+ _debounceOnChanged = Timer(const Duration(milliseconds: 300), () {
|
|
|
+ _submitUpdateTaskDescription(text);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ void _submitUpdateTaskDescription(String description) {
|
|
|
+ context.read<ChecklistCellEditorBloc>().add(
|
|
|
+ ChecklistCellEditorEvent.updateTaskName(
|
|
|
+ widget.option.data,
|
|
|
+ description,
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// Creates a new task after entering the description and pressing enter.
|
|
@@ -304,19 +293,12 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
|
|
@override
|
|
|
Widget build(BuildContext context) {
|
|
|
return Container(
|
|
|
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
|
|
+ padding: const EdgeInsets.symmetric(horizontal: 8),
|
|
|
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
|
|
child: Row(
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
children: [
|
|
|
- const FlowyIconButton(
|
|
|
- width: 32,
|
|
|
- icon: FlowySvg(
|
|
|
- FlowySvgs.uncheck_s,
|
|
|
- blendMode: BlendMode.dst,
|
|
|
- ),
|
|
|
- hoverColor: Colors.transparent,
|
|
|
- ),
|
|
|
+ const HSpace(8),
|
|
|
Expanded(
|
|
|
child: TextField(
|
|
|
focusNode: widget.focusNode,
|
|
@@ -330,7 +312,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
|
|
vertical: 6.0,
|
|
|
horizontal: 2.0,
|
|
|
),
|
|
|
- hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
|
|
+ hintText: LocaleKeys.grid_checklist_addNew.tr(),
|
|
|
),
|
|
|
onSubmitted: (taskDescription) {
|
|
|
if (taskDescription.trim().isNotEmpty) {
|
|
@@ -340,15 +322,21 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
+ widget.focusNode.requestFocus();
|
|
|
_textEditingController.clear();
|
|
|
},
|
|
|
+ onChanged: (value) => setState(() {}),
|
|
|
),
|
|
|
),
|
|
|
FlowyTextButton(
|
|
|
LocaleKeys.grid_checklist_submitNewTask.tr(),
|
|
|
fontSize: 11,
|
|
|
- fillColor: Theme.of(context).colorScheme.primary,
|
|
|
- hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
|
|
+ fillColor: _textEditingController.text.isEmpty
|
|
|
+ ? Theme.of(context).disabledColor
|
|
|
+ : Theme.of(context).colorScheme.primary,
|
|
|
+ hoverColor: _textEditingController.text.isEmpty
|
|
|
+ ? Theme.of(context).disabledColor
|
|
|
+ : Theme.of(context).colorScheme.primaryContainer,
|
|
|
fontColor: Theme.of(context).colorScheme.onPrimary,
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
|
|
onPressed: () {
|
|
@@ -359,6 +347,7 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
|
|
),
|
|
|
);
|
|
|
}
|
|
|
+ widget.focusNode.requestFocus();
|
|
|
_textEditingController.clear();
|
|
|
},
|
|
|
),
|
|
@@ -367,25 +356,3 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-@visibleForTesting
|
|
|
-class ChecklistNewTaskButton extends StatelessWidget {
|
|
|
- final VoidCallback onTap;
|
|
|
- const ChecklistNewTaskButton({super.key, required this.onTap});
|
|
|
-
|
|
|
- @override
|
|
|
- Widget build(BuildContext context) {
|
|
|
- return Padding(
|
|
|
- padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
|
|
- child: SizedBox(
|
|
|
- height: 30,
|
|
|
- child: FlowyButton(
|
|
|
- text: FlowyText.medium(LocaleKeys.grid_checklist_addNew.tr()),
|
|
|
- margin: const EdgeInsets.all(6),
|
|
|
- leftIcon: const FlowySvg(FlowySvgs.add_s),
|
|
|
- onTap: onTap,
|
|
|
- ),
|
|
|
- ),
|
|
|
- );
|
|
|
- }
|
|
|
-}
|