Bläddra i källkod

chore: revamp checklist ui (#3380)

* chore: revamp checklist editor  ui

* chore: checklist progress bar

* test: integration tests

* fix: flutter analyzer errors

* fix: checklist percentage complete
Richard Shiue 1 år sedan
förälder
incheckning
0c6a1d4ae7
36 ändrade filer med 747 tillägg och 394 borttagningar
  1. 112 0
      frontend/appflowy_flutter/integration_test/database_cell_test.dart
  2. 10 10
      frontend/appflowy_flutter/integration_test/database_share_test.dart
  3. 138 13
      frontend/appflowy_flutter/integration_test/util/database_test_op.dart
  4. 8 2
      frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart
  5. 10 4
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart
  6. 7 3
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart
  7. 19 9
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart
  8. 324 122
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart
  9. 40 50
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart
  10. 25 110
      frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart
  11. 4 36
      frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart
  12. 24 11
      frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart
  13. 1 1
      frontend/resources/translations/ar-SA.json
  14. 1 1
      frontend/resources/translations/ca-ES.json
  15. 1 1
      frontend/resources/translations/de-DE.json
  16. 3 1
      frontend/resources/translations/en.json
  17. 1 1
      frontend/resources/translations/es-VE.json
  18. 1 1
      frontend/resources/translations/eu-ES.json
  19. 1 1
      frontend/resources/translations/fa.json
  20. 1 1
      frontend/resources/translations/fr-CA.json
  21. 1 1
      frontend/resources/translations/fr-FR.json
  22. 1 1
      frontend/resources/translations/hu-HU.json
  23. 1 1
      frontend/resources/translations/id-ID.json
  24. 1 1
      frontend/resources/translations/it-IT.json
  25. 1 1
      frontend/resources/translations/ja-JP.json
  26. 1 1
      frontend/resources/translations/ko-KR.json
  27. 1 1
      frontend/resources/translations/pl-PL.json
  28. 1 1
      frontend/resources/translations/pt-BR.json
  29. 1 1
      frontend/resources/translations/pt-PT.json
  30. 1 1
      frontend/resources/translations/ru-RU.json
  31. 1 1
      frontend/resources/translations/sv.json
  32. 1 1
      frontend/resources/translations/tr-TR.json
  33. 1 1
      frontend/resources/translations/zh-CN.json
  34. 1 1
      frontend/resources/translations/zh-TW.json
  35. 1 1
      frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs
  36. 1 1
      frontend/rust-lib/flowy-test/tests/database/local_test/test.rs

+ 112 - 0
frontend/appflowy_flutter/integration_test/database_cell_test.dart

@@ -437,4 +437,116 @@ void main() {
       await tester.pumpAndSettle();
     });
   });
+
+  testWidgets('edit checklist cell', (tester) async {
+    await tester.initializeAppFlowy();
+    await tester.tapGoButton();
+
+    await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
+
+    const fieldType = FieldType.Checklist;
+    await tester.createField(fieldType, fieldType.name);
+
+    // assert that there is no progress bar in the grid
+    tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
+
+    // tap on the first checklist cell
+    await tester.tapChecklistCellInGrid(rowIndex: 0);
+
+    // assert that the checklist editor is shown
+    tester.assertChecklistEditorVisible(visible: true);
+
+    // assert that new task editor is shown
+    tester.assertNewCheckListTaskEditorVisible(visible: true);
+
+    // create a new task with enter
+    await tester.createNewChecklistTask(name: "task 0", enter: true);
+
+    // assert that the task is displayed
+    tester.assertChecklistTaskInEditor(
+      index: 0,
+      name: "task 0",
+      isChecked: false,
+    );
+
+    // update the task's name
+    await tester.renameChecklistTask(index: 0, name: "task 1");
+
+    // assert that the task's name is updated
+    tester.assertChecklistTaskInEditor(
+      index: 0,
+      name: "task 1",
+      isChecked: false,
+    );
+
+    // dismiss new task editor
+    await tester.dismissCellEditor();
+    tester.assertNewCheckListTaskEditorVisible(visible: false);
+
+    // dismiss checklist cell editor
+    await tester.dismissCellEditor();
+
+    // assert that progress bar is shown in grid at 0%
+    tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
+
+    // start editing the first checklist cell again, click on new task button
+    await tester.tapChecklistCellInGrid(rowIndex: 0);
+    tester.assertNewCheckListTaskEditorVisible(visible: false);
+    await tester.tapChecklistNewTaskButton();
+    tester.assertNewCheckListTaskEditorVisible(visible: true);
+
+    // create another task with the create button
+    await tester.createNewChecklistTask(name: "task 2", button: true);
+
+    // assert that the task was inserted
+    tester.assertChecklistTaskInEditor(
+      index: 1,
+      name: "task 2",
+      isChecked: false,
+    );
+
+    // mark it as complete
+    await tester.checkChecklistTask(index: 1);
+
+    // assert that the task was checked in the editor
+    tester.assertChecklistTaskInEditor(
+      index: 1,
+      name: "task 2",
+      isChecked: true,
+    );
+
+    // dismiss checklist editor
+    await tester.dismissCellEditor();
+    await tester.dismissCellEditor();
+
+    // assert that progressbar is shown in grid at 50%
+    tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
+
+    // re-open the cell editor
+    await tester.tapChecklistCellInGrid(rowIndex: 0);
+
+    // hover over first task and delete it
+    await tester.deleteChecklistTask(index: 0);
+
+    // dismiss cell editor
+    await tester.dismissCellEditor();
+
+    // assert that progressbar is shown in grid at 100%
+    tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
+
+    // re-open the cell edior
+    await tester.tapChecklistCellInGrid(rowIndex: 0);
+
+    // delete the remaining task
+    await tester.deleteChecklistTask(index: 0);
+
+    // assert that the new task editor is shown
+    tester.assertNewCheckListTaskEditorVisible(visible: true);
+
+    // dismiss the cell editor
+    await tester.dismissCellEditor();
+
+    // check that the progress bar is not viisble
+    tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
+  });
 }

+ 10 - 10
frontend/appflowy_flutter/integration_test/database_share_test.dart

