From 6c3d7d2079cdc3596551dbd756bfa78118436fc4 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Tue, 24 Oct 2023 10:15:28 +0800 Subject: [PATCH] feat: show checklist items inline in row page (#3737) * feat: show checklist items inline in row page * fix: tauri build --- .../util/database_test_op.dart | 2 +- .../cell/checklist_cell_service.dart | 10 - .../presentation/calendar_event_editor.dart | 5 +- .../card/cells/checklist_card_cell.dart | 3 +- .../widgets/row/accessory/cell_accessory.dart | 10 +- .../cells/checklist_cell/checklist_cell.dart | 203 ++++++++++++++---- .../checklist_cell/checklist_cell_bloc.dart | 116 +++++++--- .../checklist_cell/checklist_cell_editor.dart | 177 ++++++++------- .../checklist_cell_editor_bloc.dart | 176 --------------- .../widgets/row/row_property.dart | 8 +- .../components/database/database_bd_svc.ts | 13 -- frontend/resources/translations/en.json | 6 +- .../rust-lib/event-integration/src/lib.rs | 12 +- .../checklist_entities.rs | 20 -- .../flowy-database2/src/event_handler.rs | 16 +- .../rust-lib/flowy-database2/src/event_map.rs | 6 +- .../src/services/database/database_editor.rs | 11 +- .../tests/database/database_editor.rs | 11 +- .../filter_test/checklist_filter_test.rs | 27 ++- .../tests/database/filter_test/script.rs | 6 +- 20 files changed, 402 insertions(+), 436 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart 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 ce07d0d64b..c1c9734c70 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -529,7 +529,7 @@ extension AppFlowyDatabaseTest on WidgetTester { final widget = this.widget(task); assert( - widget.option.data.name == name && widget.option.isSelected == isChecked, + widget.task.data.name == name && widget.task.isSelected == isChecked, ); } 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 dfb9670669..3e43d67f31 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 @@ -1,5 +1,4 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; @@ -69,13 +68,4 @@ class ChecklistCellBackendService { return DatabaseEventUpdateChecklistCell(payload).send(); } - - Future> getCellData() { - final payload = CellIdPB.create() - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId; - - return DatabaseEventGetChecklistCellData(payload).send(); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart index 1c10a7c00d..8ccef33fdf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/calendar/presentation/calendar_event_editor.dart @@ -187,7 +187,10 @@ class _PropertyCellState extends State { final gesture = GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => cell.requestFocus.notify(), - child: AccessoryHover(child: cell), + child: AccessoryHover( + fieldType: widget.cellContext.fieldType, + child: cell, + ), ); return Container( 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 33d607a24d..2d1deb0055 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 @@ -33,10 +33,9 @@ class _ChecklistCellState extends State { value: _cellBloc, child: BlocBuilder( builder: (context, state) { - if (state.allOptions.isEmpty) { + if (state.tasks.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/accessory/cell_accessory.dart b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart index ef46713b12..ddf2494df1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart +++ b/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/accessory/cell_accessory.dart @@ -1,4 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -92,7 +93,12 @@ class _PrimaryCellAccessoryState extends State class AccessoryHover extends StatefulWidget { final CellAccessory child; - const AccessoryHover({required this.child, super.key}); + final FieldType fieldType; + const AccessoryHover({ + super.key, + required this.child, + required this.fieldType, + }); @override State createState() => _AccessoryHoverState(); @@ -106,7 +112,7 @@ class _AccessoryHoverState extends State { final List children = [ DecoratedBox( decoration: BoxDecoration( - color: _isHover + color: _isHover && widget.fieldType != FieldType.Checklist ? AFThemeExtension.of(context).lightGreyHover : Colors.transparent, borderRadius: Corners.s6Border, 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 8ede7c50f9..dce1ec7382 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 @@ -1,7 +1,12 @@ +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/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -11,24 +16,30 @@ import 'checklist_cell_editor.dart'; import 'checklist_progress_bar.dart'; class ChecklistCellStyle extends GridCellStyle { - String placeholder; - EdgeInsets? cellPadding; + final String placeholder; + final EdgeInsets? cellPadding; + final bool showTasksInline; - ChecklistCellStyle({ - required this.placeholder, + const ChecklistCellStyle({ + this.placeholder = "", this.cellPadding, + this.showTasksInline = false, }); } class GridChecklistCell extends GridCellWidget { final CellControllerBuilder cellControllerBuilder; - late final ChecklistCellStyle? cellStyle; + late final ChecklistCellStyle cellStyle; GridChecklistCell({ required this.cellControllerBuilder, GridCellStyle? style, super.key, }) { - cellStyle = style as ChecklistCellStyle?; + if (style != null) { + cellStyle = (style as ChecklistCellStyle); + } else { + cellStyle = const ChecklistCellStyle(); + } } @override @@ -38,14 +49,15 @@ class GridChecklistCell extends GridCellWidget { class GridChecklistCellState extends GridCellState { late ChecklistCellBloc _cellBloc; late final PopoverController _popover; + bool showIncompleteOnly = false; @override void initState() { _popover = PopoverController(); final cellController = widget.cellControllerBuilder.build() as ChecklistCellController; - _cellBloc = ChecklistCellBloc(cellController: cellController); - _cellBloc.add(const ChecklistCellEvent.initial()); + _cellBloc = ChecklistCellBloc(cellController: cellController) + ..add(const ChecklistCellEvent.initial()); super.initState(); } @@ -53,44 +65,153 @@ class GridChecklistCellState extends GridCellState { Widget build(BuildContext context) { return BlocProvider.value( value: _cellBloc, - child: AppFlowyPopover( - margin: EdgeInsets.zero, - controller: _popover, - constraints: BoxConstraints.loose(const Size(360, 400)), - direction: PopoverDirection.bottomWithLeftAligned, - triggerActions: PopoverTriggerFlags.none, - popupBuilder: (BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - widget.onCellFocus.value = true; - }); - return GridChecklistCellEditor( - cellController: - widget.cellControllerBuilder.build() as ChecklistCellController, + child: BlocBuilder( + builder: (context, state) { + if (widget.cellStyle.showTasksInline) { + final tasks = List.from(state.tasks); + if (showIncompleteOnly) { + tasks.removeWhere((task) => task.isSelected); + } + final children = tasks + .mapIndexed( + (index, task) => ChecklistItem( + task: task, + autofocus: state.newTask && index == tasks.length - 1, + ), + ) + .toList(); + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: + widget.cellStyle.cellPadding ?? GridSize.cellContentInsets, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: ChecklistProgressBar(percent: state.percent), + ), + const HSpace(6.0), + FlowyIconButton( + tooltipText: showIncompleteOnly + ? LocaleKeys.grid_checklist_showComplete.tr() + : LocaleKeys.grid_checklist_hideComplete.tr(), + width: 32, + iconColorOnHover: + Theme.of(context).colorScheme.onSecondary, + icon: FlowySvg( + showIncompleteOnly + ? FlowySvgs.show_m + : FlowySvgs.hide_m, + size: const Size.square(16), + ), + onPressed: () { + setState( + () => showIncompleteOnly = !showIncompleteOnly, + ); + }, + ), + ], + ), + ), + const VSpace(4), + ...children, + const ChecklistItemControl(), + ], + ), + ), + ); + } + + return AppFlowyPopover( + margin: EdgeInsets.zero, + controller: _popover, + constraints: BoxConstraints.loose(const Size(360, 400)), + direction: PopoverDirection.bottomWithLeftAligned, + triggerActions: PopoverTriggerFlags.none, + popupBuilder: (BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onCellFocus.value = true; + }); + return GridChecklistCellEditor( + cellController: widget.cellControllerBuilder.build() + as ChecklistCellController, + ); + }, + onClose: () => widget.onCellFocus.value = false, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: + widget.cellStyle.cellPadding ?? GridSize.cellContentInsets, + child: state.tasks.isEmpty + ? FlowyText.medium( + widget.cellStyle.placeholder, + color: Theme.of(context).hintColor, + ) + : ChecklistProgressBar(percent: state.percent), + ), + ), ); }, - onClose: () => widget.onCellFocus.value = false, - 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); - }, - ), - ), - ), ), ); } @override - void requestBeginFocus() => _popover.show(); + void requestBeginFocus() { + if (!widget.cellStyle.showTasksInline) { + _popover.show(); + } + } +} + +class ChecklistItemControl extends StatelessWidget { + const ChecklistItemControl({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0), + child: SizedBox( + height: 12, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context + .read() + .add(const ChecklistCellEvent.createNewTask("")), + child: Row( + children: [ + const Flexible(child: Center(child: Divider())), + const HSpace(12.0), + FlowyTooltip( + message: LocaleKeys.grid_checklist_addNew.tr(), + child: FilledButton( + style: FilledButton.styleFrom( + minimumSize: const Size.square(12), + maximumSize: const Size.square(12), + padding: EdgeInsets.zero, + ), + onPressed: () => context + .read() + .add(const ChecklistCellEvent.createNewTask("")), + child: FlowySvg( + FlowySvgs.add_s, + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ), + const HSpace(12.0), + const Flexible(child: Center(child: Divider())), + ], + ), + ), + ), + ); + } } 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 705557f52c..b998b50a13 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 @@ -8,13 +8,20 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:async'; part 'checklist_cell_bloc.freezed.dart'; +class ChecklistSelectOption { + final bool isSelected; + final SelectOptionPB data; + + ChecklistSelectOption(this.isSelected, this.data); +} + class ChecklistCellBloc extends Bloc { final ChecklistCellController cellController; - final ChecklistCellBackendService _checklistCellSvc; + final ChecklistCellBackendService _checklistCellService; void Function()? _onCellChangedFn; ChecklistCellBloc({ required this.cellController, - }) : _checklistCellSvc = ChecklistCellBackendService( + }) : _checklistCellService = ChecklistCellBackendService( viewId: cellController.viewId, fieldId: cellController.fieldId, rowId: cellController.rowId, @@ -23,28 +30,43 @@ class ChecklistCellBloc extends Bloc { on( (event, emit) async { await event.when( - initial: () async { + initial: () { _startListening(); - _loadOptions(); }, didReceiveOptions: (data) { if (data == null) { emit( const ChecklistCellState( - allOptions: [], - selectedOptions: [], + tasks: [], percent: 0, + newTask: false, ), ); - } else { - emit( - state.copyWith( - allOptions: data.options, - selectedOptions: data.selectedOptions, - percent: data.percentage, - ), - ); + return; } + + emit( + state.copyWith( + tasks: _makeChecklistSelectOptions(data), + percent: data.percentage, + ), + ); + }, + updateTaskName: (option, name) { + _updateOption(option, name); + }, + selectTask: (option) async { + await _checklistCellService.select(optionId: option.id); + }, + createNewTask: (name) async { + final result = await _checklistCellService.create(name: name); + result.fold( + (l) => emit(state.copyWith(newTask: true)), + (err) => Log.error(err), + ); + }, + deleteTask: (option) async { + await _deleteOption([option]); }, ); }, @@ -63,9 +85,6 @@ class ChecklistCellBloc extends Bloc { void _startListening() { _onCellChangedFn = cellController.startListening( - onCellFieldChanged: () { - _loadOptions(); - }, onCellChanged: (data) { if (!isClosed) { add(ChecklistCellEvent.didReceiveOptions(data)); @@ -74,15 +93,18 @@ class ChecklistCellBloc extends Bloc { ); } - void _loadOptions() { - _checklistCellSvc.getCellData().then((result) { - if (isClosed) return; + void _updateOption(SelectOptionPB option, String name) async { + final result = + await _checklistCellService.updateName(option: option, name: name); - return result.fold( - (data) => add(ChecklistCellEvent.didReceiveOptions(data)), - (err) => Log.error(err), - ); - }); + result.fold((l) => null, (err) => Log.error(err)); + } + + 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)); } } @@ -92,21 +114,53 @@ class ChecklistCellEvent with _$ChecklistCellEvent { const factory ChecklistCellEvent.didReceiveOptions( ChecklistCellDataPB? data, ) = _DidReceiveCellUpdate; + const factory ChecklistCellEvent.updateTaskName( + SelectOptionPB option, + String name, + ) = _UpdateTaskName; + const factory ChecklistCellEvent.selectTask(SelectOptionPB task) = + _SelectTask; + const factory ChecklistCellEvent.createNewTask(String description) = + _CreateNewTask; + const factory ChecklistCellEvent.deleteTask(SelectOptionPB option) = + _DeleteTask; } @freezed class ChecklistCellState with _$ChecklistCellState { const factory ChecklistCellState({ - required List allOptions, - required List selectedOptions, + required List tasks, required double percent, + required bool newTask, }) = _ChecklistCellState; factory ChecklistCellState.initial(ChecklistCellController cellController) { - return const ChecklistCellState( - allOptions: [], - selectedOptions: [], - percent: 0, + final cellData = cellController.getCellData(loadIfNotExist: true); + + return ChecklistCellState( + tasks: _makeChecklistSelectOptions(cellData), + percent: cellData?.percentage ?? 0, + newTask: false, ); } } + +List _makeChecklistSelectOptions( + ChecklistCellDataPB? data, +) { + if (data == null) { + return []; + } + + final List options = []; + final List allOptions = List.from(data.options); + final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList(); + + for (final option in allOptions) { + options.add( + ChecklistSelectOption(selectedOptionIds.contains(option.id), option), + ); + } + + return options; +} 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 f62f129d72..fd388d93bf 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 @@ -14,7 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'checklist_cell_editor_bloc.dart'; +import 'checklist_cell_bloc.dart'; import 'checklist_progress_bar.dart'; class GridChecklistCellEditor extends StatefulWidget { @@ -22,12 +22,11 @@ class GridChecklistCellEditor extends StatefulWidget { const GridChecklistCellEditor({required this.cellController, super.key}); @override - State createState() => - _GridChecklistCellEditorState(); + State createState() => _GridChecklistCellState(); } -class _GridChecklistCellEditorState extends State { - late ChecklistCellEditorBloc _bloc; +class _GridChecklistCellState extends State { + late ChecklistCellBloc _bloc; /// Focus node for the new task text field late final FocusNode newTaskFocusNode; @@ -45,17 +44,17 @@ class _GridChecklistCellEditorState extends State { return KeyEventResult.ignored; }, ); - _bloc = ChecklistCellEditorBloc(cellController: widget.cellController) - ..add(const ChecklistCellEditorEvent.initial()); + _bloc = ChecklistCellBloc(cellController: widget.cellController) + ..add(const ChecklistCellEvent.initial()); } @override Widget build(BuildContext context) { return BlocProvider.value( value: _bloc, - child: BlocConsumer( + child: BlocConsumer( listener: (context, state) { - if (state.allOptions.isEmpty) { + if (state.tasks.isEmpty) { newTaskFocusNode.requestFocus(); } }, @@ -65,7 +64,7 @@ class _GridChecklistCellEditorState extends State { children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 300), - child: state.allOptions.isEmpty + child: state.tasks.isEmpty ? const SizedBox.shrink() : Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), @@ -75,10 +74,10 @@ class _GridChecklistCellEditorState extends State { ), ), ChecklistItemList( - options: state.allOptions, + options: state.tasks, onUpdateTask: () => newTaskFocusNode.requestFocus(), ), - if (state.allOptions.isNotEmpty) + if (state.tasks.isNotEmpty) const TypeOptionSeparator(spacing: 0.0), Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -123,11 +122,15 @@ class _ChecklistItemListState extends State { final itemList = widget.options .mapIndexed( - (index, option) => ChecklistItem( - option: option, - onSubmitted: - index == widget.options.length - 1 ? widget.onUpdateTask : null, - key: ValueKey(option.data.id), + (index, option) => Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: ChecklistItem( + task: option, + onSubmitted: index == widget.options.length - 1 + ? widget.onUpdateTask + : null, + key: ValueKey(option.data.id), + ), ), ) .toList(); @@ -147,12 +150,14 @@ class _ChecklistItemListState extends State { /// Represents an existing task @visibleForTesting class ChecklistItem extends StatefulWidget { - final ChecklistSelectOption option; + final ChecklistSelectOption task; final VoidCallback? onSubmitted; + final bool autofocus; const ChecklistItem({ - required this.option, + required this.task, Key? key, this.onSubmitted, + this.autofocus = false, }) : super(key: key); @override @@ -168,7 +173,7 @@ class _ChecklistItemState extends State { @override void initState() { super.initState(); - _textController = TextEditingController(text: widget.option.data.name); + _textController = TextEditingController(text: widget.task.data.name); _focusNode = FocusNode( onKey: (node, event) { if (event is RawKeyDownEvent && @@ -179,72 +184,83 @@ class _ChecklistItemState extends State { return KeyEventResult.ignored; }, ); + if (widget.autofocus) { + _focusNode.requestFocus(); + } + } + + @override + void didUpdateWidget(ChecklistItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.task.data.name != oldWidget.task.data.name && + !_focusNode.hasFocus) { + _textController.text = widget.task.data.name; + } } @override Widget build(BuildContext context) { final icon = FlowySvg( - widget.option.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s, + widget.task.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: [ + constraints: BoxConstraints(maxHeight: GridSize.popoverItemHeight), + 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( + ChecklistCellEvent.selectTask(widget.task.data), + ), + ), + Expanded( + child: TextField( + controller: _textController, + focusNode: _focusNode, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 1, + decoration: InputDecoration( + border: InputBorder.none, + isCollapsed: true, + contentPadding: EdgeInsets.only( + top: 6.0, + bottom: 6.0, + left: 2.0, + right: _isHovered ? 2.0 : 8.0, + ), + hintText: LocaleKeys.grid_checklist_taskHint.tr(), + ), + onChanged: _debounceOnChangedText, + onSubmitted: (description) { + _submitUpdateTaskDescription(description); + widget.onSubmitted?.call(); + }, + ), + ), + if (_isHovered) FlowyIconButton( width: 32, - icon: icon, + icon: const FlowySvg(FlowySvgs.delete_s), hoverColor: Colors.transparent, - onPressed: () => context.read().add( - ChecklistCellEditorEvent.selectTask(widget.option.data), + iconColorOnHover: Theme.of(context).colorScheme.error, + onPressed: () => context.read().add( + ChecklistCellEvent.deleteTask(widget.task.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(), - ), - onChanged: _debounceOnChangedText, - onSubmitted: (description) { - _submitUpdateTaskDescription(description); - 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), - ), - ), - ], - ), + ], ), ), ); @@ -258,10 +274,10 @@ class _ChecklistItemState extends State { } void _submitUpdateTaskDescription(String description) { - context.read().add( - ChecklistCellEditorEvent.updateTaskName( - widget.option.data, - description, + context.read().add( + ChecklistCellEvent.updateTaskName( + widget.task.data, + description.trim(), ), ); } @@ -316,8 +332,8 @@ class _NewTaskItemState extends State { ), onSubmitted: (taskDescription) { if (taskDescription.trim().isNotEmpty) { - context.read().add( - ChecklistCellEditorEvent.newTask( + context.read().add( + ChecklistCellEvent.createNewTask( taskDescription.trim(), ), ); @@ -340,11 +356,10 @@ class _NewTaskItemState extends State { 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(), - ), + final text = _textEditingController.text.trim(); + if (text.isNotEmpty) { + context.read().add( + ChecklistCellEvent.createNewTask(text), ); } widget.focusNode.requestFocus(); 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 deleted file mode 100644 index 55185a521c..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart +++ /dev/null @@ -1,176 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database_view/application/cell/checklist_cell_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -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; - final ChecklistCellController cellController; - - ChecklistCellEditorBloc({ - required this.cellController, - }) : _checklistCellService = ChecklistCellBackendService( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - super(ChecklistCellEditorState.initial(cellController)) { - on( - (event, emit) async { - await event.when( - initial: () async { - _startListening(); - _loadOptions(); - }, - didReceiveTasks: (data) { - emit( - state.copyWith( - allOptions: _makeChecklistSelectOptions(data), - percent: data?.percentage ?? 0, - ), - ); - }, - newTask: (optionName) async { - await _createOption(optionName); - emit( - state.copyWith( - createOption: Some(optionName), - ), - ); - }, - deleteTask: (option) async { - await _deleteOption([option]); - }, - updateTaskName: (option, name) { - _updateOption(option, name); - }, - selectTask: (option) async { - await _checklistCellService.select(optionId: option.id); - }, - ); - }, - ); - } - - @override - Future close() async { - await cellController.dispose(); - return super.close(); - } - - Future _createOption(String name) async { - final result = await _checklistCellService.create(name: name); - result.fold((l) => {}, (err) => Log.error(err)); - } - - 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, String name) async { - final result = - await _checklistCellService.updateName(option: option, name: name); - - result.fold((l) => null, (err) => Log.error(err)); - } - - void _loadOptions() { - _checklistCellService.getCellData().then((result) { - if (isClosed) return; - - return result.fold( - (data) => add(ChecklistCellEditorEvent.didReceiveTasks(data)), - (err) => Log.error(err), - ); - }); - } - - void _startListening() { - cellController.startListening( - onCellChanged: ((data) { - if (!isClosed) { - add(ChecklistCellEditorEvent.didReceiveTasks(data)); - } - }), - onCellFieldChanged: () { - _loadOptions(); - }, - ); - } -} - -@freezed -class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent { - const factory ChecklistCellEditorEvent.initial() = _Initial; - 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 -class ChecklistCellEditorState with _$ChecklistCellEditorState { - const factory ChecklistCellEditorState({ - required List allOptions, - required Option createOption, - required double percent, - }) = _ChecklistCellEditorState; - - factory ChecklistCellEditorState.initial(ChecklistCellController context) { - final data = context.getCellData(loadIfNotExist: true); - - return ChecklistCellEditorState( - allOptions: _makeChecklistSelectOptions(data), - createOption: none(), - percent: data?.percentage ?? 0, - ); - } -} - -List _makeChecklistSelectOptions( - ChecklistCellDataPB? data, -) { - if (data == null) { - return []; - } - - final List options = []; - final List allOptions = List.from(data.options); - final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList(); - - for (final option in allOptions) { - options.add( - ChecklistSelectOption(selectedOptionIds.contains(option.id), option), - ); - } - - return options; -} 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 20a274328a..90099a9e71 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 @@ -167,7 +167,10 @@ class _PropertyCellState extends State<_PropertyCell> { final gesture = GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => cell.requestFocus.notify(), - child: AccessoryHover(child: cell), + child: AccessoryHover( + fieldType: widget.cellContext.fieldType, + child: cell, + ), ); return Container( @@ -271,7 +274,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) { case FieldType.Checklist: return ChecklistCellStyle( placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), - cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + cellPadding: const EdgeInsets.symmetric(vertical: 6), + showTasksInline: true, ); case FieldType.Number: return GridNumberCellStyle( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts b/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts index 48fed99cbe..518a4f493d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/database_bd_svc.ts @@ -86,7 +86,6 @@ import { DatabaseEventUpdateCell, DatabaseEventGetSelectOptionCellData, DatabaseEventUpdateSelectOptionCell, - DatabaseEventGetChecklistCellData, DatabaseEventUpdateChecklistCell, DatabaseEventUpdateDateCell, DatabaseEventExportCSV, @@ -623,18 +622,6 @@ export async function updateSelectOptionCell( return result.unwrap(); } -export async function getChecklistCell(viewId: string, rowId: string, fieldId: string): Promise { - const payload = CellIdPB.fromObject({ - view_id: viewId, - row_id: rowId, - field_id: fieldId, - }); - - const result = await DatabaseEventGetChecklistCellData(payload); - - return result.unwrap(); -} - export async function updateChecklistCell( viewId: string, rowId: string, diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index a9dd5584a6..4c55473840 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -533,7 +533,9 @@ "checklist": { "taskHint": "Task description", "addNew": "Add a new task", - "submitNewTask": "Create" + "submitNewTask": "Create", + "hideComplete": "Hide completed tasks", + "showComplete": "Show all tasks" }, "menuName": "Grid", "referencedGridPrefix": "View of" @@ -870,4 +872,4 @@ "weAreSorry": "We're sorry", "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." } -} +} \ No newline at end of file diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index 18203bd9de..ea2b28c33d 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -696,16 +696,8 @@ impl FlowyCoreTest { field_id: &str, row_id: &str, ) -> ChecklistCellDataPB { - EventBuilder::new(self.clone()) - .event(DatabaseEvent::GetChecklistCellData) - .payload(CellIdPB { - view_id: view_id.to_string(), - row_id: row_id.to_string(), - field_id: field_id.to_string(), - }) - .async_send() - .await - .parse::() + let cell = self.get_cell(view_id, row_id, field_id).await; + ChecklistCellDataPB::try_from(Bytes::from(cell.data)).unwrap() } pub async fn update_checklist_cell( diff --git a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs index 7f616c935a..ac27d959ab 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/type_option_entities/checklist_entities.rs @@ -5,7 +5,6 @@ use flowy_error::{ErrorCode, FlowyError}; use crate::entities::parser::NotEmptyStr; use crate::entities::SelectOptionPB; -use crate::services::field::checklist_type_option::ChecklistCellData; use crate::services::field::SelectOption; #[derive(Debug, Clone, Default, ProtoBuf)] @@ -20,25 +19,6 @@ pub struct ChecklistCellDataPB { pub percentage: f64, } -impl From for ChecklistCellDataPB { - fn from(cell_data: ChecklistCellData) -> Self { - let selected_options = cell_data.selected_options(); - let percentage = cell_data.percentage_complete(); - Self { - options: cell_data - .options - .into_iter() - .map(|option| option.into()) - .collect(), - selected_options: selected_options - .into_iter() - .map(|option| option.into()) - .collect(), - percentage, - } - } -} - #[derive(Debug, Clone, Default, ProtoBuf)] pub struct ChecklistCellDataChangesetPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 7129193c7c..02efd04ac3 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -596,20 +596,6 @@ pub(crate) async fn update_select_option_cell_handler( Ok(()) } -#[tracing::instrument(level = "trace", skip_all, err)] -pub(crate) async fn get_checklist_cell_data_handler( - data: AFPluginData, - manager: AFPluginState>, -) -> DataResult { - let manager = upgrade_manager(manager)?; - let params: CellIdParams = data.into_inner().try_into()?; - let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - let data = database_editor - .get_checklist_option(params.row_id, ¶ms.field_id) - .await; - data_result_ok(data) -} - #[tracing::instrument(level = "trace", skip_all, err)] pub(crate) async fn update_checklist_cell_handler( data: AFPluginData, @@ -625,7 +611,7 @@ pub(crate) async fn update_checklist_cell_handler( update_options: params.update_options, }; database_editor - .set_checklist_options(¶ms.view_id, params.row_id, ¶ms.field_id, changeset) + .update_cell_with_changeset(¶ms.view_id, params.row_id, ¶ms.field_id, changeset) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 026d16a8db..0d7d8e5d78 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -50,7 +50,6 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler) .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) // Checklist - .event(DatabaseEvent::GetChecklistCellData, get_checklist_cell_data_handler) .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) // Date .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) @@ -256,11 +255,8 @@ pub enum DatabaseEvent { #[event(input = "SelectOptionCellChangesetPB")] UpdateSelectOptionCell = 72, - #[event(input = "CellIdPB", output = "ChecklistCellDataPB")] - GetChecklistCellData = 73, - #[event(input = "ChecklistCellDataChangesetPB")] - UpdateChecklistCell = 74, + UpdateChecklistCell = 73, /// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPB] /// contains the date and the time string. It can be cast to [CellChangesetPB] that diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 88ce9c9f12..ed539a1378 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -21,7 +21,7 @@ use crate::services::cell::{ use crate::services::database::util::database_view_setting_pb_from_view; use crate::services::database::UpdatedRow; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews}; -use crate::services::field::checklist_type_option::{ChecklistCellChangeset, ChecklistCellData}; +use crate::services::field::checklist_type_option::ChecklistCellChangeset; use crate::services::field::{ default_type_option_data_from_type, select_type_option_from_field, transform_type_option, type_option_data_from_pb_or_default, type_option_to_pb, SelectOptionCellChangeset, @@ -858,15 +858,6 @@ impl DatabaseEditor { } } - pub async fn get_checklist_option(&self, row_id: RowId, field_id: &str) -> ChecklistCellDataPB { - let row_cell = self.database.lock().get_cell(field_id, &row_id); - let cell_data = match row_cell.cell { - None => ChecklistCellData::default(), - Some(cell) => ChecklistCellData::from(&cell), - }; - ChecklistCellDataPB::from(cell_data) - } - pub async fn set_checklist_options( &self, view_id: &str, diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 125b770cdb..4b102fe0e9 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -8,7 +8,7 @@ use strum::EnumCount; use event_integration::folder_event::ViewTest; use event_integration::FlowyCoreTest; -use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB, SelectOptionPB}; +use flowy_database2::entities::{FieldType, FilterPB, RowMetaPB}; use flowy_database2::services::cell::{CellBuilder, ToCellChangeset}; use flowy_database2::services::database::DatabaseEditor; use flowy_database2::services::field::checklist_type_option::{ @@ -221,7 +221,7 @@ impl DatabaseEditorTest { pub(crate) async fn set_checklist_cell( &mut self, row_id: RowId, - f: Box) -> Vec>, + selected_options: Vec, ) -> FlowyResult<()> { let field = self .editor @@ -233,13 +233,8 @@ impl DatabaseEditorTest { }) .unwrap() .clone(); - let options = self - .editor - .get_checklist_option(row_id.clone(), &field.id) - .await - .options; let cell_changeset = ChecklistCellChangeset { - selected_option_ids: f(options), + selected_option_ids: selected_options, ..Default::default() }; self diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs index 115ecd919e..b8abeeca9c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs @@ -1,4 +1,5 @@ -use flowy_database2::entities::ChecklistFilterConditionPB; +use flowy_database2::entities::{ChecklistFilterConditionPB, FieldType}; +use flowy_database2::services::field::checklist_type_option::ChecklistCellData; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; @@ -8,10 +9,12 @@ async fn grid_filter_checklist_is_incomplete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 6; let row_count = test.row_details.len(); + let option_ids = get_checklist_cell_options(&test).await; + let scripts = vec![ UpdateChecklistCell { row_id: test.row_details[0].row.id.clone(), - f: Box::new(|options| options.into_iter().map(|option| option.id).collect()), + selected_option_ids: option_ids, }, CreateChecklistFilter { condition: ChecklistFilterConditionPB::IsIncomplete, @@ -30,10 +33,11 @@ async fn grid_filter_checklist_is_complete_test() { let mut test = DatabaseFilterTest::new().await; let expected = 1; let row_count = test.row_details.len(); + let option_ids = get_checklist_cell_options(&test).await; let scripts = vec![ UpdateChecklistCell { row_id: test.row_details[0].row.id.clone(), - f: Box::new(|options| options.into_iter().map(|option| option.id).collect()), + selected_option_ids: option_ids, }, CreateChecklistFilter { condition: ChecklistFilterConditionPB::IsComplete, @@ -46,3 +50,20 @@ async fn grid_filter_checklist_is_complete_test() { ]; test.run_scripts(scripts).await; } + +async fn get_checklist_cell_options(test: &DatabaseFilterTest) -> Vec { + let field = test.get_first_field(FieldType::Checklist); + let row_cell = test + .editor + .get_cell(&field.id, &test.row_details[0].row.id) + .await; + row_cell + .map_or_else( + || ChecklistCellData::default(), + |cell| ChecklistCellData::from(&cell), + ) + .options + .into_iter() + .map(|option| option.id) + .collect() +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs index 80ca12f72e..de07820b6c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -30,7 +30,7 @@ pub enum FilterScript { }, UpdateChecklistCell{ row_id: RowId, - f: Box) -> Vec> , + selected_option_ids: Vec, }, UpdateSingleSelectCell { row_id: RowId, @@ -138,8 +138,8 @@ impl DatabaseFilterTest { self.assert_future_changed(changed).await; self.update_text_cell(row_id, &text).await.unwrap(); } - FilterScript::UpdateChecklistCell { row_id, f } => { - self.set_checklist_cell( row_id, f).await.unwrap(); + FilterScript::UpdateChecklistCell { row_id, selected_option_ids } => { + self.set_checklist_cell( row_id, selected_option_ids).await.unwrap(); } FilterScript::UpdateSingleSelectCell { row_id, option_id, changed} => { self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap());