From 0c6a1d4ae764c27f55ce0f5dd1638afcdffe77db Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 13 Sep 2023 20:44:04 +0800 Subject: [PATCH] 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 --- .../integration_test/database_cell_test.dart | 112 +++++ .../integration_test/database_share_test.dart | 20 +- .../util/database_test_op.dart | 151 +++++- .../cell/checklist_cell_service.dart | 10 +- .../card/cells/checklist_card_cell.dart | 14 +- .../cells/checklist_cell/checklist_cell.dart | 10 +- .../checklist_cell/checklist_cell_bloc.dart | 28 +- .../checklist_cell/checklist_cell_editor.dart | 446 +++++++++++++----- .../checklist_cell_editor_bloc.dart | 90 ++-- .../checklist_progress_bar.dart | 135 +----- .../appflowy_popover/lib/src/mask.dart | 40 +- .../appflowy_popover/lib/src/popover.dart | 35 +- frontend/resources/translations/ar-SA.json | 2 +- frontend/resources/translations/ca-ES.json | 2 +- frontend/resources/translations/de-DE.json | 2 +- frontend/resources/translations/en.json | 4 +- frontend/resources/translations/es-VE.json | 2 +- frontend/resources/translations/eu-ES.json | 2 +- frontend/resources/translations/fa.json | 2 +- frontend/resources/translations/fr-CA.json | 2 +- frontend/resources/translations/fr-FR.json | 2 +- frontend/resources/translations/hu-HU.json | 2 +- frontend/resources/translations/id-ID.json | 2 +- frontend/resources/translations/it-IT.json | 2 +- frontend/resources/translations/ja-JP.json | 2 +- frontend/resources/translations/ko-KR.json | 2 +- frontend/resources/translations/pl-PL.json | 2 +- frontend/resources/translations/pt-BR.json | 2 +- frontend/resources/translations/pt-PT.json | 2 +- frontend/resources/translations/ru-RU.json | 2 +- frontend/resources/translations/sv.json | 2 +- frontend/resources/translations/tr-TR.json | 2 +- frontend/resources/translations/zh-CN.json | 2 +- frontend/resources/translations/zh-TW.json | 2 +- .../checklist_entities.rs | 2 +- .../tests/database/local_test/test.rs | 2 +- 36 files changed, 747 insertions(+), 394 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/database_cell_test.dart index e9862db208..63b30f5c18 100644 --- a/frontend/appflowy_flutter/integration_test/database_cell_test.dart +++ b/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); + }); } diff --git a/frontend/appflowy_flutter/integration_test/database_share_test.dart b/frontend/appflowy_flutter/integration_test/database_share_test.dart index d81311028c..bd42847f82 100644 --- a/frontend/appflowy_flutter/integration_test/database_share_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_share_test.dart @@ -122,17 +122,17 @@ void main() { } // check the checklist cell - final List checklistCells = [ - 0.6, - 0.3, + final List 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( diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index 81ca2e4953..685af4184b 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/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 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 assertDateCellInGrid({ @@ -450,6 +462,119 @@ extension AppFlowyDatabaseTest on WidgetTester { expect(cell, matcher); } + Future 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 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(task); + assert( + widget.option.data.name == name && widget.option.isSelected == isChecked, + ); + } + + Future 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 tapChecklistNewTaskButton() async { + await tapButton(find.byType(ChecklistNewTaskButton)); + } + + Future 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 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 openFirstRowDetailPage() async { await hoverOnFirstRowOfGrid(); diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart index 9b38fc1b0d..dfb9670669 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/application/cell/checklist_cell_service.dart +++ b/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> update({ + Future> 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(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart index 132a59744d..4d1c538b05 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/card/cells/checklist_card_cell.dart @@ -32,10 +32,16 @@ class _ChecklistCardCellState extends State { return BlocProvider.value( value: _cellBloc, child: BlocBuilder( - 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), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart index c1b27a5d3d..f0d9f2280b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell.dart @@ -40,7 +40,7 @@ class GridChecklistCellState extends GridCellState { 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 { child: Padding( padding: GridSize.cellContentInsets, child: BlocBuilder( - builder: (context, state) => - ChecklistProgressBar(percent: state.percent), + builder: (context, state) { + if (state.allOptions.isEmpty) { + return const SizedBox.shrink(); + } + return ChecklistProgressBar(percent: state.percent); + }, ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart index d8445d7f4b..7d5b9a865a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_bloc.dart +++ b/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; } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart index 65d59fc5ef..d23a0325f6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart +++ b/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 createState() => @@ -23,167 +25,367 @@ class GridChecklistCellEditor extends StatefulWidget { } class _GridChecklistCellEditorState extends State { - 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( + value: _bloc, + child: BlocConsumer( + listener: (context, state) { + if (state.allOptions.isEmpty) { + setState(() => _isAddingNewTask = true); + } + }, builder: (context, state) { - final List 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(); + } } -class _ChecklistOptionCell extends StatefulWidget { +/// 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 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 createState() => _ChecklistItemListState(); +} + +class _ChecklistItemListState extends State { + @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, + ), + ); + } +} + +/// 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 createState() => _ChecklistItemState(); } -class _ChecklistOptionCellState extends State<_ChecklistOptionCell> { - late PopoverController _popoverController; +class _ChecklistItemState extends State { + 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, - ), - leftIcon: icon, - onTap: () => context - .read() - .add(ChecklistCellEditorEvent.selectOption(widget.option)), + 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().add( + ChecklistCellEditorEvent.selectTask(widget.option.data), + ), ), - ), - _disclosureButton(), - ], + 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().add( + ChecklistCellEditorEvent.updateTaskName( + widget.option.data, + taskDescription, + ), + ); + widget.onSubmitted?.call(); + }, + ), + ), + if (_isHovered) + FlowyIconButton( + width: 32, + icon: const FlowySvg(FlowySvgs.delete_s), + hoverColor: Colors.transparent, + iconColorOnHover: Theme.of(context).colorScheme.error, + onPressed: () => context.read().add( + ChecklistCellEditorEvent.deleteTask(widget.option.data), + ), + ), + ], + ), ), ), ); } +} - 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, +/// 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 createState() => _NewTaskItemState(); +} + +class _NewTaskItemState extends State { + late final TextEditingController _textEditingController; + + @override + void initState() { + super.initState(); + _textEditingController = TextEditingController(); + if (widget.focusNode.canRequestFocus) { + widget.focusNode.requestFocus(); + } + } + + @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().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().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().add( - ChecklistCellEditorEvent.deleteOption(widget.option.data), - ); +@visibleForTesting +class ChecklistNewTaskButton extends StatelessWidget { + final VoidCallback onTap; + const ChecklistNewTaskButton({super.key, required this.onTap}); - _popoverController.close(); - }, - onUpdated: (updatedOption) { - context.read().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, - ), - ); - }, + @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, + ), + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart index fbcd994e7f..55185a521c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart +++ b/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 { 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 _createOption(String name) async { final result = await _checklistCellService.create(name: name); result.fold((l) => {}, (err) => Log.error(err)); } - void _deleteOption(List options) async { + Future _deleteOption(List 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 allOptions, required Option 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 _makeChecklistSelectOptions( ChecklistCellDataPB? data, - String predicate, ) { if (data == null) { return []; @@ -164,9 +164,6 @@ List _makeChecklistSelectOptions( final List options = []; final List 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 _makeChecklistSelectOptions( return options; } - -class ChecklistSelectOption { - final bool isSelected; - final SelectOptionPB data; - - ChecklistSelectOption(this.isSelected, this.data); -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart index 04fe36dcd0..289d84b25b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart +++ b/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 createState() => _ChecklistProgressBarState(); } -class SliverChecklistProgressBar extends StatelessWidget { - const SliverChecklistProgressBar({Key? key}) : super(key: key); - +class _ChecklistProgressBarState extends State { @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( - builder: (context, state) { - return BlocListener( - 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() - .add(ChecklistCellEditorEvent.filterOption(text)); - }, - onSubmitted: (text) { - context - .read() - .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, ), ), - ); - }, + ), + ], ); } } diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart index d63e486a1e..30c744902e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart +++ b/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; @@ -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 createState() => _PopoverMaskState(); -} - -class _PopoverMaskState extends State { - @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, ), ); } diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index fdb3628011..105fc77293 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/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 { } _removeRootOverlay(); }, - onExit: () => _removeRootOverlay(), ), ); } @@ -147,7 +147,17 @@ class PopoverState extends State { ), ); - 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 { 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 { @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), ); } diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index d01c54d637..bdc8bbed84 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -386,7 +386,7 @@ "searchOption": "ابحث عن خيار" }, "checklist": { - "panelTitle": "أضف عنصرًا" + "addNew": "أضف عنصرًا" }, "menuName": "شبكة", "referencedGridPrefix": "نظرا ل", diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 04dd592c9d..295fe727c6 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/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" diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 3a748c22d3..011ce8b13d 100644 --- a/frontend/resources/translations/de-DE.json +++ b/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" diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 480c8bb3b7..a74803a7d8 100644 --- a/frontend/resources/translations/en.json +++ b/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" diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index b11783a7ce..68b81f2c80 100644 --- a/frontend/resources/translations/es-VE.json +++ b/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" }, diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index 7506bde115..fc7e809e08 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/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?", diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 4836986395..424ffc5846 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -439,7 +439,7 @@ "searchOption": "جستجوی یک گزینه" }, "checklist": { - "panelTitle": "یک مورد اضافه کنید" + "addNew": "یک مورد اضافه کنید" }, "menuName": "شبکه‌ای", "referencedGridPrefix": "نمایش" diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index b817c1c6d4..ace3de14a4 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/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" diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 65469a5961..12f507cd89 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/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" }, diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index e7ce79497f..1d7a2f5876 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/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" diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index b49ce40b21..700c85da6b 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -377,7 +377,7 @@ "addSort": "Tambahkan semacam" }, "checklist": { - "panelTitle": "Tambahkan item" + "addNew": "Tambahkan item" }, "referencedGridPrefix": "Pemandangan dari" }, diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index bbba1c702a..507c07d5cd 100644 --- a/frontend/resources/translations/it-IT.json +++ b/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" }, diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 6879d400ac..7d92b02cb4 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -370,7 +370,7 @@ "addSort": "並べ替えの追加" }, "checklist": { - "panelTitle": "アイテムを追加する" + "addNew": "アイテムを追加する" }, "menuName": "グリッド", "referencedGridPrefix": "のビュー" diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 91e41afba3..44eee7a7b8 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -383,7 +383,7 @@ "addSort": "정렬 추가" }, "checklist": { - "panelTitle": "항목 추가" + "addNew": "항목 추가" }, "referencedGridPrefix": "관점" }, diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index a97f6d2b74..8b6cbaf18a 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -402,7 +402,7 @@ "searchOption": "Wyszukaj opcję" }, "checklist": { - "panelTitle": "Dodaj element" + "addNew": "Dodaj element" }, "menuName": "Siatka", "referencedGridPrefix": "Widok" diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index fb6e11dc85..c3a38e37f1 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/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?", diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index e8f5552759..26f954fbba 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/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" diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 9345fc3e78..af67e82109 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -393,7 +393,7 @@ "searchOption": "Поиск" }, "checklist": { - "panelTitle": "Добавить элемент" + "addNew": "Добавить элемент" }, "menuName": "Сетка", "referencedGridPrefix": "Просмотр", diff --git a/frontend/resources/translations/sv.json b/frontend/resources/translations/sv.json index 45e3efceec..9844ae89ec 100644 --- a/frontend/resources/translations/sv.json +++ b/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" }, diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index 58f7a60aee..fe7ba37440 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/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ü" diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index d7e64bddc2..0b374fb2df 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -397,7 +397,7 @@ "searchOption": "搜索标签" }, "checklist": { - "panelTitle": "添加项" + "addNew": "添加项" }, "menuName": "网格", "referencedGridPrefix": "视图" diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index 4912a13c50..b637da3a4c 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -386,7 +386,7 @@ "searchOption": "搜尋選項" }, "checklist": { - "panelTitle": "新增物件" + "addNew": "新增物件" }, "menuName": "網格", "deleteView": "您確定要刪除該視圖嗎?", diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs index 95b22a0c75..d0ff75ac33 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_entities.rs +++ b/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) -> Self { diff --git a/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs b/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs index 8d953198b5..e24c007472 100644 --- a/frontend/rust-lib/flowy-test/tests/database/local_test/test.rs +++ b/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