@@ -122,17 +122,17 @@ void main() {
       }
 
       // check the checklist cell
-      final List<double> checklistCells = [
-        0.6,
-        0.3,
+      final List<double?> checklistCells = [
+        0.67,
+        0.33,
         1.0,
-        0.0,
-        0.0,
-        0.0,
-        0.0,
-        0.0,
-        0.0,
-        0.0,
+        null,
+        null,
+        null,
+        null,
+        null,
+        null,
+        null,
       ];
       for (final (index, percent) in checklistCells.indexed) {
         await tester.assertChecklistCellInGrid(

+ 138 - 13
frontend/appflowy_flutter/integration_test/util/database_test_op.dart

@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:appflowy/generated/flowy_svgs.g.dart';
 import 'package:appflowy/generated/locale_keys.g.dart';
 import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
 import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
@@ -38,6 +39,7 @@ import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'
 import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
+import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
 import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
@@ -243,23 +245,33 @@ extension AppFlowyDatabaseTest on WidgetTester {
     }
   }
 
+  /// null percent means no progress bar should be found
   Future<void> assertChecklistCellInGrid({
     required int rowIndex,
-    required double percent,
+    required double? percent,
   }) async {
     final findCell = cellFinder(rowIndex, FieldType.Checklist);
-    final finder = find.descendant(
-      of: findCell,
-      matching: find.byWidgetPredicate(
-        (widget) {
-          if (widget is ChecklistProgressBar) {
-            return widget.percent == percent;
-          }
-          return false;
-        },
-      ),
-    );
-    expect(finder, findsOneWidget);
+
+    if (percent == null) {
+      final finder = find.descendant(
+        of: findCell,
+        matching: find.byType(ChecklistProgressBar),
+      );
+      expect(finder, findsNothing);
+    } else {
+      final finder = find.descendant(
+        of: findCell,
+        matching: find.byWidgetPredicate(
+          (widget) {
+            if (widget is ChecklistProgressBar) {
+              return widget.percent == percent;
+            }
+            return false;
+          },
+        ),
+      );
+      expect(finder, findsOneWidget);
+    }
   }
 
   Future<void> assertDateCellInGrid({
@@ -450,6 +462,119 @@ extension AppFlowyDatabaseTest on WidgetTester {
     expect(cell, matcher);
   }
 
+  Future<void> tapChecklistCellInGrid({required int rowIndex}) async {
+    final findRow = find.byType(GridRow);
+    final findCell = finderForFieldType(FieldType.Checklist);
+
+    final cell = find.descendant(
+      of: findRow.at(rowIndex),
+      matching: findCell,
+    );
+
+    await tapButton(cell);
+  }
+
+  void assertChecklistEditorVisible({required bool visible}) {
+    final editor = find.byType(GridChecklistCellEditor);
+    if (visible) {
+      expect(editor, findsOneWidget);
+    } else {
+      expect(editor, findsNothing);
+    }
+  }
+
+  void assertNewCheckListTaskEditorVisible({required bool visible}) {
+    final editor = find.byType(NewTaskItem);
+    if (visible) {
+      expect(editor, findsOneWidget);
+    } else {
+      expect(editor, findsNothing);
+    }
+  }
+
+  Future<void> createNewChecklistTask({
+    required String name,
+    enter = false,
+    button = false,
+  }) async {
+    assert(!(enter && button));
+    final textField = find.descendant(
+      of: find.byType(NewTaskItem),
+      matching: find.byType(TextField),
+    );
+
+    await enterText(textField, name);
+    await pumpAndSettle(const Duration(milliseconds: 300));
+    if (enter) {
+      await testTextInput.receiveAction(TextInputAction.done);
+      await pumpAndSettle(const Duration(milliseconds: 300));
+    } else {
+      await tapButton(
+        find.descendant(
+          of: find.byType(NewTaskItem),
+          matching: find.byType(FlowyTextButton),
+        ),
+      );
+    }
+  }
+
+  void assertChecklistTaskInEditor({
+    required int index,
+    required String name,
+    required bool isChecked,
+  }) {
+    final task = find.byType(ChecklistItem).at(index);
+
+    final widget = this.widget<ChecklistItem>(task);
+    assert(
+      widget.option.data.name == name && widget.option.isSelected == isChecked,
+    );
+  }
+
+  Future<void> renameChecklistTask({
+    required int index,
+    required String name,
+  }) async {
+    final textField = find
+        .descendant(
+          of: find.byType(ChecklistItem),
+          matching: find.byType(TextField),
+        )
+        .at(index);
+
+    await enterText(textField, name);
+    await testTextInput.receiveAction(TextInputAction.done);
+    await pumpAndSettle(const Duration(milliseconds: 300));
+  }
+
+  Future<void> tapChecklistNewTaskButton() async {
+    await tapButton(find.byType(ChecklistNewTaskButton));
+  }
+
+  Future<void> checkChecklistTask({required int index}) async {
+    final button = find.descendant(
+      of: find.byType(ChecklistItem).at(index),
+      matching: find.byWidgetPredicate(
+        (widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,
+      ),
+    );
+
+    await tapButton(button);
+  }
+
+  Future<void> deleteChecklistTask({required int index}) async {
+    final task = find.byType(ChecklistItem).at(index);
+
+    await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
+    await pumpAndSettle();
+
+    final button = find.byWidgetPredicate(
+      (widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
+    );
+
+    await tapButton(button);
+  }
+
   Future<void> openFirstRowDetailPage() async {
     await hoverOnFirstRowOfGrid();
 

+ 8 - 2
frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart

@@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
 import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
 import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
 import 'package:dartz/dartz.dart';
+import 'package:protobuf/protobuf.dart';
 
 class ChecklistCellBackendService {
   final String viewId;
@@ -52,14 +53,19 @@ class ChecklistCellBackendService {
     return DatabaseEventUpdateChecklistCell(payload).send();
   }
 
-  Future<Either<Unit, FlowyError>> update({
+  Future<Either<Unit, FlowyError>> updateName({
     required SelectOptionPB option,
+    required name,
   }) {
+    option.freeze();
+    final newOption = option.rebuild((option) {
+      option.name = name;
+    });
     final payload = ChecklistCellDataChangesetPB.create()
       ..viewId = viewId
       ..fieldId = fieldId
       ..rowId = rowId
-      ..updateOptions.add(option);
+      ..updateOptions.add(newOption);
 
     return DatabaseEventUpdateChecklistCell(payload).send();
   }

+ 10 - 4
frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart

@@ -32,10 +32,16 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
     return BlocProvider.value(
       value: _cellBloc,
       child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
-        builder: (context, state) => Padding(
-          padding: const EdgeInsets.symmetric(vertical: 4),
-          child: ChecklistProgressBar(percent: state.percent),
-        ),
+        builder: (context, state) {
+          if (state.allOptions.isEmpty) {
+            return const SizedBox.shrink();
+          }
+
+          return Padding(
+            padding: const EdgeInsets.symmetric(vertical: 4),
+            child: ChecklistProgressBar(percent: state.percent),
+          );
+        },
       ),
     );
   }

+ 7 - 3
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart

@@ -40,7 +40,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
       child: AppFlowyPopover(
         margin: EdgeInsets.zero,
         controller: _popover,
-        constraints: BoxConstraints.loose(const Size(260, 400)),
+        constraints: BoxConstraints.loose(const Size(360, 400)),
         direction: PopoverDirection.bottomWithLeftAligned,
         triggerActions: PopoverTriggerFlags.none,
         popupBuilder: (BuildContext context) {
@@ -56,8 +56,12 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
         child: Padding(
           padding: GridSize.cellContentInsets,
           child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
-            builder: (context, state) =>
-                ChecklistProgressBar(percent: state.percent),
+            builder: (context, state) {
+              if (state.allOptions.isEmpty) {
+                return const SizedBox.shrink();
+              }
+              return ChecklistProgressBar(percent: state.percent);
+            },
           ),
         ),
       ),

+ 19 - 9
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart

@@ -29,13 +29,23 @@ class ChecklistCardCellBloc
             _loadOptions();
           },
           didReceiveOptions: (data) {
-            emit(
-              state.copyWith(
-                allOptions: data.options,
-                selectedOptions: data.selectedOptions,
-                percent: data.percentage,
-              ),
-            );
+            if (data == null) {
+              emit(
+                const ChecklistCellState(
+                  allOptions: [],
+                  selectedOptions: [],
+                  percent: 0,
+                ),
+              );
+            } else {
+              emit(
+                state.copyWith(
+                  allOptions: data.options,
+                  selectedOptions: data.selectedOptions,
+                  percent: data.percentage,
+                ),
+              );
+            }
           },
         );
       },
@@ -58,7 +68,7 @@ class ChecklistCardCellBloc
         _loadOptions();
       },
       onCellChanged: (data) {
-        if (!isClosed && data != null) {
+        if (!isClosed) {
           add(ChecklistCellEvent.didReceiveOptions(data));
         }
       },
@@ -81,7 +91,7 @@ class ChecklistCardCellBloc
 class ChecklistCellEvent with _$ChecklistCellEvent {
   const factory ChecklistCellEvent.initial() = _InitialCell;
   const factory ChecklistCellEvent.didReceiveOptions(
-    ChecklistCellDataPB data,
+    ChecklistCellDataPB? data,
   ) = _DidReceiveCellUpdate;
 }
 

+ 324 - 122
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart

@@ -1,21 +1,23 @@
 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';
-import 'package:appflowy_popover/appflowy_popover.dart';
-
+import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
+import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
+import 'package:collection/collection.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/size.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_bloc/flutter_bloc.dart';
 
-import '../../../../grid/presentation/layout/sizes.dart';
-import '../../../../grid/presentation/widgets/header/type_option/select_option_editor.dart';
 import 'checklist_cell_editor_bloc.dart';
 import 'checklist_progress_bar.dart';
 
 class GridChecklistCellEditor extends StatefulWidget {
   final ChecklistCellController cellController;
-  const GridChecklistCellEditor({required this.cellController, Key? key})
-      : super(key: key);
+  const GridChecklistCellEditor({required this.cellController, super.key});
 
   @override
   State<GridChecklistCellEditor> createState() =>
@@ -23,167 +25,367 @@ class GridChecklistCellEditor extends StatefulWidget {
 }
 
 class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
-  late ChecklistCellEditorBloc bloc;
-  late PopoverMutex popoverMutex;
+  late ChecklistCellEditorBloc _bloc;
+
+  /// 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() {
-    popoverMutex = PopoverMutex();
-    bloc = ChecklistCellEditorBloc(cellController: widget.cellController);
-    bloc.add(const ChecklistCellEditorEvent.initial());
     super.initState();
-  }
-
-  @override
-  void dispose() {
-    bloc.close();
-    super.dispose();
+    newTaskFocusNode = FocusNode();
+    _bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
+      ..add(const ChecklistCellEditorEvent.initial());
   }
 
   @override
   Widget build(BuildContext context) {
     return BlocProvider.value(
-      value: bloc,
-      child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
+      value: _bloc,
+      child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
+        listener: (context, state) {
+          if (state.allOptions.isEmpty) {
+            setState(() => _isAddingNewTask = true);
+          }
+        },
         builder: (context, state) {
-          final List<Widget> slivers = [
-            const SliverChecklistProgressBar(),
-            SliverToBoxAdapter(
-              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.all(8.0),
-            child: ScrollConfiguration(
-              behavior: const ScrollBehavior().copyWith(scrollbars: false),
-              child: CustomScrollView(
-                shrinkWrap: true,
-                slivers: slivers,
-                controller: ScrollController(),
-                physics: StyledScrollPhysics(),
-              ),
+          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),
+                  ),
+                ),
+              ],
             ),
           );
         },
       ),
     );
   }
+
+  @override
+  void dispose() {
+    _bloc.close();
+    super.dispose();
+  }
+}
+
+/// Displays the a list of all the exisiting tasks and an input field to create
+/// 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
+  State<ChecklistItemList> createState() => _ChecklistItemListState();
+}
+
+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(
+            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,
+      ),
+    );
+  }
 }
 
