feat: show checklist items inline in row page (#3737)

* feat: show checklist items inline in row page

* fix: tauri build
This commit is contained in:
Richard Shiue 2023-10-24 10:15:28 +08:00 committed by GitHub
parent 25a98cda81
commit 6c3d7d2079
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 402 additions and 436 deletions

View File

@ -529,7 +529,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
final widget = this.widget<ChecklistItem>(task);
assert(
widget.option.data.name == name && widget.option.isSelected == isChecked,
widget.task.data.name == name && widget.task.isSelected == isChecked,
);
}

View File

@ -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<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
final payload = CellIdPB.create()
..viewId = viewId
..fieldId = fieldId
..rowId = rowId;
return DatabaseEventGetChecklistCellData(payload).send();
}
}

View File

@ -187,7 +187,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(

View File

@ -33,10 +33,9 @@ class _ChecklistCellState extends State<ChecklistCardCell> {
value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
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),

View File

@ -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<PrimaryCellAccessory>
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<AccessoryHover> createState() => _AccessoryHoverState();
@ -106,7 +112,7 @@ class _AccessoryHoverState extends State<AccessoryHover> {
final List<Widget> children = [
DecoratedBox(
decoration: BoxDecoration(
color: _isHover
color: _isHover && widget.fieldType != FieldType.Checklist
? AFThemeExtension.of(context).lightGreyHover
: Colors.transparent,
borderRadius: Corners.s6Border,

View File

@ -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<GridChecklistCell> {
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<GridChecklistCell> {
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<ChecklistCellBloc, ChecklistCellState>(
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<ChecklistCellBloc, ChecklistCellState>(
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<ChecklistCellBloc>()
.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<ChecklistCellBloc>()
.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())),
],
),
),
),
);
}
}

View File

