mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
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:
@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
@ -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,44 +65,153 @@ 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>(
|
||||||
margin: EdgeInsets.zero,
|
builder: (context, state) {
|
||||||
controller: _popover,
|
if (widget.cellStyle.showTasksInline) {
|
||||||
constraints: BoxConstraints.loose(const Size(360, 400)),
|
final tasks = List.from(state.tasks);
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
if (showIncompleteOnly) {
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
tasks.removeWhere((task) => task.isSelected);
|
||||||
popupBuilder: (BuildContext context) {
|
}
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
final children = tasks
|
||||||
widget.onCellFocus.value = true;
|
.mapIndexed(
|
||||||
});
|
(index, task) => ChecklistItem(
|
||||||
return GridChecklistCellEditor(
|
task: task,
|
||||||
cellController:
|
autofocus: state.newTask && index == tasks.length - 1,
|
||||||
widget.cellControllerBuilder.build() as ChecklistCellController,
|
),
|
||||||
|
)
|
||||||
|
.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
|
@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())),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(
|
|
||||||
state.copyWith(
|
|
||||||
allOptions: data.options,
|
|
||||||
selectedOptions: data.selectedOptions,
|
|
||||||
percent: data.percentage,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
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;
|
||||||
|
}
|
||||||
|
@ -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,11 +122,15 @@ 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,
|
||||||
key: ValueKey(option.data.id),
|
onSubmitted: index == widget.options.length - 1
|
||||||
|
? widget.onUpdateTask
|
||||||
|
: null,
|
||||||
|
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,72 +184,83 @@ 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),
|
decoration: BoxDecoration(
|
||||||
child: DecoratedBox(
|
color: _isHovered
|
||||||
decoration: BoxDecoration(
|
? AFThemeExtension.of(context).lightGreyHover
|
||||||
color: _isHovered
|
: Colors.transparent,
|
||||||
? AFThemeExtension.of(context).lightGreyHover
|
borderRadius: Corners.s6Border,
|
||||||
: Colors.transparent,
|
),
|
||||||
borderRadius: Corners.s6Border,
|
child: Row(
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: Row(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
FlowyIconButton(
|
||||||
children: [
|
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(
|
FlowyIconButton(
|
||||||
width: 32,
|
width: 32,
|
||||||
icon: icon,
|
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||||
hoverColor: Colors.transparent,
|
hoverColor: Colors.transparent,
|
||||||
onPressed: () => context.read<ChecklistCellEditorBloc>().add(
|
iconColorOnHover: Theme.of(context).colorScheme.error,
|
||||||
ChecklistCellEditorEvent.selectTask(widget.option.data),
|
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) {
|
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();
|
||||||
|
@ -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;
|
|
||||||
}
|
|
@ -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(
|
||||||
|
@ -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,
|
||||||
|
@ -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"
|
||||||
@ -870,4 +872,4 @@
|
|||||||
"weAreSorry": "We're sorry",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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(
|
||||||
|
@ -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)]
|
||||||
|
@ -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(¶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)]
|
#[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(¶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?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
}
|
||||||
|
@ -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());
|
||||||
|
Reference in New Issue
Block a user