-class _ChecklistOptionCell extends StatefulWidget {
+/// Represents an existing task
+@visibleForTesting
+class ChecklistItem extends StatefulWidget {
   final ChecklistSelectOption option;
-  final PopoverMutex popoverMutex;
-  const _ChecklistOptionCell({
+  final VoidCallback? onSubmitted;
+  const ChecklistItem({
     required this.option,
-    required this.popoverMutex,
     Key? key,
+    this.onSubmitted,
   }) : super(key: key);
 
   @override
-  State<_ChecklistOptionCell> createState() => _ChecklistOptionCellState();
+  State<ChecklistItem> createState() => _ChecklistItemState();
 }
 
-class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
-  late PopoverController _popoverController;
+class _ChecklistItemState extends State<ChecklistItem> {
+  late final TextEditingController _textController;
+  late final FocusNode _focusNode;
+  bool _isHovered = false;
 
   @override
   void initState() {
-    _popoverController = PopoverController();
     super.initState();
+    _textController = TextEditingController(text: widget.option.data.name);
+    _focusNode = FocusNode(
+      onKey: (node, event) {
+        if (event is RawKeyDownEvent &&
+            event.logicalKey == LogicalKeyboardKey.escape) {
+          node.unfocus();
+          return KeyEventResult.handled;
+        }
+        return KeyEventResult.ignored;
+      },
+    );
   }
 
   @override
   Widget build(BuildContext context) {
-    final icon = widget.option.isSelected
-        ? const FlowySvg(
-            FlowySvgs.check_filled_s,
-            blendMode: BlendMode.dst,
-          )
-        : const FlowySvg(FlowySvgs.uncheck_s);
-    return _wrapPopover(
-      SizedBox(
-        height: GridSize.popoverItemHeight,
-        child: Row(
-          children: [
-            Expanded(
-              child: FlowyButton(
-                hoverColor: AFThemeExtension.of(context).lightGreyHover,
-                text: FlowyText(
-                  widget.option.data.name,
-                  color: AFThemeExtension.of(context).textColor,
+    final icon = FlowySvg(
+      widget.option.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
+      blendMode: BlendMode.dst,
+    );
+    return MouseRegion(
+      onEnter: (event) => setState(() => _isHovered = true),
+      onExit: (event) => setState(() => _isHovered = false),
+      child: Container(
+        padding: const EdgeInsets.symmetric(horizontal: 8.0),
+        constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
+        child: DecoratedBox(
+          decoration: BoxDecoration(
+            color: _isHovered
+                ? AFThemeExtension.of(context).lightGreyHover
+                : Colors.transparent,
+            borderRadius: Corners.s6Border,
+          ),
+          child: Row(
+            crossAxisAlignment: CrossAxisAlignment.center,
+            children: [
+              FlowyIconButton(
+                width: 32,
+                icon: icon,
+                hoverColor: Colors.transparent,
+                onPressed: () => context.read<ChecklistCellEditorBloc>().add(
+                      ChecklistCellEditorEvent.selectTask(widget.option.data),
+                    ),
+              ),
+              Expanded(
+                child: TextField(
+                  controller: _textController,
+                  focusNode: _focusNode,
+                  style: Theme.of(context).textTheme.bodyMedium,
+                  maxLines: 1,
+                  decoration: InputDecoration(
+                    border: InputBorder.none,
+                    isCollapsed: true,
+                    contentPadding: const EdgeInsets.symmetric(
+                      vertical: 6.0,
+                      horizontal: 2.0,
+                    ),
+                    hintText: LocaleKeys.grid_checklist_taskHint.tr(),
+                  ),
+                  onSubmitted: (taskDescription) {
+                    context.read<ChecklistCellEditorBloc>().add(
+                          ChecklistCellEditorEvent.updateTaskName(
+                            widget.option.data,
+                            taskDescription,
+                          ),
+                        );
+                    widget.onSubmitted?.call();
+                  },
                 ),
-                leftIcon: icon,
-                onTap: () => context
-                    .read<ChecklistCellEditorBloc>()
-                    .add(ChecklistCellEditorEvent.selectOption(widget.option)),
               ),
-            ),
-            _disclosureButton(),
-          ],
+              if (_isHovered)
+                FlowyIconButton(
+                  width: 32,
+                  icon: const FlowySvg(FlowySvgs.delete_s),
+                  hoverColor: Colors.transparent,
+                  iconColorOnHover: Theme.of(context).colorScheme.error,
+                  onPressed: () => context.read<ChecklistCellEditorBloc>().add(
+                        ChecklistCellEditorEvent.deleteTask(widget.option.data),
+                      ),
+                ),
+            ],
+          ),
         ),
       ),
     );
   }
+}
+
+/// Creates a new task after entering the description and pressing enter.
+/// This can be cancelled by pressing escape
+@visibleForTesting
+class NewTaskItem extends StatefulWidget {
+  final FocusNode focusNode;
+  const NewTaskItem({super.key, required this.focusNode});
+
+  @override
+  State<NewTaskItem> createState() => _NewTaskItemState();
+}
+
+class _NewTaskItemState extends State<NewTaskItem> {
+  late final TextEditingController _textEditingController;
+
+  @override
+  void initState() {
+    super.initState();
+    _textEditingController = TextEditingController();
+    if (widget.focusNode.canRequestFocus) {
+      widget.focusNode.requestFocus();
+    }
+  }
 
-  Widget _disclosureButton() {
-    return FlowyIconButton(
-      hoverColor: AFThemeExtension.of(context).lightGreyHover,
-      width: 20,
-      onPressed: () => _popoverController.show(),
-      iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
-      icon: FlowySvg(
-        FlowySvgs.details_s,
-        color: Theme.of(context).iconTheme.color,
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      padding: const EdgeInsets.symmetric(horizontal: 8.0),
+      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,
+          ),
+          Expanded(
+            child: TextField(
+              focusNode: widget.focusNode,
+              controller: _textEditingController,
+              style: Theme.of(context).textTheme.bodyMedium,
+              maxLines: 1,
+              decoration: InputDecoration(
+                border: InputBorder.none,
+                isCollapsed: true,
+                contentPadding: const EdgeInsets.symmetric(
+                  vertical: 6.0,
+                  horizontal: 2.0,
+                ),
+                hintText: LocaleKeys.grid_checklist_taskHint.tr(),
+              ),
+              onSubmitted: (taskDescription) {
+                if (taskDescription.trim().isNotEmpty) {
+                  context.read<ChecklistCellEditorBloc>().add(
+                        ChecklistCellEditorEvent.newTask(
+                          taskDescription.trim(),
+                        ),
+                      );
+                }
+                _textEditingController.clear();
+              },
+            ),
+          ),
+          FlowyTextButton(
+            LocaleKeys.grid_checklist_submitNewTask.tr(),
+            fontSize: 11,
+            fillColor: Theme.of(context).colorScheme.primary,
+            hoverColor: Theme.of(context).colorScheme.primaryContainer,
+            fontColor: Theme.of(context).colorScheme.onPrimary,
+            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+            onPressed: () {
+              if (_textEditingController.text.trim().isNotEmpty) {
+                context.read<ChecklistCellEditorBloc>().add(
+                      ChecklistCellEditorEvent.newTask(
+                        _textEditingController.text..trim(),
+                      ),
+                    );
+              }
+              _textEditingController.clear();
+            },
+          ),
+        ],
       ),
     );
   }
+}
 
-  Widget _wrapPopover(Widget child) {
-    return AppFlowyPopover(
-      controller: _popoverController,
-      offset: const Offset(8, 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: () {
-            context.read<ChecklistCellEditorBloc>().add(
-                  ChecklistCellEditorEvent.deleteOption(widget.option.data),
-                );
-
-            _popoverController.close();
-          },
-          onUpdated: (updatedOption) {
-            context.read<ChecklistCellEditorBloc>().add(
-                  ChecklistCellEditorEvent.updateOption(updatedOption),
-                );
-          },
-          showOptions: false,
-          autoFocus: false,
-          // Use ValueKey to refresh the UI, otherwise, it will remain the old value.
-          key: ValueKey(
-            widget.option.data.id,
-          ),
-        );
-      },
+@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,
+        ),
+      ),
     );
   }
 }

+ 40 - 50
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart

@@ -11,6 +11,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
 
 part 'checklist_cell_editor_bloc.freezed.dart';
 
+class ChecklistSelectOption {
+  final bool isSelected;
+  final SelectOptionPB data;
+
+  ChecklistSelectOption(this.isSelected, this.data);
+}
+
 class ChecklistCellEditorBloc
     extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
   final ChecklistCellBackendService _checklistCellService;
@@ -31,33 +38,31 @@ class ChecklistCellEditorBloc
             _startListening();
             _loadOptions();
           },
-          didReceiveOptions: (data) {
+          didReceiveTasks: (data) {
             emit(
               state.copyWith(
-                allOptions: _makeChecklistSelectOptions(data, state.predicate),
-                percent: data.percentage,
+                allOptions: _makeChecklistSelectOptions(data),
+                percent: data?.percentage ?? 0,
               ),
             );
           },
-          newOption: (optionName) {
-            _createOption(optionName);
+          newTask: (optionName) async {
+            await _createOption(optionName);
             emit(
               state.copyWith(
                 createOption: Some(optionName),
-                predicate: '',
               ),
             );
           },
-          deleteOption: (option) {
-            _deleteOption([option]);
+          deleteTask: (option) async {
+            await _deleteOption([option]);
           },
-          updateOption: (option) {
-            _updateOption(option);
+          updateTaskName: (option, name) {
+            _updateOption(option, name);
           },
-          selectOption: (option) async {
-            await _checklistCellService.select(optionId: option.data.id);
+          selectTask: (option) async {
+            await _checklistCellService.select(optionId: option.id);
           },
-          filterOption: (String predicate) {},
         );
       },
     );
@@ -69,22 +74,21 @@ class ChecklistCellEditorBloc
     return super.close();
   }
 
-  void _createOption(String name) async {
+  Future<void> _createOption(String name) async {
     final result = await _checklistCellService.create(name: name);
     result.fold((l) => {}, (err) => Log.error(err));
   }
 
-  void _deleteOption(List<SelectOptionPB> options) async {
+  Future<void> _deleteOption(List<SelectOptionPB> options) async {
     final result = await _checklistCellService.delete(
       optionIds: options.map((e) => e.id).toList(),
     );
     result.fold((l) => null, (err) => Log.error(err));
   }
 
-  void _updateOption(SelectOptionPB option) async {
-    final result = await _checklistCellService.update(
-      option: option,
-    );
+  void _updateOption(SelectOptionPB option, String name) async {
+    final result =
+        await _checklistCellService.updateName(option: option, name: name);
 
     result.fold((l) => null, (err) => Log.error(err));
   }
@@ -94,7 +98,7 @@ class ChecklistCellEditorBloc
       if (isClosed) return;
 
       return result.fold(
-        (data) => add(ChecklistCellEditorEvent.didReceiveOptions(data)),
+        (data) => add(ChecklistCellEditorEvent.didReceiveTasks(data)),
         (err) => Log.error(err),
       );
     });
@@ -103,8 +107,8 @@ class ChecklistCellEditorBloc
   void _startListening() {
     cellController.startListening(
       onCellChanged: ((data) {
-        if (!isClosed && data != null) {
-          add(ChecklistCellEditorEvent.didReceiveOptions(data));
+        if (!isClosed) {
+          add(ChecklistCellEditorEvent.didReceiveTasks(data));
         }
       }),
       onCellFieldChanged: () {
@@ -117,20 +121,19 @@ class ChecklistCellEditorBloc
 @freezed
 class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
   const factory ChecklistCellEditorEvent.initial() = _Initial;
-  const factory ChecklistCellEditorEvent.didReceiveOptions(
-    ChecklistCellDataPB data,
-  ) = _DidReceiveOptions;
-  const factory ChecklistCellEditorEvent.newOption(String optionName) =
-      _NewOption;
-  const factory ChecklistCellEditorEvent.selectOption(
-    ChecklistSelectOption option,
-  ) = _SelectOption;
-  const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) =
-      _UpdateOption;
-  const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) =
-      _DeleteOption;
-  const factory ChecklistCellEditorEvent.filterOption(String predicate) =
-      _FilterOption;
+  const factory ChecklistCellEditorEvent.didReceiveTasks(
+    ChecklistCellDataPB? data,
+  ) = _DidReceiveTasks;
+  const factory ChecklistCellEditorEvent.newTask(String taskName) = _NewOption;
+  const factory ChecklistCellEditorEvent.selectTask(
+    SelectOptionPB option,
+  ) = _SelectTask;
+  const factory ChecklistCellEditorEvent.updateTaskName(
+    SelectOptionPB option,
+    String name,
+  ) = _UpdateTaskName;
+  const factory ChecklistCellEditorEvent.deleteTask(SelectOptionPB option) =
+      _DeleteTask;
 }
 
 @freezed
@@ -139,24 +142,21 @@ class ChecklistCellEditorState with _$ChecklistCellEditorState {
     required List<ChecklistSelectOption> allOptions,
     required Option<String> createOption,
     required double percent,
-    required String predicate,
   }) = _ChecklistCellEditorState;
 
   factory ChecklistCellEditorState.initial(ChecklistCellController context) {
     final data = context.getCellData(loadIfNotExist: true);
 
     return ChecklistCellEditorState(
-      allOptions: _makeChecklistSelectOptions(data, ''),
+      allOptions: _makeChecklistSelectOptions(data),
       createOption: none(),
       percent: data?.percentage ?? 0,
-      predicate: '',
     );
   }
 }
 
 List<ChecklistSelectOption> _makeChecklistSelectOptions(
   ChecklistCellDataPB? data,
-  String predicate,
 ) {
   if (data == null) {
     return [];
@@ -164,9 +164,6 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
 
   final List<ChecklistSelectOption> options = [];
   final List<SelectOptionPB> allOptions = List.from(data.options);
-  if (predicate.isNotEmpty) {
-    allOptions.retainWhere((element) => element.name.contains(predicate));
-  }
   final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList();
 
   for (final option in allOptions) {
@@ -177,10 +174,3 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
 
   return options;
 }
-
-class ChecklistSelectOption {
-  final bool isSelected;
-  final SelectOptionPB data;
-
-  ChecklistSelectOption(this.isSelected, this.data);
-}

+ 25 - 110
frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart

@@ -1,129 +1,44 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
-import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart';
-import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
-import 'package:easy_localization/easy_localization.dart';
 import 'package:flowy_infra/theme_extension.dart';
 import 'package:flowy_infra_ui/flowy_infra_ui.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
 import 'package:percent_indicator/percent_indicator.dart';
 
-class ChecklistProgressBar extends StatelessWidget {
+class ChecklistProgressBar extends StatefulWidget {
   final double percent;
   const ChecklistProgressBar({required this.percent, Key? key})
       : super(key: key);
 
   @override
-  Widget build(BuildContext context) {
-    return LinearPercentIndicator(
-      lineHeight: 10.0,
-      percent: percent,
-      padding: EdgeInsets.zero,
-      progressColor: percent < 1.0
-          ? SelectOptionColorPB.Purple.toColor(context)
-          : SelectOptionColorPB.Green.toColor(context),
-      backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
-      barRadius: const Radius.circular(5),
-    );
-  }
+  State<ChecklistProgressBar> createState() => _ChecklistProgressBarState();
 }
 
-class SliverChecklistProgressBar extends StatelessWidget {
-  const SliverChecklistProgressBar({Key? key}) : super(key: key);
-
+class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
   @override
   Widget build(BuildContext context) {
-    return SliverPersistentHeader(
-      pinned: true,
-      delegate: _SliverChecklistProgressBarDelegate(),
-    );
-  }
-}
-
-class _SliverChecklistProgressBarDelegate
-    extends SliverPersistentHeaderDelegate {
-  _SliverChecklistProgressBarDelegate();
-
-  double fixHeight = 60;
-
-  @override
-  Widget build(
-    BuildContext context,
-    double shrinkOffset,
-    bool overlapsContent,
-  ) {
-    return const _AutoFocusTextField();
-  }
-
-  @override
-  double get maxExtent => fixHeight;
-
-  @override
-  double get minExtent => fixHeight;
-
-  @override
-  bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
-    return true;
-  }
-}
-
-class _AutoFocusTextField extends StatefulWidget {
-  const _AutoFocusTextField();
-
-  @override
-  State<_AutoFocusTextField> createState() => _AutoFocusTextFieldState();
-}
-
-class _AutoFocusTextFieldState extends State<_AutoFocusTextField> {
-  final _focusNode = FocusNode();
-
-  @override
-  Widget build(BuildContext context) {
-    return BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
-      builder: (context, state) {
-        return BlocListener<ChecklistCellEditorBloc, ChecklistCellEditorState>(
-          listenWhen: (previous, current) =>
-              previous.createOption != current.createOption,
-          listener: (context, state) {
-            if (_focusNode.canRequestFocus) {
-              _focusNode.requestFocus();
-            }
-          },
-          child: Container(
-            color: Theme.of(context).cardColor,
-            child: Padding(
-              padding: GridSize.typeOptionContentInsets,
-              child: Column(
-                children: [
-                  FlowyTextField(
-                    autoFocus: true,
-                    focusNode: _focusNode,
-                    autoClearWhenDone: true,
-                    submitOnLeave: 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));
-                    },
-                  ),
-                  Padding(
-                    padding: const EdgeInsets.only(top: 6.0),
-                    child: ChecklistProgressBar(percent: state.percent),
-                  ),
-                ],
-              ),
+    return Row(
+      children: [
+        Expanded(
+          child: LinearPercentIndicator(
+            lineHeight: 4.0,
+            percent: widget.percent,
+            padding: EdgeInsets.zero,
+            progressColor: Theme.of(context).colorScheme.primary,
+            backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
+            barRadius: const Radius.circular(5),
+          ),
+        ),
+        SizedBox(
+          width: 36,
+          child: Align(
+            alignment: AlignmentDirectional.centerEnd,
+            child: FlowyText.regular(
+              "${(widget.percent * 100).round()}%",
+              fontSize: 11,
+              color: Theme.of(context).hintColor,
             ),
           ),
-        );
-      },
+        ),
+      ],
     );
   }
 }

+ 4 - 36
frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart

@@ -2,7 +2,6 @@ import 'dart:collection';
 
 import 'package:appflowy_popover/appflowy_popover.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 
 typedef EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;
 
@@ -67,50 +66,19 @@ class OverlayEntryContext {
   );
 }
 
-class PopoverMask extends StatefulWidget {
+class PopoverMask extends StatelessWidget {
   final void Function() onTap;
-  final void Function()? onExit;
   final Decoration? decoration;
 
-  const PopoverMask(
-      {Key? key, required this.onTap, this.onExit, this.decoration})
+  const PopoverMask({Key? key, required this.onTap, this.decoration})
       : super(key: key);
 
-  @override
-  State<StatefulWidget> createState() => _PopoverMaskState();
-}
-
-class _PopoverMaskState extends State<PopoverMask> {
-  @override
-  void initState() {
-    HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
-    super.initState();
-  }
-
-  bool _handleGlobalKeyEvent(KeyEvent event) {
-    if (event.logicalKey == LogicalKeyboardKey.escape &&
-        event is KeyDownEvent) {
-      if (widget.onExit != null) {
-        widget.onExit!();
-      }
-      return true;
-    } else {
-      return false;
-    }
-  }
-
-  @override
-  void deactivate() {
-    HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
-    super.deactivate();
-  }
-
   @override
   Widget build(BuildContext context) {
     return GestureDetector(
-      onTap: widget.onTap,
+      onTap: onTap,
       child: Container(
-        decoration: widget.decoration,
+        decoration: decoration,
       ),
     );
   }

+ 24 - 11
frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart

@@ -1,5 +1,6 @@
 import 'package:appflowy_popover/src/layout.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'mask.dart';
 import 'mutex.dart';
 
@@ -130,7 +131,6 @@ class PopoverState extends State<Popover> {
               }
               _removeRootOverlay();
             },
-            onExit: () => _removeRootOverlay(),
           ),
         );
       }
