diff --git a/frontend/appflowy_flutter/integration_test/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/database_cell_test.dart index cf40bb1575..0131c5834e 100644 --- a/frontend/appflowy_flutter/integration_test/database_cell_test.dart +++ b/frontend/appflowy_flutter/integration_test/database_cell_test.dart @@ -456,9 +456,6 @@ void main() { // 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); @@ -481,7 +478,6 @@ void main() { // dismiss new task editor await tester.dismissCellEditor(); - tester.assertNewCheckListTaskEditorVisible(visible: false); // dismiss checklist cell editor await tester.dismissCellEditor(); @@ -489,11 +485,8 @@ void main() { // 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 + // start editing the first checklist cell again 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); @@ -540,9 +533,6 @@ void main() { // 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(); 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 4bed821b42..8f863648e5 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -46,7 +46,6 @@ import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_ import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/select_option_editor.dart'; import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/text_field.dart'; -import 'package:appflowy/plugins/database_view/widgets/row/cells/timestamp_cell/timestamp_cell.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_action.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database_view/widgets/row/row_detail.dart'; @@ -494,15 +493,6 @@ extension AppFlowyDatabaseTest on WidgetTester { } } - 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, @@ -515,10 +505,10 @@ extension AppFlowyDatabaseTest on WidgetTester { ); await enterText(textField, name); - await pumpAndSettle(const Duration(milliseconds: 300)); + await pumpAndSettle(); if (enter) { await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(const Duration(milliseconds: 300)); + await pumpAndSettle(); } else { await tapButton( find.descendant( @@ -555,11 +545,7 @@ extension AppFlowyDatabaseTest on WidgetTester { await enterText(textField, name); await testTextInput.receiveAction(TextInputAction.done); - await pumpAndSettle(const Duration(milliseconds: 300)); - } - - Future tapChecklistNewTaskButton() async { - await tapButton(find.byType(ChecklistNewTaskButton)); + await pumpAndSettle(); } Future checkChecklistTask({required int index}) async { diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart index e17a5f104e..d45c699231 100755 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cell_builder.dart @@ -67,6 +67,7 @@ class GridCellBuilder { case FieldType.Checklist: return GridChecklistCell( cellControllerBuilder: cellControllerBuilder, + style: style, key: key, ); case FieldType.Number: diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart index 0a76539284..372b71fcce 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/cells.dart @@ -4,4 +4,5 @@ export 'date_cell/date_cell.dart'; export 'number_cell/number_cell.dart'; export 'select_option_cell/select_option_cell.dart'; export 'text_cell/text_cell.dart'; +export 'timestamp_cell/timestamp_cell.dart'; export 'url_cell/url_cell.dart'; 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 f0d9f2280b..d40debbb83 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 @@ -10,10 +10,26 @@ import 'checklist_cell_bloc.dart'; import 'checklist_cell_editor.dart'; import 'checklist_progress_bar.dart'; +class ChecklistCellStyle extends GridCellStyle { + String placeholder; + EdgeInsets? cellPadding; + + ChecklistCellStyle({ + required this.placeholder, + this.cellPadding, + }); +} + class GridChecklistCell extends GridCellWidget { final CellControllerBuilder cellControllerBuilder; - GridChecklistCell({required this.cellControllerBuilder, Key? key}) - : super(key: key); + late final ChecklistCellStyle? cellStyle; + GridChecklistCell({ + required this.cellControllerBuilder, + GridCellStyle? style, + super.key, + }) { + cellStyle = style as ChecklistCellStyle?; + } @override GridCellState createState() => GridChecklistCellState(); @@ -53,15 +69,22 @@ class GridChecklistCellState extends GridCellState { ); }, onClose: () => widget.onCellFocus.value = false, - child: Padding( - padding: GridSize.cellContentInsets, - child: BlocBuilder( - builder: (context, state) { - if (state.allOptions.isEmpty) { - return const SizedBox.shrink(); - } - return ChecklistProgressBar(percent: state.percent); - }, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: + widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets, + child: BlocBuilder( + builder: (context, state) { + if (state.allOptions.isEmpty) { + return FlowyText.medium( + widget.cellStyle?.placeholder ?? "", + color: Theme.of(context).hintColor, + ); + } + return ChecklistProgressBar(percent: state.percent); + }, + ), ), ), ), 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 d23a0325f6..f62f129d72 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,3 +1,5 @@ +import 'dart:async'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; @@ -30,13 +32,19 @@ class _GridChecklistCellEditorState extends State { /// Focus node for the new task text field late final FocusNode newTaskFocusNode; - /// A flag that determines whether the new task text field is visible - bool _isAddingNewTask = false; - @override void initState() { super.initState(); - newTaskFocusNode = FocusNode(); + newTaskFocusNode = FocusNode( + onKey: (node, event) { + if (event is RawKeyDownEvent && + event.logicalKey == LogicalKeyboardKey.escape) { + node.unfocus(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + ); _bloc = ChecklistCellEditorBloc(cellController: widget.cellController) ..add(const ChecklistCellEditorEvent.initial()); } @@ -48,59 +56,35 @@ class _GridChecklistCellEditorState extends State { child: BlocConsumer( listener: (context, state) { if (state.allOptions.isEmpty) { - setState(() => _isAddingNewTask = true); + newTaskFocusNode.requestFocus(); } }, builder: (context, state) { - return Focus( - onKey: (node, event) { - // don't hide new task text field if there are no tasks at all - if (state.allOptions.isNotEmpty && - event is RawKeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - setState(() { - _isAddingNewTask = false; - }); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - child: CustomScrollView( - shrinkWrap: true, - physics: StyledScrollPhysics(), - slivers: [ - SliverToBoxAdapter( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: state.allOptions.isEmpty - ? const SizedBox.shrink() - : Padding( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), - child: ChecklistProgressBar( - percent: state.percent, - ), - ), - ), - ), - ChecklistItemList( - options: state.allOptions, - newTaskFocusNode: newTaskFocusNode, - isAddingNewTask: _isAddingNewTask, - onUpdateTask: () => setState(() { - _isAddingNewTask = true; - newTaskFocusNode.requestFocus(); - }), - ), - const SliverToBoxAdapter( - child: TypeOptionSeparator(spacing: 0.0), - ), - SliverToBoxAdapter( - child: ChecklistNewTaskButton( - onTap: () => setState(() => _isAddingNewTask = true), - ), - ), - ], - ), + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: state.allOptions.isEmpty + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), + child: ChecklistProgressBar( + percent: state.percent, + ), + ), + ), + ChecklistItemList( + options: state.allOptions, + onUpdateTask: () => newTaskFocusNode.requestFocus(), + ), + if (state.allOptions.isNotEmpty) + const TypeOptionSeparator(spacing: 0.0), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: NewTaskItem(focusNode: newTaskFocusNode), + ), + ], ); }, ), @@ -118,16 +102,12 @@ class _GridChecklistCellEditorState extends State { /// 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 @@ -137,32 +117,28 @@ class ChecklistItemList extends StatefulWidget { 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( + if (widget.options.isEmpty) { + return const SizedBox.shrink(); + } + + final itemList = widget.options + .mapIndexed( + (index, option) => ChecklistItem( option: option, onSubmitted: index == widget.options.length - 1 ? widget.onUpdateTask : null, key: ValueKey(option.data.id), - // only allow calling the callback for the last task in the list ), - ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: widget.isAddingNewTask - ? NewTaskItem(focusNode: widget.newTaskFocusNode) - : const SizedBox.shrink(), - ), - const VSpace(6.0), - ]; - return SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) => itemList[index], - childCount: itemList.length, + ) + .toList(); + + return Flexible( + child: ListView.separated( + itemBuilder: (context, index) => itemList[index], + separatorBuilder: (context, index) => const VSpace(4), + itemCount: itemList.length, + shrinkWrap: true, + padding: const EdgeInsets.symmetric(vertical: 8.0), ), ); } @@ -187,6 +163,7 @@ class _ChecklistItemState extends State { late final TextEditingController _textController; late final FocusNode _focusNode; bool _isHovered = false; + Timer? _debounceOnChanged; @override void initState() { @@ -249,13 +226,9 @@ class _ChecklistItemState extends State { ), hintText: LocaleKeys.grid_checklist_taskHint.tr(), ), - onSubmitted: (taskDescription) { - context.read().add( - ChecklistCellEditorEvent.updateTaskName( - widget.option.data, - taskDescription, - ), - ); + onChanged: _debounceOnChangedText, + onSubmitted: (description) { + _submitUpdateTaskDescription(description); widget.onSubmitted?.call(); }, ), @@ -276,6 +249,22 @@ class _ChecklistItemState extends State { ), ); } + + void _debounceOnChangedText(String text) { + _debounceOnChanged?.cancel(); + _debounceOnChanged = Timer(const Duration(milliseconds: 300), () { + _submitUpdateTaskDescription(text); + }); + } + + void _submitUpdateTaskDescription(String description) { + context.read().add( + ChecklistCellEditorEvent.updateTaskName( + widget.option.data, + description, + ), + ); + } } /// Creates a new task after entering the description and pressing enter. @@ -304,19 +293,12 @@ class _NewTaskItemState extends State { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 8), constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - const FlowyIconButton( - width: 32, - icon: FlowySvg( - FlowySvgs.uncheck_s, - blendMode: BlendMode.dst, - ), - hoverColor: Colors.transparent, - ), + const HSpace(8), Expanded( child: TextField( focusNode: widget.focusNode, @@ -330,7 +312,7 @@ class _NewTaskItemState extends State { vertical: 6.0, horizontal: 2.0, ), - hintText: LocaleKeys.grid_checklist_taskHint.tr(), + hintText: LocaleKeys.grid_checklist_addNew.tr(), ), onSubmitted: (taskDescription) { if (taskDescription.trim().isNotEmpty) { @@ -340,15 +322,21 @@ class _NewTaskItemState extends State { ), ); } + widget.focusNode.requestFocus(); _textEditingController.clear(); }, + onChanged: (value) => setState(() {}), ), ), FlowyTextButton( LocaleKeys.grid_checklist_submitNewTask.tr(), fontSize: 11, - fillColor: Theme.of(context).colorScheme.primary, - hoverColor: Theme.of(context).colorScheme.primaryContainer, + fillColor: _textEditingController.text.isEmpty + ? Theme.of(context).disabledColor + : Theme.of(context).colorScheme.primary, + hoverColor: _textEditingController.text.isEmpty + ? Theme.of(context).disabledColor + : Theme.of(context).colorScheme.primaryContainer, fontColor: Theme.of(context).colorScheme.onPrimary, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), onPressed: () { @@ -359,6 +347,7 @@ class _NewTaskItemState extends State { ), ); } + widget.focusNode.requestFocus(); _textEditingController.clear(); }, ), @@ -367,25 +356,3 @@ class _NewTaskItemState extends State { ); } } - -@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, - ), - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart index ef73c74062..1bdf2c3fc7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/row_property.dart @@ -6,6 +6,7 @@ import 'package:appflowy/plugins/database_view/application/field/type_option/typ import 'package:appflowy/plugins/database_view/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_cell.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; @@ -19,13 +20,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'accessory/cell_accessory.dart'; import 'cell_builder.dart'; -import 'cells/checkbox_cell/checkbox_cell.dart'; -import 'cells/date_cell/date_cell.dart'; -import 'cells/number_cell/number_cell.dart'; -import 'cells/select_option_cell/select_option_cell.dart'; -import 'cells/text_cell/text_cell.dart'; -import 'cells/timestamp_cell/timestamp_cell.dart'; -import 'cells/url_cell/url_cell.dart'; /// Display the row properties in a list. Only use this widget in the /// [RowDetailPage]. @@ -251,8 +245,9 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5), ); case FieldType.Checklist: - return SelectOptionCellStyle( + return ChecklistCellStyle( placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), + cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), ); case FieldType.Number: return GridNumberCellStyle( diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs index a37aa48824..825e39b539 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs @@ -87,7 +87,7 @@ impl CellDataChangeset for ChecklistTypeOption { #[inline] fn update_cell_data_with_changeset( cell_data: &mut ChecklistCellData, - mut changeset: ChecklistCellChangeset, + changeset: ChecklistCellChangeset, ) { // Delete the options cell_data @@ -98,12 +98,6 @@ fn update_cell_data_with_changeset( .retain(|option_id| !changeset.delete_option_ids.contains(option_id)); // Insert new options - changeset.insert_options.retain(|option_name| { - !cell_data - .options - .iter() - .any(|option| option.name == *option_name) - }); changeset .insert_options .into_iter()