@ -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<ChecklistCellEvent, ChecklistCellState> {
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<ChecklistCellEvent, ChecklistCellState> {
on<ChecklistCellEvent>(
(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<ChecklistCellEvent, ChecklistCellState> {
void _startListening() {
_onCellChangedFn = cellController.startListening(
onCellFieldChanged: () {
_loadOptions();
},
onCellChanged: (data) {
if (!isClosed) {
add(ChecklistCellEvent.didReceiveOptions(data));
@ -74,15 +93,18 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
);
}
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<void> _deleteOption(List<SelectOptionPB> options) async {
final result = await _checklistCellService.delete(
optionIds: options.map((e) => e.id).toList(),
);
result.fold((l) => null, (err) => Log.error(err));
}
}
@ -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<SelectOptionPB> allOptions,
required List<SelectOptionPB> selectedOptions,
required List<ChecklistSelectOption> 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<ChecklistSelectOption> _makeChecklistSelectOptions(
ChecklistCellDataPB? data,
) {
if (data == null) {
return [];
}
final List<ChecklistSelectOption> options = [];
final List<SelectOptionPB> 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;
}

View File

@ -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<GridChecklistCellEditor> createState() =>
_GridChecklistCellEditorState();
State<GridChecklistCellEditor> createState() => _GridChecklistCellState();
}
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
late ChecklistCellEditorBloc _bloc;
class _GridChecklistCellState extends State<GridChecklistCellEditor> {
late ChecklistCellBloc _bloc;
/// Focus node for the new task text field
late final FocusNode newTaskFocusNode;
@ -45,17 +44,17 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
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<ChecklistCellEditorBloc, ChecklistCellEditorState>(
child: BlocConsumer<ChecklistCellBloc, ChecklistCellState>(
listener: (context, state) {
if (state.allOptions.isEmpty) {
if (state.tasks.isEmpty) {
newTaskFocusNode.requestFocus();
}
},
@ -65,7 +64,7 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
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<GridChecklistCellEditor> {
),
),
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<ChecklistItemList> {
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<ChecklistItemList> {
/// 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<ChecklistItem> {
@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<ChecklistItem> {
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<ChecklistCellBloc>().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<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.selectTask(widget.option.data),
iconColorOnHover: Theme.of(context).colorScheme.error,
onPressed: () => context.read<ChecklistCellBloc>().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<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.deleteTask(widget.option.data),
),
),
],
),
],
),
),
);
@ -258,10 +274,10 @@ class _ChecklistItemState extends State<ChecklistItem> {
}
void _submitUpdateTaskDescription(String description) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.updateTaskName(
widget.option.data,
description,
context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.updateTaskName(
widget.task.data,
description.trim(),
),
);
}
@ -316,8 +332,8 @@ class _NewTaskItemState extends State<NewTaskItem> {
),
onSubmitted: (taskDescription) {
if (taskDescription.trim().isNotEmpty) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.newTask(
context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.createNewTask(
taskDescription.trim(),
),
);
@ -340,11 +356,10 @@ class _NewTaskItemState extends State<NewTaskItem> {
fontColor: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
onPressed: () {
if (_textEditingController.text.trim().isNotEmpty) {
context.read<ChecklistCellEditorBloc>().add(
ChecklistCellEditorEvent.newTask(
_textEditingController.text..trim(),
),
final text = _textEditingController.text.trim();
if (text.isNotEmpty) {
context.read<ChecklistCellBloc>().add(
ChecklistCellEvent.createNewTask(text),
);
}
widget.focusNode.requestFocus();

View File

@ -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<ChecklistCellEditorEvent, ChecklistCellEditorState> {
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<ChecklistCellEditorEvent>(
(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<void> close() async {
await cellController.dispose();
return super.close();
}
Future<void> _createOption(String name) async {
final result = await _checklistCellService.create(name: name);
result.fold((l) => {}, (err) => Log.error(err));
}
Future<void> _deleteOption(List<SelectOptionPB> options) async {
final result = await _checklistCellService.delete(
optionIds: options.map((e) => e.id).toList(),
);
result.fold((l) => null, (err) => Log.error(err));
}
void _updateOption(SelectOptionPB option, 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<ChecklistSelectOption> allOptions,
required Option<String> 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<ChecklistSelectOption> _makeChecklistSelectOptions(
ChecklistCellDataPB? data,
) {
if (data == null) {
return [];
}
final List<ChecklistSelectOption> options = [];
final List<SelectOptionPB> 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;
}

View File

@ -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(

View File

@ -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<ChecklistCellDataPB> {
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,

View File

@ -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."
}
}
}

View File

@ -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::<ChecklistCellDataPB>()
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(

View File

@ -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<ChecklistCellData> 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)]

View File

@ -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<CellIdPB>,
manager: AFPluginState<Weak<DatabaseManager>>,
) -> DataResult<ChecklistCellDataPB, FlowyError> {
let manager = upgrade_manager(manager)?;
let params: CellIdParams = data.into_inner().try_into()?;
let database_editor = manager.get_database_with_view_id(&params.view_id).await?;
let data = database_editor
.get_checklist_option(params.row_id, &params.field_id)
.await;
data_result_ok(data)
}
#[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_checklist_cell_handler(
data: AFPluginData<ChecklistCellDataChangesetPB>,
@ -625,7 +611,7 @@ pub(crate) async fn update_checklist_cell_handler(
update_options: params.update_options,
};
database_editor
.set_checklist_options(&params.view_id, params.row_id, &params.field_id, changeset)
.update_cell_with_changeset(&params.view_id, params.row_id, &params.field_id, changeset)
.await?;
Ok(())
}

View File

@ -50,7 +50,6 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> 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

View File

@ -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,

View File

@ -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<dyn FnOnce(Vec<SelectOptionPB>) -> Vec<String>>,
selected_options: Vec<String>,
) -> 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

View File

@ -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<String> {
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()
}

View File

@ -30,7 +30,7 @@ pub enum FilterScript {
},
UpdateChecklistCell{
row_id: RowId,
f: Box<dyn FnOnce(Vec<SelectOptionPB>) -> Vec<String>> ,
selected_option_ids: Vec<String>,
},
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());