@@ -147,7 +147,17 @@ class PopoverState extends State<Popover> {
         ),
       );
 
-      return Stack(children: children);
+      return FocusScope(
+        onKey: (node, event) {
+          if (event is RawKeyDownEvent &&
+              event.logicalKey == LogicalKeyboardKey.escape) {
+            _removeRootOverlay();
+            return KeyEventResult.handled;
+          }
+          return KeyEventResult.ignored;
+        },
+        child: Stack(children: children),
+      );
     });
     _rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
   }
@@ -192,9 +202,9 @@ class PopoverState extends State<Popover> {
           showOverlay();
         }
       },
-      child: Listener(
+      child: GestureDetector(
         child: widget.child,
-        onPointerDown: (PointerDownEvent event) {
+        onTap: () {
           if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
             showOverlay();
           }
@@ -240,14 +250,17 @@ class PopoverContainer extends StatefulWidget {
 class PopoverContainerState extends State<PopoverContainer> {
   @override
   Widget build(BuildContext context) {
-    return CustomSingleChildLayout(
-      delegate: PopoverLayoutDelegate(
-        direction: widget.direction,
-        link: widget.popoverLink,
-        offset: widget.offset,
-        windowPadding: widget.windowPadding,
+    return Focus(
+      autofocus: true,
+      child: CustomSingleChildLayout(
+        delegate: PopoverLayoutDelegate(
+          direction: widget.direction,
+          link: widget.popoverLink,
+          offset: widget.offset,
+          windowPadding: widget.windowPadding,
+        ),
+        child: widget.popupBuilder(context),
       ),
-      child: widget.popupBuilder(context),
     );
   }
 

+ 1 - 1
frontend/resources/translations/ar-SA.json

@@ -386,7 +386,7 @@
             "searchOption": "ابحث عن خيار"
         },
         "checklist": {
-            "panelTitle": "أضف عنصرًا"
+            "addNew": "أضف عنصرًا"
         },
         "menuName": "شبكة",
         "referencedGridPrefix": "نظرا ل",

+ 1 - 1
frontend/resources/translations/ca-ES.json

@@ -402,7 +402,7 @@
       "searchOption": "Cerca una opció"
     },
     "checklist": {
-      "panelTitle": "Afegeix un element"
+      "addNew": "Afegeix un element"
     },
     "menuName": "Quadrícula",
     "referencedGridPrefix": "Vista de"

+ 1 - 1
frontend/resources/translations/de-DE.json

@@ -411,7 +411,7 @@
       "searchOption": "Suchen Sie nach einer Option"
     },
     "checklist": {
-      "panelTitle": "Fügen Sie einen Artikel hinzu"
+      "addNew": "Fügen Sie einen Artikel hinzu"
     },
     "menuName": "Netz",
     "referencedGridPrefix": "Sicht von"

+ 3 - 1
frontend/resources/translations/en.json

@@ -477,7 +477,9 @@
       "orSelectOne": "Or select an option"
     },
     "checklist": {
-      "panelTitle": "Add an item"
+      "taskHint": "Task description",
+      "addNew": "Add a new task",
+      "submitNewTask": "Create"
     },
     "menuName": "Grid",
     "referencedGridPrefix": "View of"

+ 1 - 1
frontend/resources/translations/es-VE.json

@@ -377,7 +377,7 @@
       "addSort": "Agregar clasificación"
     },
     "checklist": {
-      "panelTitle": "Agregar un elemento"
+      "addNew": "Agregar un elemento"
     },
     "referencedGridPrefix": "Vista de"
   },

+ 1 - 1
frontend/resources/translations/eu-ES.json

@@ -387,7 +387,7 @@
       "searchOption": "Aukera bat bilatu"
     },
     "checklist": {
-      "panelTitle": "Gehitu elementu bat"
+      "addNew": "Gehitu elementu bat"
     },
     "menuName": "Sareta",
     "deleteView": "Ziur ikuspegi hau ezabatu nahi duzula?",

