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
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); final widget = this.widget<ChecklistItem>(task);
assert( 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/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/checklist_entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@ -69,13 +68,4 @@ class ChecklistCellBackendService {
return DatabaseEventUpdateChecklistCell(payload).send(); 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( final gesture = GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(), onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(child: cell), child: AccessoryHover(
fieldType: widget.cellContext.fieldType,
child: cell,
),
); );
return Container( return Container(

View File

@ -33,10 +33,9 @@ class _ChecklistCellState extends State<ChecklistCardCell> {
value: _cellBloc, value: _cellBloc,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>( child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
builder: (context, state) { builder: (context, state) {
if (state.allOptions.isEmpty) { if (state.tasks.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
child: ChecklistProgressBar(percent: state.percent), child: ChecklistProgressBar(percent: state.percent),

View File

@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart'; 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/size.dart';
import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra/theme_extension.dart';
@ -92,7 +93,12 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
class AccessoryHover extends StatefulWidget { class AccessoryHover extends StatefulWidget {
final CellAccessory child; final CellAccessory child;
const AccessoryHover({required this.child, super.key}); final FieldType fieldType;
const AccessoryHover({
super.key,
required this.child,
required this.fieldType,
});
@override @override
State<AccessoryHover> createState() => _AccessoryHoverState(); State<AccessoryHover> createState() => _AccessoryHoverState();
@ -106,7 +112,7 @@ class _AccessoryHoverState extends State<AccessoryHover> {
final List<Widget> children = [ final List<Widget> children = [
DecoratedBox( DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: _isHover color: _isHover && widget.fieldType != FieldType.Checklist
? AFThemeExtension.of(context).lightGreyHover ? AFThemeExtension.of(context).lightGreyHover
: Colors.transparent, : Colors.transparent,
borderRadius: Corners.s6Border, 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/application/cell/cell_controller_builder.dart';
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
import 'package:appflowy_popover/appflowy_popover.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/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -11,24 +16,30 @@ import 'checklist_cell_editor.dart';
import 'checklist_progress_bar.dart'; import 'checklist_progress_bar.dart';
class ChecklistCellStyle extends GridCellStyle { class ChecklistCellStyle extends GridCellStyle {
String placeholder; final String placeholder;
EdgeInsets? cellPadding; final EdgeInsets? cellPadding;
final bool showTasksInline;
ChecklistCellStyle({ const ChecklistCellStyle({
required this.placeholder, this.placeholder = "",
this.cellPadding, this.cellPadding,
this.showTasksInline = false,
}); });
} }
class GridChecklistCell extends GridCellWidget { class GridChecklistCell extends GridCellWidget {
final CellControllerBuilder cellControllerBuilder; final CellControllerBuilder cellControllerBuilder;
late final ChecklistCellStyle? cellStyle; late final ChecklistCellStyle cellStyle;
GridChecklistCell({ GridChecklistCell({
required this.cellControllerBuilder, required this.cellControllerBuilder,
GridCellStyle? style, GridCellStyle? style,
super.key, super.key,
}) { }) {
cellStyle = style as ChecklistCellStyle?; if (style != null) {
cellStyle = (style as ChecklistCellStyle);
} else {
cellStyle = const ChecklistCellStyle();
}
} }
@override @override
@ -38,14 +49,15 @@ class GridChecklistCell extends GridCellWidget {
class GridChecklistCellState extends GridCellState<GridChecklistCell> { class GridChecklistCellState extends GridCellState<GridChecklistCell> {
late ChecklistCellBloc _cellBloc; late ChecklistCellBloc _cellBloc;
late final PopoverController _popover; late final PopoverController _popover;
bool showIncompleteOnly = false;
@override @override
void initState() { void initState() {
_popover = PopoverController(); _popover = PopoverController();
final cellController = final cellController =
widget.cellControllerBuilder.build() as ChecklistCellController; widget.cellControllerBuilder.build() as ChecklistCellController;
_cellBloc = ChecklistCellBloc(cellController: cellController); _cellBloc = ChecklistCellBloc(cellController: cellController)
_cellBloc.add(const ChecklistCellEvent.initial()); ..add(const ChecklistCellEvent.initial());
super.initState(); super.initState();
} }
@ -53,7 +65,70 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _cellBloc, value: _cellBloc,
child: AppFlowyPopover( 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, margin: EdgeInsets.zero,
controller: _popover, controller: _popover,
constraints: BoxConstraints.loose(const Size(360, 400)), constraints: BoxConstraints.loose(const Size(360, 400)),
@ -64,8 +139,8 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
widget.onCellFocus.value = true; widget.onCellFocus.value = true;
}); });
return GridChecklistCellEditor( return GridChecklistCellEditor(
cellController: cellController: widget.cellControllerBuilder.build()
widget.cellControllerBuilder.build() as ChecklistCellController, as ChecklistCellController,
); );
}, },
onClose: () => widget.onCellFocus.value = false, onClose: () => widget.onCellFocus.value = false,
@ -73,24 +148,70 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Padding( child: Padding(
padding: padding:
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets, widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>( child: state.tasks.isEmpty
builder: (context, state) { ? FlowyText.medium(
if (state.allOptions.isEmpty) { widget.cellStyle.placeholder,
return FlowyText.medium(
widget.cellStyle?.placeholder ?? "",
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
)
: ChecklistProgressBar(percent: state.percent),
),
),
); );
}
return ChecklistProgressBar(percent: state.percent);
}, },
), ),
),
),
),
); );
} }
@override @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'; import 'dart:async';
part 'checklist_cell_bloc.freezed.dart'; 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> { class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
final ChecklistCellController cellController; final ChecklistCellController cellController;
final ChecklistCellBackendService _checklistCellSvc; final ChecklistCellBackendService _checklistCellService;
void Function()? _onCellChangedFn; void Function()? _onCellChangedFn;
ChecklistCellBloc({ ChecklistCellBloc({
required this.cellController, required this.cellController,
}) : _checklistCellSvc = ChecklistCellBackendService( }) : _checklistCellService = ChecklistCellBackendService(
viewId: cellController.viewId, viewId: cellController.viewId,
fieldId: cellController.fieldId, fieldId: cellController.fieldId,
rowId: cellController.rowId, rowId: cellController.rowId,
@ -23,28 +30,43 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
on<ChecklistCellEvent>( on<ChecklistCellEvent>(
(event, emit) async { (event, emit) async {
await event.when( await event.when(
initial: () async { initial: () {
_startListening(); _startListening();
_loadOptions();
}, },
didReceiveOptions: (data) { didReceiveOptions: (data) {
if (data == null) { if (data == null) {
emit( emit(
const ChecklistCellState( const ChecklistCellState(
allOptions: [], tasks: [],
selectedOptions: [],
percent: 0, percent: 0,
newTask: false,
), ),
); );
} else { return;
}
emit( emit(
state.copyWith( state.copyWith(
allOptions: data.options, tasks: _makeChecklistSelectOptions(data),
selectedOptions: data.selectedOptions,
percent: data.percentage, 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() { void _startListening() {
_onCellChangedFn = cellController.startListening( _onCellChangedFn = cellController.startListening(
onCellFieldChanged: () {
_loadOptions();
},
onCellChanged: (data) { onCellChanged: (data) {
if (!isClosed) { if (!isClosed) {
add(ChecklistCellEvent.didReceiveOptions(data)); add(ChecklistCellEvent.didReceiveOptions(data));
@ -74,15 +93,18 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
); );
} }
void _loadOptions() { void _updateOption(SelectOptionPB option, String name) async {
_checklistCellSvc.getCellData().then((result) { final result =
if (isClosed) return; await _checklistCellService.updateName(option: option, name: name);
return result.fold( result.fold((l) => null, (err) => Log.error(err));
(data) => add(ChecklistCellEvent.didReceiveOptions(data)), }
(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( const factory ChecklistCellEvent.didReceiveOptions(
ChecklistCellDataPB? data, ChecklistCellDataPB? data,
) = _DidReceiveCellUpdate; ) = _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 @freezed
class ChecklistCellState with _$ChecklistCellState { class ChecklistCellState with _$ChecklistCellState {
const factory ChecklistCellState({ const factory ChecklistCellState({
required List<SelectOptionPB> allOptions, required List<ChecklistSelectOption> tasks,
required List<SelectOptionPB> selectedOptions,
required double percent, required double percent,
required bool newTask,
}) = _ChecklistCellState; }) = _ChecklistCellState;
factory ChecklistCellState.initial(ChecklistCellController cellController) { factory ChecklistCellState.initial(ChecklistCellController cellController) {
return const ChecklistCellState( final cellData = cellController.getCellData(loadIfNotExist: true);
allOptions: [],
selectedOptions: [], return ChecklistCellState(
percent: 0, 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/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'checklist_cell_editor_bloc.dart'; import 'checklist_cell_bloc.dart';
import 'checklist_progress_bar.dart'; import 'checklist_progress_bar.dart';
class GridChecklistCellEditor extends StatefulWidget { class GridChecklistCellEditor extends StatefulWidget {
@ -22,12 +22,11 @@ class GridChecklistCellEditor extends StatefulWidget {
const GridChecklistCellEditor({required this.cellController, super.key}); const GridChecklistCellEditor({required this.cellController, super.key});
@override @override
State<GridChecklistCellEditor> createState() => State<GridChecklistCellEditor> createState() => _GridChecklistCellState();
_GridChecklistCellEditorState();
} }
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> { class _GridChecklistCellState extends State<GridChecklistCellEditor> {
late ChecklistCellEditorBloc _bloc; late ChecklistCellBloc _bloc;
/// Focus node for the new task text field /// Focus node for the new task text field
late final FocusNode newTaskFocusNode; late final FocusNode newTaskFocusNode;
@ -45,17 +44,17 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
return KeyEventResult.ignored; return KeyEventResult.ignored;
}, },
); );
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController) _bloc = ChecklistCellBloc(cellController: widget.cellController)
..add(const ChecklistCellEditorEvent.initial()); ..add(const ChecklistCellEvent.initial());
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _bloc, value: _bloc,
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>( child: BlocConsumer<ChecklistCellBloc, ChecklistCellState>(
listener: (context, state) { listener: (context, state) {
if (state.allOptions.isEmpty) { if (state.tasks.isEmpty) {
newTaskFocusNode.requestFocus(); newTaskFocusNode.requestFocus();
} }
}, },
@ -65,7 +64,7 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
children: [ children: [
AnimatedSwitcher( AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: state.allOptions.isEmpty child: state.tasks.isEmpty
? const SizedBox.shrink() ? const SizedBox.shrink()
: Padding( : Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
@ -75,10 +74,10 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
), ),
), ),
ChecklistItemList( ChecklistItemList(
options: state.allOptions, options: state.tasks,
onUpdateTask: () => newTaskFocusNode.requestFocus(), onUpdateTask: () => newTaskFocusNode.requestFocus(),
), ),
if (state.allOptions.isNotEmpty) if (state.tasks.isNotEmpty)
const TypeOptionSeparator(spacing: 0.0), const TypeOptionSeparator(spacing: 0.0),
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
@ -123,12 +122,16 @@ class _ChecklistItemListState extends State<ChecklistItemList> {
final itemList = widget.options final itemList = widget.options
.mapIndexed( .mapIndexed(
(index, option) => ChecklistItem( (index, option) => Padding(
option: option, padding: const EdgeInsets.symmetric(horizontal: 8.0),
onSubmitted: child: ChecklistItem(
index == widget.options.length - 1 ? widget.onUpdateTask : null, task: option,
onSubmitted: index == widget.options.length - 1
? widget.onUpdateTask
: null,
key: ValueKey(option.data.id), key: ValueKey(option.data.id),
), ),
),
) )
.toList(); .toList();
@ -147,12 +150,14 @@ class _ChecklistItemListState extends State<ChecklistItemList> {
/// Represents an existing task /// Represents an existing task
@visibleForTesting @visibleForTesting
class ChecklistItem extends StatefulWidget { class ChecklistItem extends StatefulWidget {
final ChecklistSelectOption option; final ChecklistSelectOption task;
final VoidCallback? onSubmitted; final VoidCallback? onSubmitted;
final bool autofocus;
const ChecklistItem({ const ChecklistItem({
required this.option, required this.task,
Key? key, Key? key,
this.onSubmitted, this.onSubmitted,
this.autofocus = false,
}) : super(key: key); }) : super(key: key);
@override @override
@ -168,7 +173,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_textController = TextEditingController(text: widget.option.data.name); _textController = TextEditingController(text: widget.task.data.name);
_focusNode = FocusNode( _focusNode = FocusNode(
onKey: (node, event) { onKey: (node, event) {
if (event is RawKeyDownEvent && if (event is RawKeyDownEvent &&
@ -179,21 +184,31 @@ class _ChecklistItemState extends State<ChecklistItem> {
return KeyEventResult.ignored; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final icon = FlowySvg( 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, blendMode: BlendMode.dst,
); );
return MouseRegion( return MouseRegion(
onEnter: (event) => setState(() => _isHovered = true), onEnter: (event) => setState(() => _isHovered = true),
onExit: (event) => setState(() => _isHovered = false), onExit: (event) => setState(() => _isHovered = false),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0), constraints: BoxConstraints(maxHeight: GridSize.popoverItemHeight),
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: _isHovered color: _isHovered
? AFThemeExtension.of(context).lightGreyHover ? AFThemeExtension.of(context).lightGreyHover
@ -207,8 +222,8 @@ class _ChecklistItemState extends State<ChecklistItem> {
width: 32, width: 32,
icon: icon, icon: icon,
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
onPressed: () => context.read<ChecklistCellEditorBloc>().add( onPressed: () => context.read<ChecklistCellBloc>().add(
ChecklistCellEditorEvent.selectTask(widget.option.data), ChecklistCellEvent.selectTask(widget.task.data),
), ),
), ),
Expanded( Expanded(
@ -220,9 +235,11 @@ class _ChecklistItemState extends State<ChecklistItem> {
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
isCollapsed: true, isCollapsed: true,
contentPadding: const EdgeInsets.symmetric( contentPadding: EdgeInsets.only(
vertical: 6.0, top: 6.0,
horizontal: 2.0, bottom: 6.0,
left: 2.0,
right: _isHovered ? 2.0 : 8.0,
), ),
hintText: LocaleKeys.grid_checklist_taskHint.tr(), hintText: LocaleKeys.grid_checklist_taskHint.tr(),
), ),
@ -239,14 +256,13 @@ class _ChecklistItemState extends State<ChecklistItem> {
icon: const FlowySvg(FlowySvgs.delete_s), icon: const FlowySvg(FlowySvgs.delete_s),
hoverColor: Colors.transparent, hoverColor: Colors.transparent,
iconColorOnHover: Theme.of(context).colorScheme.error, iconColorOnHover: Theme.of(context).colorScheme.error,
onPressed: () => context.read<ChecklistCellEditorBloc>().add( onPressed: () => context.read<ChecklistCellBloc>().add(
ChecklistCellEditorEvent.deleteTask(widget.option.data), ChecklistCellEvent.deleteTask(widget.task.data),
), ),
), ),
], ],
), ),
), ),
),
); );
} }
@ -258,10 +274,10 @@ class _ChecklistItemState extends State<ChecklistItem> {
} }
void _submitUpdateTaskDescription(String description) { void _submitUpdateTaskDescription(String description) {
context.read<ChecklistCellEditorBloc>().add( context.read<ChecklistCellBloc>().add(
ChecklistCellEditorEvent.updateTaskName( ChecklistCellEvent.updateTaskName(
widget.option.data, widget.task.data,
description, description.trim(),
), ),
); );
} }
@ -316,8 +332,8 @@ class _NewTaskItemState extends State<NewTaskItem> {
), ),
onSubmitted: (taskDescription) { onSubmitted: (taskDescription) {
if (taskDescription.trim().isNotEmpty) { if (taskDescription.trim().isNotEmpty) {
context.read<ChecklistCellEditorBloc>().add( context.read<ChecklistCellBloc>().add(
ChecklistCellEditorEvent.newTask( ChecklistCellEvent.createNewTask(
taskDescription.trim(), taskDescription.trim(),
), ),
); );
@ -340,11 +356,10 @@ class _NewTaskItemState extends State<NewTaskItem> {
fontColor: Theme.of(context).colorScheme.onPrimary, fontColor: Theme.of(context).colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
onPressed: () { onPressed: () {
if (_textEditingController.text.trim().isNotEmpty) { final text = _textEditingController.text.trim();
context.read<ChecklistCellEditorBloc>().add( if (text.isNotEmpty) {
ChecklistCellEditorEvent.newTask( context.read<ChecklistCellBloc>().add(
_textEditingController.text..trim(), ChecklistCellEvent.createNewTask(text),
),
); );
} }
widget.focusNode.requestFocus(); 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( final gesture = GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => cell.requestFocus.notify(), onTap: () => cell.requestFocus.notify(),
child: AccessoryHover(child: cell), child: AccessoryHover(
fieldType: widget.cellContext.fieldType,
child: cell,
),
); );
return Container( return Container(
@ -271,7 +274,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
case FieldType.Checklist: case FieldType.Checklist:
return ChecklistCellStyle( return ChecklistCellStyle(
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(), 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: case FieldType.Number:
return GridNumberCellStyle( return GridNumberCellStyle(

View File

@ -86,7 +86,6 @@ import {
DatabaseEventUpdateCell, DatabaseEventUpdateCell,
DatabaseEventGetSelectOptionCellData, DatabaseEventGetSelectOptionCellData,
DatabaseEventUpdateSelectOptionCell, DatabaseEventUpdateSelectOptionCell,
DatabaseEventGetChecklistCellData,
DatabaseEventUpdateChecklistCell, DatabaseEventUpdateChecklistCell,
DatabaseEventUpdateDateCell, DatabaseEventUpdateDateCell,
DatabaseEventExportCSV, DatabaseEventExportCSV,
@ -623,18 +622,6 @@ export async function updateSelectOptionCell(
return result.unwrap(); 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( export async function updateChecklistCell(
viewId: string, viewId: string,
rowId: string, rowId: string,

View File

@ -533,7 +533,9 @@
"checklist": { "checklist": {
"taskHint": "Task description", "taskHint": "Task description",
"addNew": "Add a new task", "addNew": "Add a new task",
"submitNewTask": "Create" "submitNewTask": "Create",
"hideComplete": "Hide completed tasks",
"showComplete": "Show all tasks"
}, },
"menuName": "Grid", "menuName": "Grid",
"referencedGridPrefix": "View of" "referencedGridPrefix": "View of"

View File

@ -696,16 +696,8 @@ impl FlowyCoreTest {
field_id: &str, field_id: &str,
row_id: &str, row_id: &str,
) -> ChecklistCellDataPB { ) -> ChecklistCellDataPB {
EventBuilder::new(self.clone()) let cell = self.get_cell(view_id, row_id, field_id).await;
.event(DatabaseEvent::GetChecklistCellData) ChecklistCellDataPB::try_from(Bytes::from(cell.data)).unwrap()
.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>()
} }
pub async fn update_checklist_cell( 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::parser::NotEmptyStr;
use crate::entities::SelectOptionPB; use crate::entities::SelectOptionPB;
use crate::services::field::checklist_type_option::ChecklistCellData;
use crate::services::field::SelectOption; use crate::services::field::SelectOption;
#[derive(Debug, Clone, Default, ProtoBuf)] #[derive(Debug, Clone, Default, ProtoBuf)]
@ -20,25 +19,6 @@ pub struct ChecklistCellDataPB {
pub percentage: f64, 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)] #[derive(Debug, Clone, Default, ProtoBuf)]
pub struct ChecklistCellDataChangesetPB { pub struct ChecklistCellDataChangesetPB {
#[pb(index = 1)] #[pb(index = 1)]

View File

@ -596,20 +596,6 @@ pub(crate) async fn update_select_option_cell_handler(
Ok(()) 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)] #[tracing::instrument(level = "trace", skip_all, err)]
pub(crate) async fn update_checklist_cell_handler( pub(crate) async fn update_checklist_cell_handler(
data: AFPluginData<ChecklistCellDataChangesetPB>, data: AFPluginData<ChecklistCellDataChangesetPB>,
@ -625,7 +611,7 @@ pub(crate) async fn update_checklist_cell_handler(
update_options: params.update_options, update_options: params.update_options,
}; };
database_editor 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?; .await?;
Ok(()) Ok(())
} }

View File

@ -50,7 +50,6 @@ pub fn init(database_manager: Weak<DatabaseManager>) -> AFPlugin {
.event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler) .event(DatabaseEvent::GetSelectOptionCellData, get_select_option_handler)
.event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler) .event(DatabaseEvent::UpdateSelectOptionCell, update_select_option_cell_handler)
// Checklist // Checklist
.event(DatabaseEvent::GetChecklistCellData, get_checklist_cell_data_handler)
.event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler) .event(DatabaseEvent::UpdateChecklistCell, update_checklist_cell_handler)
// Date // Date
.event(DatabaseEvent::UpdateDateCell, update_date_cell_handler) .event(DatabaseEvent::UpdateDateCell, update_date_cell_handler)
@ -256,11 +255,8 @@ pub enum DatabaseEvent {
#[event(input = "SelectOptionCellChangesetPB")] #[event(input = "SelectOptionCellChangesetPB")]
UpdateSelectOptionCell = 72, UpdateSelectOptionCell = 72,
#[event(input = "CellIdPB", output = "ChecklistCellDataPB")]
GetChecklistCellData = 73,
#[event(input = "ChecklistCellDataChangesetPB")] #[event(input = "ChecklistCellDataChangesetPB")]
UpdateChecklistCell = 74, UpdateChecklistCell = 73,
/// [UpdateDateCell] event is used to update a date cell's data. [DateChangesetPB] /// [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 /// 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::util::database_view_setting_pb_from_view;
use crate::services::database::UpdatedRow; use crate::services::database::UpdatedRow;
use crate::services::database_view::{DatabaseViewChanged, DatabaseViewData, DatabaseViews}; 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::{ use crate::services::field::{
default_type_option_data_from_type, select_type_option_from_field, transform_type_option, 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, 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( pub async fn set_checklist_options(
&self, &self,
view_id: &str, view_id: &str,

View File

@ -8,7 +8,7 @@ use strum::EnumCount;
use event_integration::folder_event::ViewTest; use event_integration::folder_event::ViewTest;
use event_integration::FlowyCoreTest; 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::cell::{CellBuilder, ToCellChangeset};
use flowy_database2::services::database::DatabaseEditor; use flowy_database2::services::database::DatabaseEditor;
use flowy_database2::services::field::checklist_type_option::{ use flowy_database2::services::field::checklist_type_option::{
@ -221,7 +221,7 @@ impl DatabaseEditorTest {
pub(crate) async fn set_checklist_cell( pub(crate) async fn set_checklist_cell(
&mut self, &mut self,
row_id: RowId, row_id: RowId,
f: Box<dyn FnOnce(Vec<SelectOptionPB>) -> Vec<String>>, selected_options: Vec<String>,
) -> FlowyResult<()> { ) -> FlowyResult<()> {
let field = self let field = self
.editor .editor
@ -233,13 +233,8 @@ impl DatabaseEditorTest {
}) })
.unwrap() .unwrap()
.clone(); .clone();
let options = self
.editor
.get_checklist_option(row_id.clone(), &field.id)
.await
.options;
let cell_changeset = ChecklistCellChangeset { let cell_changeset = ChecklistCellChangeset {
selected_option_ids: f(options), selected_option_ids: selected_options,
..Default::default() ..Default::default()
}; };
self 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::FilterScript::*;
use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; 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 mut test = DatabaseFilterTest::new().await;
let expected = 6; let expected = 6;
let row_count = test.row_details.len(); let row_count = test.row_details.len();
let option_ids = get_checklist_cell_options(&test).await;
let scripts = vec![ let scripts = vec![
UpdateChecklistCell { UpdateChecklistCell {
row_id: test.row_details[0].row.id.clone(), 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 { CreateChecklistFilter {
condition: ChecklistFilterConditionPB::IsIncomplete, condition: ChecklistFilterConditionPB::IsIncomplete,
@ -30,10 +33,11 @@ async fn grid_filter_checklist_is_complete_test() {
let mut test = DatabaseFilterTest::new().await; let mut test = DatabaseFilterTest::new().await;
let expected = 1; let expected = 1;
let row_count = test.row_details.len(); let row_count = test.row_details.len();
let option_ids = get_checklist_cell_options(&test).await;
let scripts = vec![ let scripts = vec![
UpdateChecklistCell { UpdateChecklistCell {
row_id: test.row_details[0].row.id.clone(), 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 { CreateChecklistFilter {
condition: ChecklistFilterConditionPB::IsComplete, condition: ChecklistFilterConditionPB::IsComplete,
@ -46,3 +50,20 @@ async fn grid_filter_checklist_is_complete_test() {
]; ];
test.run_scripts(scripts).await; 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{ UpdateChecklistCell{
row_id: RowId, row_id: RowId,
f: Box<dyn FnOnce(Vec<SelectOptionPB>) -> Vec<String>> , selected_option_ids: Vec<String>,
}, },
UpdateSingleSelectCell { UpdateSingleSelectCell {
row_id: RowId, row_id: RowId,
@ -138,8 +138,8 @@ impl DatabaseFilterTest {
self.assert_future_changed(changed).await; self.assert_future_changed(changed).await;
self.update_text_cell(row_id, &text).await.unwrap(); self.update_text_cell(row_id, &text).await.unwrap();
} }
FilterScript::UpdateChecklistCell { row_id, f } => { FilterScript::UpdateChecklistCell { row_id, selected_option_ids } => {
self.set_checklist_cell( row_id, f).await.unwrap(); self.set_checklist_cell( row_id, selected_option_ids).await.unwrap();
} }
FilterScript::UpdateSingleSelectCell { row_id, option_id, changed} => { FilterScript::UpdateSingleSelectCell { row_id, option_id, changed} => {
self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap());