mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: revamp checklist ui (#3380)
* chore: revamp checklist editor ui * chore: checklist progress bar * test: integration tests * fix: flutter analyzer errors * fix: checklist percentage complete
This commit is contained in:
parent
524efc2620
commit
0c6a1d4ae7
@ -437,4 +437,116 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('edit checklist cell', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapGoButton();
|
||||
|
||||
await tester.createNewPageWithName(layout: ViewLayoutPB.Grid);
|
||||
|
||||
const fieldType = FieldType.Checklist;
|
||||
await tester.createField(fieldType, fieldType.name);
|
||||
|
||||
// assert that there is no progress bar in the grid
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
|
||||
// tap on the first checklist cell
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// assert that the checklist editor is shown
|
||||
tester.assertChecklistEditorVisible(visible: true);
|
||||
|
||||
// assert that new task editor is shown
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// create a new task with enter
|
||||
await tester.createNewChecklistTask(name: "task 0", enter: true);
|
||||
|
||||
// assert that the task is displayed
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 0",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// update the task's name
|
||||
await tester.renameChecklistTask(index: 0, name: "task 1");
|
||||
|
||||
// assert that the task's name is updated
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 0,
|
||||
name: "task 1",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// dismiss new task editor
|
||||
await tester.dismissCellEditor();
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: false);
|
||||
|
||||
// dismiss checklist cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progress bar is shown in grid at 0%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0);
|
||||
|
||||
// start editing the first checklist cell again, click on new task button
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: false);
|
||||
await tester.tapChecklistNewTaskButton();
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// create another task with the create button
|
||||
await tester.createNewChecklistTask(name: "task 2", button: true);
|
||||
|
||||
// assert that the task was inserted
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: false,
|
||||
);
|
||||
|
||||
// mark it as complete
|
||||
await tester.checkChecklistTask(index: 1);
|
||||
|
||||
// assert that the task was checked in the editor
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
name: "task 2",
|
||||
isChecked: true,
|
||||
);
|
||||
|
||||
// dismiss checklist editor
|
||||
await tester.dismissCellEditor();
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 50%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 0.5);
|
||||
|
||||
// re-open the cell editor
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// hover over first task and delete it
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// dismiss cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// assert that progressbar is shown in grid at 100%
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: 1);
|
||||
|
||||
// re-open the cell edior
|
||||
await tester.tapChecklistCellInGrid(rowIndex: 0);
|
||||
|
||||
// delete the remaining task
|
||||
await tester.deleteChecklistTask(index: 0);
|
||||
|
||||
// assert that the new task editor is shown
|
||||
tester.assertNewCheckListTaskEditorVisible(visible: true);
|
||||
|
||||
// dismiss the cell editor
|
||||
await tester.dismissCellEditor();
|
||||
|
||||
// check that the progress bar is not viisble
|
||||
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
|
||||
});
|
||||
}
|
||||
|
@ -122,17 +122,17 @@ void main() {
|
||||
}
|
||||
|
||||
// check the checklist cell
|
||||
final List<double> checklistCells = [
|
||||
0.6,
|
||||
0.3,
|
||||
final List<double?> checklistCells = [
|
||||
0.67,
|
||||
0.33,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
];
|
||||
for (final (index, percent) in checklistCells.indexed) {
|
||||
await tester.assertChecklistCellInGrid(
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/board/presentation/board_page.dart';
|
||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.dart';
|
||||
@ -38,6 +39,7 @@ import 'package:appflowy/plugins/database_view/widgets/database_layout_ext.dart'
|
||||
import 'package:appflowy/plugins/database_view/widgets/field/grid_property.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/cells.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_progress_bar.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/date_cell/date_editor.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
@ -243,23 +245,33 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
/// null percent means no progress bar should be found
|
||||
Future<void> assertChecklistCellInGrid({
|
||||
required int rowIndex,
|
||||
required double percent,
|
||||
required double? percent,
|
||||
}) async {
|
||||
final findCell = cellFinder(rowIndex, FieldType.Checklist);
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is ChecklistProgressBar) {
|
||||
return widget.percent == percent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
|
||||
if (percent == null) {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byType(ChecklistProgressBar),
|
||||
);
|
||||
expect(finder, findsNothing);
|
||||
} else {
|
||||
final finder = find.descendant(
|
||||
of: findCell,
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) {
|
||||
if (widget is ChecklistProgressBar) {
|
||||
return widget.percent == percent;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(finder, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> assertDateCellInGrid({
|
||||
@ -450,6 +462,119 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(cell, matcher);
|
||||
}
|
||||
|
||||
Future<void> tapChecklistCellInGrid({required int rowIndex}) async {
|
||||
final findRow = find.byType(GridRow);
|
||||
final findCell = finderForFieldType(FieldType.Checklist);
|
||||
|
||||
final cell = find.descendant(
|
||||
of: findRow.at(rowIndex),
|
||||
matching: findCell,
|
||||
);
|
||||
|
||||
await tapButton(cell);
|
||||
}
|
||||
|
||||
void assertChecklistEditorVisible({required bool visible}) {
|
||||
final editor = find.byType(GridChecklistCellEditor);
|
||||
if (visible) {
|
||||
expect(editor, findsOneWidget);
|
||||
} else {
|
||||
expect(editor, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
void assertNewCheckListTaskEditorVisible({required bool visible}) {
|
||||
final editor = find.byType(NewTaskItem);
|
||||
if (visible) {
|
||||
expect(editor, findsOneWidget);
|
||||
} else {
|
||||
expect(editor, findsNothing);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createNewChecklistTask({
|
||||
required String name,
|
||||
enter = false,
|
||||
button = false,
|
||||
}) async {
|
||||
assert(!(enter && button));
|
||||
final textField = find.descendant(
|
||||
of: find.byType(NewTaskItem),
|
||||
matching: find.byType(TextField),
|
||||
);
|
||||
|
||||
await enterText(textField, name);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
if (enter) {
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
} else {
|
||||
await tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(NewTaskItem),
|
||||
matching: find.byType(FlowyTextButton),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void assertChecklistTaskInEditor({
|
||||
required int index,
|
||||
required String name,
|
||||
required bool isChecked,
|
||||
}) {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
final widget = this.widget<ChecklistItem>(task);
|
||||
assert(
|
||||
widget.option.data.name == name && widget.option.isSelected == isChecked,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> renameChecklistTask({
|
||||
required int index,
|
||||
required String name,
|
||||
}) async {
|
||||
final textField = find
|
||||
.descendant(
|
||||
of: find.byType(ChecklistItem),
|
||||
matching: find.byType(TextField),
|
||||
)
|
||||
.at(index);
|
||||
|
||||
await enterText(textField, name);
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle(const Duration(milliseconds: 300));
|
||||
}
|
||||
|
||||
Future<void> tapChecklistNewTaskButton() async {
|
||||
await tapButton(find.byType(ChecklistNewTaskButton));
|
||||
}
|
||||
|
||||
Future<void> checkChecklistTask({required int index}) async {
|
||||
final button = find.descendant(
|
||||
of: find.byType(ChecklistItem).at(index),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.uncheck_s,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> deleteChecklistTask({required int index}) async {
|
||||
final task = find.byType(ChecklistItem).at(index);
|
||||
|
||||
await startGesture(getCenter(task), kind: PointerDeviceKind.mouse);
|
||||
await pumpAndSettle();
|
||||
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is FlowySvg && widget.svg == FlowySvgs.delete_s,
|
||||
);
|
||||
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-database2/checklist_entities.pb.
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
class ChecklistCellBackendService {
|
||||
final String viewId;
|
||||
@ -52,14 +53,19 @@ class ChecklistCellBackendService {
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> update({
|
||||
Future<Either<Unit, FlowyError>> updateName({
|
||||
required SelectOptionPB option,
|
||||
required name,
|
||||
}) {
|
||||
option.freeze();
|
||||
final newOption = option.rebuild((option) {
|
||||
option.name = name;
|
||||
});
|
||||
final payload = ChecklistCellDataChangesetPB.create()
|
||||
..viewId = viewId
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId
|
||||
..updateOptions.add(option);
|
||||
..updateOptions.add(newOption);
|
||||
|
||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||
}
|
||||
|
@ -32,10 +32,16 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
),
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
child: AppFlowyPopover(
|
||||
margin: EdgeInsets.zero,
|
||||
controller: _popover,
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
constraints: BoxConstraints.loose(const Size(360, 400)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (BuildContext context) {
|
||||
@ -56,8 +56,12 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) =>
|
||||
ChecklistProgressBar(percent: state.percent),
|
||||
builder: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return ChecklistProgressBar(percent: state.percent);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -29,13 +29,23 @@ class ChecklistCardCellBloc
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (data) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: data.options,
|
||||
selectedOptions: data.selectedOptions,
|
||||
percent: data.percentage,
|
||||
),
|
||||
);
|
||||
if (data == null) {
|
||||
emit(
|
||||
const ChecklistCellState(
|
||||
allOptions: [],
|
||||
selectedOptions: [],
|
||||
percent: 0,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: data.options,
|
||||
selectedOptions: data.selectedOptions,
|
||||
percent: data.percentage,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
@ -58,7 +68,7 @@ class ChecklistCardCellBloc
|
||||
_loadOptions();
|
||||
},
|
||||
onCellChanged: (data) {
|
||||
if (!isClosed && data != null) {
|
||||
if (!isClosed) {
|
||||
add(ChecklistCellEvent.didReceiveOptions(data));
|
||||
}
|
||||
},
|
||||
@ -81,7 +91,7 @@ class ChecklistCardCellBloc
|
||||
class ChecklistCellEvent with _$ChecklistCellEvent {
|
||||
const factory ChecklistCellEvent.initial() = _InitialCell;
|
||||
const factory ChecklistCellEvent.didReceiveOptions(
|
||||
ChecklistCellDataPB data,
|
||||
ChecklistCellDataPB? data,
|
||||
) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,23 @@
|
||||
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_popover/appflowy_popover.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/widgets/common/type_option_separator.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../grid/presentation/layout/sizes.dart';
|
||||
import '../../../../grid/presentation/widgets/header/type_option/select_option_editor.dart';
|
||||
import 'checklist_cell_editor_bloc.dart';
|
||||
import 'checklist_progress_bar.dart';
|
||||
|
||||
class GridChecklistCellEditor extends StatefulWidget {
|
||||
final ChecklistCellController cellController;
|
||||
const GridChecklistCellEditor({required this.cellController, Key? key})
|
||||
: super(key: key);
|
||||
const GridChecklistCellEditor({required this.cellController, super.key});
|
||||
|
||||
@override
|
||||
State<GridChecklistCellEditor> createState() =>
|
||||
@ -23,167 +25,367 @@ class GridChecklistCellEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
late ChecklistCellEditorBloc bloc;
|
||||
late PopoverMutex popoverMutex;
|
||||
late ChecklistCellEditorBloc _bloc;
|
||||
|
||||
/// Focus node for the new task text field
|
||||
late final FocusNode newTaskFocusNode;
|
||||
|
||||
/// A flag that determines whether the new task text field is visible
|
||||
bool _isAddingNewTask = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverMutex = PopoverMutex();
|
||||
bloc = ChecklistCellEditorBloc(cellController: widget.cellController);
|
||||
bloc.add(const ChecklistCellEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
super.dispose();
|
||||
newTaskFocusNode = FocusNode();
|
||||
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
|
||||
..add(const ChecklistCellEditorEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
value: _bloc,
|
||||
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
listener: (context, state) {
|
||||
if (state.allOptions.isEmpty) {
|
||||
setState(() => _isAddingNewTask = true);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final List<Widget> slivers = [
|
||||
const SliverChecklistProgressBar(),
|
||||
SliverToBoxAdapter(
|
||||
child: ListView.separated(
|
||||
controller: ScrollController(),
|
||||
shrinkWrap: true,
|
||||
itemCount: state.allOptions.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _ChecklistOptionCell(
|
||||
option: state.allOptions[index],
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ScrollConfiguration(
|
||||
behavior: const ScrollBehavior().copyWith(scrollbars: false),
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: slivers,
|
||||
controller: ScrollController(),
|
||||
physics: StyledScrollPhysics(),
|
||||
),
|
||||
return Focus(
|
||||
onKey: (node, event) {
|
||||
// don't hide new task text field if there are no tasks at all
|
||||
if (state.allOptions.isNotEmpty &&
|
||||
event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
setState(() {
|
||||
_isAddingNewTask = false;
|
||||
});
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
physics: StyledScrollPhysics(),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: state.allOptions.isEmpty
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||
child: ChecklistProgressBar(
|
||||
percent: state.percent,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ChecklistItemList(
|
||||
options: state.allOptions,
|
||||
newTaskFocusNode: newTaskFocusNode,
|
||||
isAddingNewTask: _isAddingNewTask,
|
||||
onUpdateTask: () => setState(() {
|
||||
_isAddingNewTask = true;
|
||||
newTaskFocusNode.requestFocus();
|
||||
}),
|
||||
),
|
||||
const SliverToBoxAdapter(
|
||||
child: TypeOptionSeparator(spacing: 0.0),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: ChecklistNewTaskButton(
|
||||
onTap: () => setState(() => _isAddingNewTask = true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistOptionCell extends StatefulWidget {
|
||||
/// Displays the a list of all the exisiting tasks and an input field to create
|
||||
/// a new task if `isAddingNewTask` is true
|
||||
class ChecklistItemList extends StatefulWidget {
|
||||
final List<ChecklistSelectOption> options;
|
||||
final FocusNode newTaskFocusNode;
|
||||
final bool isAddingNewTask;
|
||||
final VoidCallback onUpdateTask;
|
||||
|
||||
const ChecklistItemList({
|
||||
super.key,
|
||||
required this.options,
|
||||
required this.onUpdateTask,
|
||||
required this.isAddingNewTask,
|
||||
required this.newTaskFocusNode,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ChecklistItemList> createState() => _ChecklistItemListState();
|
||||
}
|
||||
|
||||
class _ChecklistItemListState extends State<ChecklistItemList> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final itemList = [
|
||||
const VSpace(6.0),
|
||||
...widget.options.mapIndexed(
|
||||
(index, option) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: ChecklistItem(
|
||||
option: option,
|
||||
onSubmitted:
|
||||
index == widget.options.length - 1 ? widget.onUpdateTask : null,
|
||||
key: ValueKey(option.data.id),
|
||||
// only allow calling the callback for the last task in the list
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: widget.isAddingNewTask
|
||||
? NewTaskItem(focusNode: widget.newTaskFocusNode)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
const VSpace(6.0),
|
||||
];
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int index) => itemList[index],
|
||||
childCount: itemList.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an existing task
|
||||
@visibleForTesting
|
||||
class ChecklistItem extends StatefulWidget {
|
||||
final ChecklistSelectOption option;
|
||||
final PopoverMutex popoverMutex;
|
||||
const _ChecklistOptionCell({
|
||||
final VoidCallback? onSubmitted;
|
||||
const ChecklistItem({
|
||||
required this.option,
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
this.onSubmitted,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_ChecklistOptionCell> createState() => _ChecklistOptionCellState();
|
||||
State<ChecklistItem> createState() => _ChecklistItemState();
|
||||
}
|
||||
|
||||
class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
|
||||
late PopoverController _popoverController;
|
||||
class _ChecklistItemState extends State<ChecklistItem> {
|
||||
late final TextEditingController _textController;
|
||||
late final FocusNode _focusNode;
|
||||
bool _isHovered = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_popoverController = PopoverController();
|
||||
super.initState();
|
||||
_textController = TextEditingController(text: widget.option.data.name);
|
||||
_focusNode = FocusNode(
|
||||
onKey: (node, event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
node.unfocus();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = widget.option.isSelected
|
||||
? const FlowySvg(
|
||||
FlowySvgs.check_filled_s,
|
||||
blendMode: BlendMode.dst,
|
||||
)
|
||||
: const FlowySvg(FlowySvgs.uncheck_s);
|
||||
return _wrapPopover(
|
||||
SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText(
|
||||
widget.option.data.name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: icon,
|
||||
onTap: () => context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.selectOption(widget.option)),
|
||||
final icon = FlowySvg(
|
||||
widget.option.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: [
|
||||
FlowyIconButton(
|
||||
width: 32,
|
||||
icon: icon,
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.selectTask(widget.option.data),
|
||||
),
|
||||
),
|
||||
),
|
||||
_disclosureButton(),
|
||||
],
|
||||
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(),
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateTaskName(
|
||||
widget.option.data,
|
||||
taskDescription,
|
||||
),
|
||||
);
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _disclosureButton() {
|
||||
return FlowyIconButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
width: 20,
|
||||
onPressed: () => _popoverController.show(),
|
||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.details_s,
|
||||
color: Theme.of(context).iconTheme.color,
|
||||
/// Creates a new task after entering the description and pressing enter.
|
||||
/// This can be cancelled by pressing escape
|
||||
@visibleForTesting
|
||||
class NewTaskItem extends StatefulWidget {
|
||||
final FocusNode focusNode;
|
||||
const NewTaskItem({super.key, required this.focusNode});
|
||||
|
||||
@override
|
||||
State<NewTaskItem> createState() => _NewTaskItemState();
|
||||
}
|
||||
|
||||
class _NewTaskItemState extends State<NewTaskItem> {
|
||||
late final TextEditingController _textEditingController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController = TextEditingController();
|
||||
if (widget.focusNode.canRequestFocus) {
|
||||
widget.focusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const FlowyIconButton(
|
||||
width: 32,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.uncheck_s,
|
||||
blendMode: BlendMode.dst,
|
||||
),
|
||||
hoverColor: Colors.transparent,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
focusNode: widget.focusNode,
|
||||
controller: _textEditingController,
|
||||
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(),
|
||||
),
|
||||
onSubmitted: (taskDescription) {
|
||||
if (taskDescription.trim().isNotEmpty) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.newTask(
|
||||
taskDescription.trim(),
|
||||
),
|
||||
);
|
||||
}
|
||||
_textEditingController.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
FlowyTextButton(
|
||||
LocaleKeys.grid_checklist_submitNewTask.tr(),
|
||||
fontSize: 11,
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
_textEditingController.clear();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _wrapPopover(Widget child) {
|
||||
return AppFlowyPopover(
|
||||
controller: _popoverController,
|
||||
offset: const Offset(8, 0),
|
||||
asBarrier: true,
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
mutex: widget.popoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return SelectOptionTypeOptionEditor(
|
||||
option: widget.option.data,
|
||||
onDeleted: () {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.deleteOption(widget.option.data),
|
||||
);
|
||||
@visibleForTesting
|
||||
class ChecklistNewTaskButton extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
const ChecklistNewTaskButton({super.key, required this.onTap});
|
||||
|
||||
_popoverController.close();
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateOption(updatedOption),
|
||||
);
|
||||
},
|
||||
showOptions: false,
|
||||
autoFocus: false,
|
||||
// Use ValueKey to refresh the UI, otherwise, it will remain the old value.
|
||||
key: ValueKey(
|
||||
widget.option.data.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||
child: SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.grid_checklist_addNew.tr()),
|
||||
margin: const EdgeInsets.all(6),
|
||||
leftIcon: const FlowySvg(FlowySvgs.add_s),
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,13 @@ 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;
|
||||
@ -31,33 +38,31 @@ class ChecklistCellEditorBloc
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (data) {
|
||||
didReceiveTasks: (data) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
allOptions: _makeChecklistSelectOptions(data, state.predicate),
|
||||
percent: data.percentage,
|
||||
allOptions: _makeChecklistSelectOptions(data),
|
||||
percent: data?.percentage ?? 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
newOption: (optionName) {
|
||||
_createOption(optionName);
|
||||
newTask: (optionName) async {
|
||||
await _createOption(optionName);
|
||||
emit(
|
||||
state.copyWith(
|
||||
createOption: Some(optionName),
|
||||
predicate: '',
|
||||
),
|
||||
);
|
||||
},
|
||||
deleteOption: (option) {
|
||||
_deleteOption([option]);
|
||||
deleteTask: (option) async {
|
||||
await _deleteOption([option]);
|
||||
},
|
||||
updateOption: (option) {
|
||||
_updateOption(option);
|
||||
updateTaskName: (option, name) {
|
||||
_updateOption(option, name);
|
||||
},
|
||||
selectOption: (option) async {
|
||||
await _checklistCellService.select(optionId: option.data.id);
|
||||
selectTask: (option) async {
|
||||
await _checklistCellService.select(optionId: option.id);
|
||||
},
|
||||
filterOption: (String predicate) {},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -69,22 +74,21 @@ class ChecklistCellEditorBloc
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _createOption(String name) async {
|
||||
Future<void> _createOption(String name) async {
|
||||
final result = await _checklistCellService.create(name: name);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _deleteOption(List<SelectOptionPB> options) async {
|
||||
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) async {
|
||||
final result = await _checklistCellService.update(
|
||||
option: option,
|
||||
);
|
||||
void _updateOption(SelectOptionPB option, String name) async {
|
||||
final result =
|
||||
await _checklistCellService.updateName(option: option, name: name);
|
||||
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
@ -94,7 +98,7 @@ class ChecklistCellEditorBloc
|
||||
if (isClosed) return;
|
||||
|
||||
return result.fold(
|
||||
(data) => add(ChecklistCellEditorEvent.didReceiveOptions(data)),
|
||||
(data) => add(ChecklistCellEditorEvent.didReceiveTasks(data)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
@ -103,8 +107,8 @@ class ChecklistCellEditorBloc
|
||||
void _startListening() {
|
||||
cellController.startListening(
|
||||
onCellChanged: ((data) {
|
||||
if (!isClosed && data != null) {
|
||||
add(ChecklistCellEditorEvent.didReceiveOptions(data));
|
||||
if (!isClosed) {
|
||||
add(ChecklistCellEditorEvent.didReceiveTasks(data));
|
||||
}
|
||||
}),
|
||||
onCellFieldChanged: () {
|
||||
@ -117,20 +121,19 @@ class ChecklistCellEditorBloc
|
||||
@freezed
|
||||
class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
|
||||
const factory ChecklistCellEditorEvent.initial() = _Initial;
|
||||
const factory ChecklistCellEditorEvent.didReceiveOptions(
|
||||
ChecklistCellDataPB data,
|
||||
) = _DidReceiveOptions;
|
||||
const factory ChecklistCellEditorEvent.newOption(String optionName) =
|
||||
_NewOption;
|
||||
const factory ChecklistCellEditorEvent.selectOption(
|
||||
ChecklistSelectOption option,
|
||||
) = _SelectOption;
|
||||
const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) =
|
||||
_UpdateOption;
|
||||
const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) =
|
||||
_DeleteOption;
|
||||
const factory ChecklistCellEditorEvent.filterOption(String predicate) =
|
||||
_FilterOption;
|
||||
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
|
||||
@ -139,24 +142,21 @@ class ChecklistCellEditorState with _$ChecklistCellEditorState {
|
||||
required List<ChecklistSelectOption> allOptions,
|
||||
required Option<String> createOption,
|
||||
required double percent,
|
||||
required String predicate,
|
||||
}) = _ChecklistCellEditorState;
|
||||
|
||||
factory ChecklistCellEditorState.initial(ChecklistCellController context) {
|
||||
final data = context.getCellData(loadIfNotExist: true);
|
||||
|
||||
return ChecklistCellEditorState(
|
||||
allOptions: _makeChecklistSelectOptions(data, ''),
|
||||
allOptions: _makeChecklistSelectOptions(data),
|
||||
createOption: none(),
|
||||
percent: data?.percentage ?? 0,
|
||||
predicate: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
ChecklistCellDataPB? data,
|
||||
String predicate,
|
||||
) {
|
||||
if (data == null) {
|
||||
return [];
|
||||
@ -164,9 +164,6 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
|
||||
final List<ChecklistSelectOption> options = [];
|
||||
final List<SelectOptionPB> allOptions = List.from(data.options);
|
||||
if (predicate.isNotEmpty) {
|
||||
allOptions.retainWhere((element) => element.name.contains(predicate));
|
||||
}
|
||||
final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList();
|
||||
|
||||
for (final option in allOptions) {
|
||||
@ -177,10 +174,3 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
class ChecklistSelectOption {
|
||||
final bool isSelected;
|
||||
final SelectOptionPB data;
|
||||
|
||||
ChecklistSelectOption(this.isSelected, this.data);
|
||||
}
|
||||
|
@ -1,129 +1,44 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database_view/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/checklist_cell/checklist_cell_editor_bloc.dart';
|
||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
|
||||
class ChecklistProgressBar extends StatelessWidget {
|
||||
class ChecklistProgressBar extends StatefulWidget {
|
||||
final double percent;
|
||||
const ChecklistProgressBar({required this.percent, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LinearPercentIndicator(
|
||||
lineHeight: 10.0,
|
||||
percent: percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: percent < 1.0
|
||||
? SelectOptionColorPB.Purple.toColor(context)
|
||||
: SelectOptionColorPB.Green.toColor(context),
|
||||
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(5),
|
||||
);
|
||||
}
|
||||
State<ChecklistProgressBar> createState() => _ChecklistProgressBarState();
|
||||
}
|
||||
|
||||
class SliverChecklistProgressBar extends StatelessWidget {
|
||||
const SliverChecklistProgressBar({Key? key}) : super(key: key);
|
||||
|
||||
class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _SliverChecklistProgressBarDelegate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverChecklistProgressBarDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
_SliverChecklistProgressBarDelegate();
|
||||
|
||||
double fixHeight = 60;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
double shrinkOffset,
|
||||
bool overlapsContent,
|
||||
) {
|
||||
return const _AutoFocusTextField();
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => fixHeight;
|
||||
|
||||
@override
|
||||
double get minExtent => fixHeight;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class _AutoFocusTextField extends StatefulWidget {
|
||||
const _AutoFocusTextField();
|
||||
|
||||
@override
|
||||
State<_AutoFocusTextField> createState() => _AutoFocusTextFieldState();
|
||||
}
|
||||
|
||||
class _AutoFocusTextFieldState extends State<_AutoFocusTextField> {
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
builder: (context, state) {
|
||||
return BlocListener<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.createOption != current.createOption,
|
||||
listener: (context, state) {
|
||||
if (_focusNode.canRequestFocus) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
color: Theme.of(context).cardColor,
|
||||
child: Padding(
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: Column(
|
||||
children: [
|
||||
FlowyTextField(
|
||||
autoFocus: true,
|
||||
focusNode: _focusNode,
|
||||
autoClearWhenDone: true,
|
||||
submitOnLeave: true,
|
||||
hintText: LocaleKeys.grid_checklist_panelTitle.tr(),
|
||||
onChanged: (text) {
|
||||
context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: (text) {
|
||||
context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.newOption(text));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
child: ChecklistProgressBar(percent: state.percent),
|
||||
),
|
||||
],
|
||||
),
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LinearPercentIndicator(
|
||||
lineHeight: 4.0,
|
||||
percent: widget.percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||
barRadius: const Radius.circular(5),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 36,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: FlowyText.regular(
|
||||
"${(widget.percent * 100).round()}%",
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ import 'dart:collection';
|
||||
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
typedef EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;
|
||||
|
||||
@ -67,50 +66,19 @@ class OverlayEntryContext {
|
||||
);
|
||||
}
|
||||
|
||||
class PopoverMask extends StatefulWidget {
|
||||
class PopoverMask extends StatelessWidget {
|
||||
final void Function() onTap;
|
||||
final void Function()? onExit;
|
||||
final Decoration? decoration;
|
||||
|
||||
const PopoverMask(
|
||||
{Key? key, required this.onTap, this.onExit, this.decoration})
|
||||
const PopoverMask({Key? key, required this.onTap, this.decoration})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _PopoverMaskState();
|
||||
}
|
||||
|
||||
class _PopoverMaskState extends State<PopoverMask> {
|
||||
@override
|
||||
void initState() {
|
||||
HardwareKeyboard.instance.addHandler(_handleGlobalKeyEvent);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
bool _handleGlobalKeyEvent(KeyEvent event) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.escape &&
|
||||
event is KeyDownEvent) {
|
||||
if (widget.onExit != null) {
|
||||
widget.onExit!();
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void deactivate() {
|
||||
HardwareKeyboard.instance.removeHandler(_handleGlobalKeyEvent);
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
decoration: widget.decoration,
|
||||
decoration: decoration,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy_popover/src/layout.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'mask.dart';
|
||||
import 'mutex.dart';
|
||||
|
||||
@ -130,7 +131,6 @@ class PopoverState extends State<Popover> {
|
||||
}
|
||||
_removeRootOverlay();
|
||||
},
|
||||
onExit: () => _removeRootOverlay(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -147,7 +147,17 @@ class PopoverState extends State<Popover> {
|
||||
),
|
||||
);
|
||||
|
||||
return Stack(children: children);
|
||||
return FocusScope(
|
||||
onKey: (node, event) {
|
||||
if (event is RawKeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
_removeRootOverlay();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Stack(children: children),
|
||||
);
|
||||
});
|
||||
_rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
|
||||
}
|
||||
@ -192,9 +202,9 @@ class PopoverState extends State<Popover> {
|
||||
showOverlay();
|
||||
}
|
||||
},
|
||||
child: Listener(
|
||||
child: GestureDetector(
|
||||
child: widget.child,
|
||||
onPointerDown: (PointerDownEvent event) {
|
||||
onTap: () {
|
||||
if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
|
||||
showOverlay();
|
||||
}
|
||||
@ -240,14 +250,17 @@ class PopoverContainer extends StatefulWidget {
|
||||
class PopoverContainerState extends State<PopoverContainer> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomSingleChildLayout(
|
||||
delegate: PopoverLayoutDelegate(
|
||||
direction: widget.direction,
|
||||
link: widget.popoverLink,
|
||||
offset: widget.offset,
|
||||
windowPadding: widget.windowPadding,
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
child: CustomSingleChildLayout(
|
||||
delegate: PopoverLayoutDelegate(
|
||||
direction: widget.direction,
|
||||
link: widget.popoverLink,
|
||||
offset: widget.offset,
|
||||
windowPadding: widget.windowPadding,
|
||||
),
|
||||
child: widget.popupBuilder(context),
|
||||
),
|
||||
child: widget.popupBuilder(context),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -386,7 +386,7 @@
|
||||
"searchOption": "ابحث عن خيار"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "أضف عنصرًا"
|
||||
"addNew": "أضف عنصرًا"
|
||||
},
|
||||
"menuName": "شبكة",
|
||||
"referencedGridPrefix": "نظرا ل",
|
||||
|
@ -402,7 +402,7 @@
|
||||
"searchOption": "Cerca una opció"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Afegeix un element"
|
||||
"addNew": "Afegeix un element"
|
||||
},
|
||||
"menuName": "Quadrícula",
|
||||
"referencedGridPrefix": "Vista de"
|
||||
|
@ -411,7 +411,7 @@
|
||||
"searchOption": "Suchen Sie nach einer Option"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Fügen Sie einen Artikel hinzu"
|
||||
"addNew": "Fügen Sie einen Artikel hinzu"
|
||||
},
|
||||
"menuName": "Netz",
|
||||
"referencedGridPrefix": "Sicht von"
|
||||
|
@ -477,7 +477,9 @@
|
||||
"orSelectOne": "Or select an option"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Add an item"
|
||||
"taskHint": "Task description",
|
||||
"addNew": "Add a new task",
|
||||
"submitNewTask": "Create"
|
||||
},
|
||||
"menuName": "Grid",
|
||||
"referencedGridPrefix": "View of"
|
||||
|
@ -377,7 +377,7 @@
|
||||
"addSort": "Agregar clasificación"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Agregar un elemento"
|
||||
"addNew": "Agregar un elemento"
|
||||
},
|
||||
"referencedGridPrefix": "Vista de"
|
||||
},
|
||||
|
@ -387,7 +387,7 @@
|
||||
"searchOption": "Aukera bat bilatu"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Gehitu elementu bat"
|
||||
"addNew": "Gehitu elementu bat"
|
||||
},
|
||||
"menuName": "Sareta",
|
||||
"deleteView": "Ziur ikuspegi hau ezabatu nahi duzula?",
|
||||
|
@ -439,7 +439,7 @@
|
||||
"searchOption": "جستجوی یک گزینه"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "یک مورد اضافه کنید"
|
||||
"addNew": "یک مورد اضافه کنید"
|
||||
},
|
||||
"menuName": "شبکهای",
|
||||
"referencedGridPrefix": "نمایش"
|
||||
|
@ -402,7 +402,7 @@
|
||||
"searchOption": "Rechercher une option"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Ajouter un article"
|
||||
"addNew": "Ajouter un article"
|
||||
},
|
||||
"menuName": "Grille",
|
||||
"referencedGridPrefix": "Vue"
|
||||
|
@ -381,7 +381,7 @@
|
||||
"addSort": "Ajouter un tri"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Ajouter un élément"
|
||||
"addNew": "Ajouter un élément"
|
||||
},
|
||||
"referencedGridPrefix": "Vue"
|
||||
},
|
||||
|
@ -402,7 +402,7 @@
|
||||
"searchOption": "Keressen egy lehetőséget"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Adjon hozzá egy elemet"
|
||||
"addNew": "Adjon hozzá egy elemet"
|
||||
},
|
||||
"menuName": "Rács",
|
||||
"referencedGridPrefix": "Nézet"
|
||||
|
@ -377,7 +377,7 @@
|
||||
"addSort": "Tambahkan semacam"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Tambahkan item"
|
||||
"addNew": "Tambahkan item"
|
||||
},
|
||||
"referencedGridPrefix": "Pemandangan dari"
|
||||
},
|
||||
|
@ -370,7 +370,7 @@
|
||||
"searchOption": "Cerca un'opzione"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Aggiungi un elemento"
|
||||
"addNew": "Aggiungi un elemento"
|
||||
},
|
||||
"referencedGridPrefix": "Vista di"
|
||||
},
|
||||
|
@ -370,7 +370,7 @@
|
||||
"addSort": "並べ替えの追加"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "アイテムを追加する"
|
||||
"addNew": "アイテムを追加する"
|
||||
},
|
||||
"menuName": "グリッド",
|
||||
"referencedGridPrefix": "のビュー"
|
||||
|
@ -383,7 +383,7 @@
|
||||
"addSort": "정렬 추가"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "항목 추가"
|
||||
"addNew": "항목 추가"
|
||||
},
|
||||
"referencedGridPrefix": "관점"
|
||||
},
|
||||
|
@ -402,7 +402,7 @@
|
||||
"searchOption": "Wyszukaj opcję"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Dodaj element"
|
||||
"addNew": "Dodaj element"
|
||||
},
|
||||
"menuName": "Siatka",
|
||||
"referencedGridPrefix": "Widok"
|
||||
|
@ -438,7 +438,7 @@
|
||||
"panelTitle": "Selecione uma opção ou crie uma"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Adicionar um item"
|
||||
"addNew": "Adicionar um item"
|
||||
},
|
||||
"menuName": "Grade",
|
||||
"deleteView": "Tem certeza de que deseja excluir esta visualização?",
|
||||
|
@ -455,7 +455,7 @@
|
||||
"searchOption": "Pesquise uma opção"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Adicionar um item"
|
||||
"addNew": "Adicionar um item"
|
||||
},
|
||||
"menuName": "Grade",
|
||||
"referencedGridPrefix": "Vista de"
|
||||
|
@ -393,7 +393,7 @@
|
||||
"searchOption": "Поиск"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Добавить элемент"
|
||||
"addNew": "Добавить элемент"
|
||||
},
|
||||
"menuName": "Сетка",
|
||||
"referencedGridPrefix": "Просмотр",
|
||||
|
@ -381,7 +381,7 @@
|
||||
"addSort": "Lägg till sortering"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Lägg till ett objekt"
|
||||
"addNew": "Lägg till ett objekt"
|
||||
},
|
||||
"referencedGridPrefix": "Utsikt över"
|
||||
},
|
||||
|
@ -402,7 +402,7 @@
|
||||
"searchOption": "Bir seçenek arayın"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "öğe ekle"
|
||||
"addNew": "öğe ekle"
|
||||
},
|
||||
"menuName": "Kafes",
|
||||
"referencedGridPrefix": "görünümü"
|
||||
|
@ -397,7 +397,7 @@
|
||||
"searchOption": "搜索标签"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "添加项"
|
||||
"addNew": "添加项"
|
||||
},
|
||||
"menuName": "网格",
|
||||
"referencedGridPrefix": "视图"
|
||||
|
@ -386,7 +386,7 @@
|
||||
"searchOption": "搜尋選項"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "新增物件"
|
||||
"addNew": "新增物件"
|
||||
},
|
||||
"menuName": "網格",
|
||||
"deleteView": "您確定要刪除該視圖嗎?",
|
||||
|
@ -42,7 +42,7 @@ impl ChecklistCellData {
|
||||
if total_options == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
((selected_options as f64) / (total_options as f64) * 10.0).trunc() / 10.0
|
||||
((selected_options as f64) / (total_options as f64) * 100.0).round() / 100.0
|
||||
}
|
||||
|
||||
pub fn from_options(options: Vec<String>) -> Self {
|
||||
|
@ -653,7 +653,7 @@ async fn update_checklist_cell_test() {
|
||||
|
||||
assert_eq!(cell.options.len(), 3);
|
||||
assert_eq!(cell.selected_options.len(), 2);
|
||||
assert_eq!(cell.percentage, 0.6);
|
||||
assert_eq!(cell.percentage, 0.67);
|
||||
}
|
||||
|
||||
// The number of groups should be 0 if there is no group by field in grid
|
||||
|
Loading…
Reference in New Issue
Block a user