+ 1 - 1
frontend/resources/translations/fa.json

@@ -439,7 +439,7 @@
       "searchOption": "جستجوی یک گزینه"
     },
     "checklist": {
-      "panelTitle": "یک مورد اضافه کنید"
+      "addNew": "یک مورد اضافه کنید"
     },
     "menuName": "شبکه‌ای",
     "referencedGridPrefix": "نمایش"

+ 1 - 1
frontend/resources/translations/fr-CA.json

@@ -402,7 +402,7 @@
       "searchOption": "Rechercher une option"
     },
     "checklist": {
-      "panelTitle": "Ajouter un article"
+      "addNew": "Ajouter un article"
     },
     "menuName": "Grille",
     "referencedGridPrefix": "Vue"

+ 1 - 1
frontend/resources/translations/fr-FR.json

@@ -381,7 +381,7 @@
       "addSort": "Ajouter un tri"
     },
     "checklist": {
-      "panelTitle": "Ajouter un élément"
+      "addNew": "Ajouter un élément"
     },
     "referencedGridPrefix": "Vue"
   },

+ 1 - 1
frontend/resources/translations/hu-HU.json

@@ -402,7 +402,7 @@
       "searchOption": "Keressen egy lehetőséget"
     },
     "checklist": {
-      "panelTitle": "Adjon hozzá egy elemet"
+      "addNew": "Adjon hozzá egy elemet"
     },
     "menuName": "Rács",
     "referencedGridPrefix": "Nézet"

