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:
@ -1,5 +1,4 @@
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/cell_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@ -69,13 +68,4 @@ class ChecklistCellBackendService {
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<ChecklistCellDataPB, FlowyError>> getCellData() {
|
||||
final payload = CellIdPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
|
||||
return DatabaseEventGetChecklistCellData(payload).send();
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +187,10 @@ class _PropertyCellState extends State<PropertyCell> {
|
||||
final gesture = GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => cell.requestFocus.notify(),
|
||||
child: AccessoryHover(child: cell),
|
||||
child: AccessoryHover(
|
||||
fieldType: widget.cellContext.fieldType,
|
||||
child: cell,
|
||||
),
|
||||
);
|
||||
|
||||
return Container(
|
||||
|
@ -33,10 +33,9 @@ class _ChecklistCellState extends State<ChecklistCardCell> {
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
if (state.tasks.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
@ -92,7 +93,12 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
||||
|
||||
class AccessoryHover extends StatefulWidget {
|
||||
final CellAccessory child;
|
||||
const AccessoryHover({required this.child, super.key});
|
||||
final FieldType fieldType;
|
||||
const AccessoryHover({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.fieldType,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AccessoryHover> createState() => _AccessoryHoverState();
|
||||
@ -106,7 +112,7 @@ class _AccessoryHoverState extends State<AccessoryHover> {
|
||||
final List<Widget> children = [
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _isHover
|
||||
color: _isHover && widget.fieldType != FieldType.Checklist
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: Colors.transparent,
|
||||
borderRadius: Corners.s6Border,
|
||||
|
@ -1,7 +1,12 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@ -11,24 +16,30 @@ import 'checklist_cell_editor.dart';
|
||||
import 'checklist_progress_bar.dart';
|
||||
|
||||
class ChecklistCellStyle extends GridCellStyle {
|
||||
String placeholder;
|
||||
EdgeInsets? cellPadding;
|
||||
final String placeholder;
|
||||
final EdgeInsets? cellPadding;
|
||||
final bool showTasksInline;
|
||||
|
||||
ChecklistCellStyle({
|
||||
required this.placeholder,
|
||||
const ChecklistCellStyle({
|
||||
this.placeholder = "",
|
||||
this.cellPadding,
|
||||
this.showTasksInline = false,
|
||||
});
|
||||
}
|
||||
|
||||
class GridChecklistCell extends GridCellWidget {
|
||||
final CellControllerBuilder cellControllerBuilder;
|
||||
late final ChecklistCellStyle? cellStyle;
|
||||
late final ChecklistCellStyle cellStyle;
|
||||
GridChecklistCell({
|
||||
required this.cellControllerBuilder,
|
||||
GridCellStyle? style,
|
||||
super.key,
|
||||
}) {
|
||||
cellStyle = style as ChecklistCellStyle?;
|
||||
if (style != null) {
|
||||
cellStyle = (style as ChecklistCellStyle);
|
||||
} else {
|
||||
cellStyle = const ChecklistCellStyle();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -38,14 +49,15 @@ class GridChecklistCell extends GridCellWidget {
|
||||
class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
late ChecklistCellBloc _cellBloc;
|
||||
late final PopoverController _popover;
|
||||
bool showIncompleteOnly = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_popover = PopoverController();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as ChecklistCellController;
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const ChecklistCellEvent.initial());
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController)
|
||||
..add(const ChecklistCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -53,44 +65,153 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: AppFlowyPopover(
|
||||
margin: EdgeInsets.zero,
|
||||
controller: _popover,
|
||||
constraints: BoxConstraints.loose(const Size(360, 400)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onCellFocus.value = true;
|
||||
});
|
||||
return GridChecklistCellEditor(
|
||||
cellController:
|
||||
widget.cellControllerBuilder.build() as ChecklistCellController,
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (widget.cellStyle.showTasksInline) {
|
||||
final tasks = List.from(state.tasks);
|
||||
if (showIncompleteOnly) {
|
||||
tasks.removeWhere((task) => task.isSelected);
|
||||
}
|
||||
final children = tasks
|
||||
.mapIndexed(
|
||||
(index, task) => ChecklistItem(
|
||||
task: task,
|
||||
autofocus: state.newTask && index == tasks.length - 1,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
return Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
FlowyIconButton(
|
||||
tooltipText: showIncompleteOnly
|
||||
? LocaleKeys.grid_checklist_showComplete.tr()
|
||||
: LocaleKeys.grid_checklist_hideComplete.tr(),
|
||||
width: 32,
|
||||
iconColorOnHover:
|
||||
Theme.of(context).colorScheme.onSecondary,
|
||||
icon: FlowySvg(
|
||||
showIncompleteOnly
|
||||
? FlowySvgs.show_m
|
||||
: FlowySvgs.hide_m,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(
|
||||
() => showIncompleteOnly = !showIncompleteOnly,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const VSpace(4),
|
||||
...children,
|
||||
const ChecklistItemControl(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return AppFlowyPopover(
|
||||
margin: EdgeInsets.zero,
|
||||
controller: _popover,
|
||||
constraints: BoxConstraints.loose(const Size(360, 400)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onCellFocus.value = true;
|
||||
});
|
||||
return GridChecklistCellEditor(
|
||||
cellController: widget.cellControllerBuilder.build()
|
||||
as ChecklistCellController,
|
||||
);
|
||||
},
|
||||
onClose: () => widget.onCellFocus.value = false,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
widget.cellStyle.cellPadding ?? GridSize.cellContentInsets,
|
||||
child: state.tasks.isEmpty
|
||||
? FlowyText.medium(
|
||||
widget.cellStyle.placeholder,
|
||||
color: Theme.of(context).hintColor,
|
||||
)
|
||||
: ChecklistProgressBar(percent: state.percent),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
onClose: () => widget.onCellFocus.value = false,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding:
|
||||
widget.cellStyle?.cellPadding ?? GridSize.cellContentInsets,
|
||||
child: BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return FlowyText.medium(
|
||||
widget.cellStyle?.placeholder ?? "",
|
||||
color: Theme.of(context).hintColor,
|
||||
);
|
||||
}
|
||||
return ChecklistProgressBar(percent: state.percent);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void requestBeginFocus() => _popover.show();
|
||||
void requestBeginFocus() {
|
||||
if (!widget.cellStyle.showTasksInline) {
|
||||
_popover.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistItemControl extends StatelessWidget {
|
||||
const ChecklistItemControl({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0),
|
||||
child: SizedBox(
|
||||
height: 12,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.createNewTask("")),
|
||||
child: Row(
|
||||
children: [
|
||||
const Flexible(child: Center(child: Divider())),
|
||||
const HSpace(12.0),
|
||||
FlowyTooltip(
|
||||
message: LocaleKeys.grid_checklist_addNew.tr(),
|
||||
child: FilledButton(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size.square(12),
|
||||
maximumSize: const Size.square(12),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<ChecklistCellBloc>()
|
||||
.add(const ChecklistCellEvent.createNewTask("")),
|
||||
child: FlowySvg(
|
||||
FlowySvgs.add_s,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const HSpace(12.0),
|
||||
const Flexible(child: Center(child: Divider())),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -8,13 +8,20 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
part 'checklist_cell_bloc.freezed.dart';
|
||||
|
||||
class ChecklistSelectOption {
|
||||
final bool isSelected;
|
||||
final SelectOptionPB data;
|
||||
|
||||
ChecklistSelectOption(this.isSelected, this.data);
|
||||
}
|
||||
|
||||
class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
final ChecklistCellController cellController;
|
||||
final ChecklistCellBackendService _checklistCellSvc;
|
||||
final ChecklistCellBackendService _checklistCellService;
|
||||
void Function()? _onCellChangedFn;
|
||||
ChecklistCellBloc({
|
||||
required this.cellController,
|
||||
}) : _checklistCellSvc = ChecklistCellBackendService(
|
||||
}) : _checklistCellService = ChecklistCellBackendService(
|
||||
viewId: cellController.viewId,
|
||||
fieldId: cellController.fieldId,
|
||||
rowId: cellController.rowId,
|
||||
@ -23,28 +30,43 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
on<ChecklistCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
initial: () {
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (data) {
|
||||
if (data == null) {
|
||||
emit(
|
||||
const ChecklistCellState(
|
||||
allOptions: [],
|
||||
selectedOptions: [],
|
||||
tasks: [],
|
||||
percent: 0,
|
||||
newTask: false,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: data.options,
|
||||
selectedOptions: data.selectedOptions,
|
||||
percent: data.percentage,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
tasks: _makeChecklistSelectOptions(data),
|
||||
percent: data.percentage,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateTaskName: (option, name) {
|
||||
_updateOption(option, name);
|
||||
},
|
||||
selectTask: (option) async {
|
||||
await _checklistCellService.select(optionId: option.id);
|
||||
},
|
||||
createNewTask: (name) async {
|
||||
final result = await _checklistCellService.create(name: name);
|
||||
result.fold(
|
||||
(l) => emit(state.copyWith(newTask: true)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
deleteTask: (option) async {
|
||||
await _deleteOption([option]);
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -63,9 +85,6 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellFieldChanged: () {
|
||||
_loadOptions();
|
||||
},
|
||||
onCellChanged: (data) {
|
||||
if (!isClosed) {
|
||||
add(ChecklistCellEvent.didReceiveOptions(data));
|
||||
@ -74,15 +93,18 @@ class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
_checklistCellSvc.getCellData().then((result) {
|
||||
if (isClosed) return;
|
||||
void _updateOption(SelectOptionPB option, String name) async {
|
||||
final result =
|
||||
await _checklistCellService.updateName(option: option, name: name);
|
||||
|
||||
return result.fold(
|
||||
(data) => add(ChecklistCellEvent.didReceiveOptions(data)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
Future<void> _deleteOption(List<SelectOptionPB> options) async {
|
||||
final result = await _checklistCellService.delete(
|
||||
optionIds: options.map((e) => e.id).toList(),
|
||||
);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,21 +114,53 @@ class ChecklistCellEvent with _$ChecklistCellEvent {
|
||||
const factory ChecklistCellEvent.didReceiveOptions(
|
||||
ChecklistCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
const factory ChecklistCellEvent.updateTaskName(
|
||||
SelectOptionPB option,
|
||||
String name,
|
||||
) = _UpdateTaskName;
|
||||
const factory ChecklistCellEvent.selectTask(SelectOptionPB task) =
|
||||
_SelectTask;
|
||||
const factory ChecklistCellEvent.createNewTask(String description) =
|
||||
_CreateNewTask;
|
||||
const factory ChecklistCellEvent.deleteTask(SelectOptionPB option) =
|
||||
_DeleteTask;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChecklistCellState with _$ChecklistCellState {
|
||||
const factory ChecklistCellState({
|
||||
required List<SelectOptionPB> allOptions,
|
||||
required List<SelectOptionPB> selectedOptions,
|
||||
required List<ChecklistSelectOption> tasks,
|
||||
required double percent,
|
||||
required bool newTask,
|
||||
}) = _ChecklistCellState;
|
||||
|
||||
factory ChecklistCellState.initial(ChecklistCellController cellController) {
|
||||
return const ChecklistCellState(
|
||||
allOptions: [],
|
||||
selectedOptions: [],
|
||||
percent: 0,
|
||||
final cellData = cellController.getCellData(loadIfNotExist: true);
|
||||
|
||||
return ChecklistCellState(
|
||||
tasks: _makeChecklistSelectOptions(cellData),
|
||||
percent: cellData?.percentage ?? 0,
|
||||
newTask: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
ChecklistCellDataPB? data,
|
||||
) {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<ChecklistSelectOption> options = [];
|
||||
final List<SelectOptionPB> allOptions = List.from(data.options);
|
||||
final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList();
|
||||
|
||||
for (final option in allOptions) {
|
||||
options.add(
|
||||
ChecklistSelectOption(selectedOptionIds.contains(option.id), option),
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'checklist_cell_editor_bloc.dart';
|
||||
import 'checklist_cell_bloc.dart';
|
||||
import 'checklist_progress_bar.dart';
|
||||
|
||||
class GridChecklistCellEditor extends StatefulWidget {
|
||||
@ -22,12 +22,11 @@ class GridChecklistCellEditor extends StatefulWidget {
|
||||
const GridChecklistCellEditor({required this.cellController, super.key});
|
||||
|
||||
@override
|
||||
State<GridChecklistCellEditor> createState() =>
|
||||
_GridChecklistCellEditorState();
|
||||
State<GridChecklistCellEditor> createState() => _GridChecklistCellState();
|
||||
}
|
||||
|
||||
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
late ChecklistCellEditorBloc _bloc;
|
||||
class _GridChecklistCellState extends State<GridChecklistCellEditor> {
|
||||
late ChecklistCellBloc _bloc;
|
||||
|
||||
/// Focus node for the new task text field
|
||||
late final FocusNode newTaskFocusNode;
|
||||
@ -45,17 +44,17 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
|
||||
..add(const ChecklistCellEditorEvent.initial());
|
||||
_bloc = ChecklistCellBloc(cellController: widget.cellController)
|
||||
..add(const ChecklistCellEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _bloc,
|
||||
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
child: BlocConsumer<ChecklistCellBloc, ChecklistCellState>(
|
||||
listener: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
if (state.tasks.isEmpty) {
|
||||
newTaskFocusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
@ -65,7 +64,7 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
children: [
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.allOptions.isEmpty
|
||||
child: state.tasks.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
@ -75,10 +74,10 @@ class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
),
|
||||
),
|
||||
ChecklistItemList(
|
||||
options: state.allOptions,
|
||||
options: state.tasks,
|
||||
onUpdateTask: () => newTaskFocusNode.requestFocus(),
|
||||
),
|
||||
if (state.allOptions.isNotEmpty)
|
||||
if (state.tasks.isNotEmpty)
|
||||
const TypeOptionSeparator(spacing: 0.0),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
@ -123,11 +122,15 @@ class _ChecklistItemListState extends State<ChecklistItemList> {
|
||||
|
||||
final itemList = widget.options
|
||||
.mapIndexed(
|
||||
(index, option) => ChecklistItem(
|
||||
option: option,
|
||||
onSubmitted:
|
||||
index == widget.options.length - 1 ? widget.onUpdateTask : null,
|
||||
key: ValueKey(option.data.id),
|
||||
(index, option) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: ChecklistItem(
|
||||
task: option,
|
||||
onSubmitted: index == widget.options.length - 1
|
||||
? widget.onUpdateTask
|
||||
: null,
|
||||
key: ValueKey(option.data.id),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
@ -147,12 +150,14 @@ class _ChecklistItemListState extends State<ChecklistItemList> {
|
||||
/// Represents an existing task
|
||||
@visibleForTesting
|
||||
class ChecklistItem extends StatefulWidget {
|
||||
final ChecklistSelectOption option;
|
||||
final ChecklistSelectOption task;
|
||||
final VoidCallback? onSubmitted;
|
||||
final bool autofocus;
|
||||
const ChecklistItem({
|
||||
required this.option,
|
||||
required this.task,
|
||||
Key? key,
|
||||
this.onSubmitted,
|
||||
this.autofocus = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@ -168,7 +173,7 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController = TextEditingController(text: widget.option.data.name);
|
||||
_textController = TextEditingController(text: widget.task.data.name);
|
||||
_focusNode = FocusNode(
|
||||
onKey: (node, event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
@ -179,72 +184,83 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
if (widget.autofocus) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ChecklistItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.task.data.name != oldWidget.task.data.name &&
|
||||
!_focusNode.hasFocus) {
|
||||
_textController.text = widget.task.data.name;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = FlowySvg(
|
||||
widget.option.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
widget.task.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
);
|
||||
return MouseRegion(
|
||||
onEnter: (event) => setState(() => _isHovered = true),
|
||||
onExit: (event) => setState(() => _isHovered = false),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: Colors.transparent,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
constraints: BoxConstraints(maxHeight: GridSize.popoverItemHeight),
|
||||
decoration: BoxDecoration(
|
||||
color: _isHovered
|
||||
? AFThemeExtension.of(context).lightGreyHover
|
||||
: Colors.transparent,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
FlowyIconButton(
|
||||
width: 32,
|
||||
icon: icon,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.selectTask(widget.task.data),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
focusNode: _focusNode,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: 6.0,
|
||||
bottom: 6.0,
|
||||
left: 2.0,
|
||||
right: _isHovered ? 2.0 : 8.0,
|
||||
),
|
||||
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
||||
),
|
||||
onChanged: _debounceOnChangedText,
|
||||
onSubmitted: (description) {
|
||||
_submitUpdateTaskDescription(description);
|
||||
widget.onSubmitted?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isHovered)
|
||||
FlowyIconButton(
|
||||
width: 32,
|
||||
icon: icon,
|
||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.selectTask(widget.option.data),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.error,
|
||||
onPressed: () => context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.deleteTask(widget.task.data),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
focusNode: _focusNode,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
isCollapsed: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 6.0,
|
||||
horizontal: 2.0,
|
||||
),
|
||||
hintText: LocaleKeys.grid_checklist_taskHint.tr(),
|
||||
),
|
||||
onChanged: _debounceOnChangedText,
|
||||
onSubmitted: (description) {
|
||||
_submitUpdateTaskDescription(description);
|
||||
widget.onSubmitted?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
if (_isHovered)
|
||||
FlowyIconButton(
|
||||
width: 32,
|
||||
icon: const FlowySvg(FlowySvgs.delete_s),
|
||||
hoverColor: Colors.transparent,
|
||||
iconColorOnHover: Theme.of(context).colorScheme.error,
|
||||
onPressed: () => context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.deleteTask(widget.option.data),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -258,10 +274,10 @@ class _ChecklistItemState extends State<ChecklistItem> {
|
||||
}
|
||||
|
||||
void _submitUpdateTaskDescription(String description) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateTaskName(
|
||||
widget.option.data,
|
||||
description,
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.updateTaskName(
|
||||
widget.task.data,
|
||||
description.trim(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -316,8 +332,8 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
if (taskDescription.trim().isNotEmpty) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.newTask(
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.createNewTask(
|
||||
taskDescription.trim(),
|
||||
),
|
||||
);
|
||||
@ -340,11 +356,10 @@ class _NewTaskItemState extends State<NewTaskItem> {
|
||||
fontColor: Theme.of(context).colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
onPressed: () {
|
||||
if (_textEditingController.text.trim().isNotEmpty) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.newTask(
|
||||
_textEditingController.text..trim(),
|
||||
),
|
||||
final text = _textEditingController.text.trim();
|
||||
if (text.isNotEmpty) {
|
||||
context.read<ChecklistCellBloc>().add(
|
||||
ChecklistCellEvent.createNewTask(text),
|
||||
);
|
||||
}
|
||||
widget.focusNode.requestFocus();
|
||||
|
@ -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(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => cell.requestFocus.notify(),
|
||||
child: AccessoryHover(child: cell),
|
||||
child: AccessoryHover(
|
||||
fieldType: widget.cellContext.fieldType,
|
||||
child: cell,
|
||||
),
|
||||
);
|
||||
|
||||
return Container(
|
||||
@ -271,7 +274,8 @@ GridCellStyle? _customCellStyle(FieldType fieldType) {
|
||||
case FieldType.Checklist:
|
||||
return ChecklistCellStyle(
|
||||
placeholder: LocaleKeys.grid_row_textPlaceholder.tr(),
|
||||
cellPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
cellPadding: const EdgeInsets.symmetric(vertical: 6),
|
||||
showTasksInline: true,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return GridNumberCellStyle(
|
||||
|
Reference in New Issue
Block a user