+ 1 - 1
frontend/resources/translations/id-ID.json

@@ -377,7 +377,7 @@
       "addSort": "Tambahkan semacam"
     },
     "checklist": {
-      "panelTitle": "Tambahkan item"
+      "addNew": "Tambahkan item"
     },
     "referencedGridPrefix": "Pemandangan dari"
   },

+ 1 - 1
frontend/resources/translations/it-IT.json

@@ -370,7 +370,7 @@
       "searchOption": "Cerca un'opzione"
     },
     "checklist": {
-      "panelTitle": "Aggiungi un elemento"
+      "addNew": "Aggiungi un elemento"
     },
     "referencedGridPrefix": "Vista di"
   },

+ 1 - 1
frontend/resources/translations/ja-JP.json

@@ -370,7 +370,7 @@
       "addSort": "並べ替えの追加"
     },
     "checklist": {
-      "panelTitle": "アイテムを追加する"
+      "addNew": "アイテムを追加する"
     },
     "menuName": "グリッド",
     "referencedGridPrefix": "のビュー"

+ 1 - 1
frontend/resources/translations/ko-KR.json

@@ -383,7 +383,7 @@
       "addSort": "정렬 추가"
     },
     "checklist": {
-      "panelTitle": "항목 추가"
+      "addNew": "항목 추가"
     },
     "referencedGridPrefix": "관점"
   },

+ 1 - 1
frontend/resources/translations/pl-PL.json

@@ -402,7 +402,7 @@
       "searchOption": "Wyszukaj opcję"
     },
     "checklist": {
-      "panelTitle": "Dodaj element"
+      "addNew": "Dodaj element"
     },
     "menuName": "Siatka",
     "referencedGridPrefix": "Widok"

+ 1 - 1
frontend/resources/translations/pt-BR.json

@@ -438,7 +438,7 @@
       "panelTitle": "Selecione uma opção ou crie uma"
     },
     "checklist": {
-      "panelTitle": "Adicionar um item"
+      "addNew": "Adicionar um item"
     },
     "menuName": "Grade",
     "deleteView": "Tem certeza de que deseja excluir esta visualização?",

+ 1 - 1
frontend/resources/translations/pt-PT.json

@@ -455,7 +455,7 @@
       "searchOption": "Pesquise uma opção"
     },
     "checklist": {
-      "panelTitle": "Adicionar um item"
+      "addNew": "Adicionar um item"
     },
     "menuName": "Grade",
     "referencedGridPrefix": "Vista de"

+ 1 - 1
frontend/resources/translations/ru-RU.json

@@ -393,7 +393,7 @@
       "searchOption": "Поиск"
     },
     "checklist": {
-      "panelTitle": "Добавить элемент"
+      "addNew": "Добавить элемент"
     },
     "menuName": "Сетка",
     "referencedGridPrefix": "Просмотр",

+ 1 - 1
frontend/resources/translations/sv.json

@@ -381,7 +381,7 @@
       "addSort": "Lägg till sortering"
     },
     "checklist": {
-      "panelTitle": "Lägg till ett objekt"
+      "addNew": "Lägg till ett objekt"
     },
     "referencedGridPrefix": "Utsikt över"
   },

+ 1 - 1
frontend/resources/translations/tr-TR.json

@@ -402,7 +402,7 @@
       "searchOption": "Bir seçenek arayın"
     },
     "checklist": {
-      "panelTitle": "öğe ekle"
+      "addNew": "öğe ekle"
     },
     "menuName": "Kafes",
     "referencedGridPrefix": "görünümü"

+ 1 - 1
frontend/resources/translations/zh-CN.json

@@ -397,7 +397,7 @@
       "searchOption": "搜索标签"
     },
     "checklist": {
-      "panelTitle": "添加项"
+      "addNew": "添加项"
     },
     "menuName": "网格",
     "referencedGridPrefix": "视图"

+ 1 - 1
frontend/resources/translations/zh-TW.json

@@ -386,7 +386,7 @@
       "searchOption": "搜尋選項"
     },
     "checklist": {
-      "panelTitle": "新增物件"
+      "addNew": "新增物件"
     },
     "menuName": "網格",
     "deleteView": "您確定要刪除該視圖嗎?",

+ 1 - 1
frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs

@@ -42,7 +42,7 @@ impl ChecklistCellData {
     if total_options == 0 {
       return 0.0;
     }
-    ((selected_options as f64) / (total_options as f64) * 10.0).trunc() / 10.0
+    ((selected_options as f64) / (total_options as f64) * 100.0).round() / 100.0
   }
 
   pub fn from_options(options: Vec<String>) -> Self {

+ 1 - 1
frontend/rust-lib/flowy-test/tests/database/local_test/test.rs

@@ -653,7 +653,7 @@ async fn update_checklist_cell_test() {
 
   assert_eq!(cell.options.len(), 3);
   assert_eq!(cell.selected_options.len(), 2);
-  assert_eq!(cell.percentage, 0.6);
+  assert_eq!(cell.percentage, 0.67);
 }
 
 // The number of groups should be 0 if there is no group by field in grid