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();
|
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
|
// check the checklist cell
|
||||||
final List<double> checklistCells = [
|
final List<double?> checklistCells = [
|
||||||
0.6,
|
0.67,
|
||||||
0.3,
|
0.33,
|
||||||
1.0,
|
1.0,
|
||||||
0.0,
|
null,
|
||||||
0.0,
|
null,
|
||||||
0.0,
|
null,
|
||||||
0.0,
|
null,
|
||||||
0.0,
|
null,
|
||||||
0.0,
|
null,
|
||||||
0.0,
|
null,
|
||||||
];
|
];
|
||||||
for (final (index, percent) in checklistCells.indexed) {
|
for (final (index, percent) in checklistCells.indexed) {
|
||||||
await tester.assertChecklistCellInGrid(
|
await tester.assertChecklistCellInGrid(
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||||
import 'package:appflowy/generated/locale_keys.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/board/presentation/board_page.dart';
|
||||||
import 'package:appflowy/plugins/database_view/calendar/application/calendar_bloc.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/field/grid_property.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/accessory/cell_accessory.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/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/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/date_cell/date_editor.dart';
|
||||||
import 'package:appflowy/plugins/database_view/widgets/row/cells/select_option_cell/extension.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({
|
Future<void> assertChecklistCellInGrid({
|
||||||
required int rowIndex,
|
required int rowIndex,
|
||||||
required double percent,
|
required double? percent,
|
||||||
}) async {
|
}) async {
|
||||||
final findCell = cellFinder(rowIndex, FieldType.Checklist);
|
final findCell = cellFinder(rowIndex, FieldType.Checklist);
|
||||||
final finder = find.descendant(
|
|
||||||
of: findCell,
|
if (percent == null) {
|
||||||
matching: find.byWidgetPredicate(
|
final finder = find.descendant(
|
||||||
(widget) {
|
of: findCell,
|
||||||
if (widget is ChecklistProgressBar) {
|
matching: find.byType(ChecklistProgressBar),
|
||||||
return widget.percent == percent;
|
);
|
||||||
}
|
expect(finder, findsNothing);
|
||||||
return false;
|
} else {
|
||||||
},
|
final finder = find.descendant(
|
||||||
),
|
of: findCell,
|
||||||
);
|
matching: find.byWidgetPredicate(
|
||||||
expect(finder, findsOneWidget);
|
(widget) {
|
||||||
|
if (widget is ChecklistProgressBar) {
|
||||||
|
return widget.percent == percent;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(finder, findsOneWidget);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> assertDateCellInGrid({
|
Future<void> assertDateCellInGrid({
|
||||||
@ -450,6 +462,119 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
|||||||
expect(cell, matcher);
|
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 {
|
Future<void> openFirstRowDetailPage() async {
|
||||||
await hoverOnFirstRowOfGrid();
|
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-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';
|
||||||
import 'package:dartz/dartz.dart';
|
import 'package:dartz/dartz.dart';
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
class ChecklistCellBackendService {
|
class ChecklistCellBackendService {
|
||||||
final String viewId;
|
final String viewId;
|
||||||
@ -52,14 +53,19 @@ class ChecklistCellBackendService {
|
|||||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Either<Unit, FlowyError>> update({
|
Future<Either<Unit, FlowyError>> updateName({
|
||||||
required SelectOptionPB option,
|
required SelectOptionPB option,
|
||||||
|
required name,
|
||||||
}) {
|
}) {
|
||||||
|
option.freeze();
|
||||||
|
final newOption = option.rebuild((option) {
|
||||||
|
option.name = name;
|
||||||
|
});
|
||||||
final payload = ChecklistCellDataChangesetPB.create()
|
final payload = ChecklistCellDataChangesetPB.create()
|
||||||
..viewId = viewId
|
..viewId = viewId
|
||||||
..fieldId = fieldId
|
..fieldId = fieldId
|
||||||
..rowId = rowId
|
..rowId = rowId
|
||||||
..updateOptions.add(option);
|
..updateOptions.add(newOption);
|
||||||
|
|
||||||
return DatabaseEventUpdateChecklistCell(payload).send();
|
return DatabaseEventUpdateChecklistCell(payload).send();
|
||||||
}
|
}
|
||||||
|
@ -32,10 +32,16 @@ class _ChecklistCardCellState extends State<ChecklistCardCell> {
|
|||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _cellBloc,
|
value: _cellBloc,
|
||||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||||
builder: (context, state) => Padding(
|
builder: (context, state) {
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
if (state.allOptions.isEmpty) {
|
||||||
child: ChecklistProgressBar(percent: state.percent),
|
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(
|
child: AppFlowyPopover(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
controller: _popover,
|
controller: _popover,
|
||||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
constraints: BoxConstraints.loose(const Size(360, 400)),
|
||||||
direction: PopoverDirection.bottomWithLeftAligned,
|
direction: PopoverDirection.bottomWithLeftAligned,
|
||||||
triggerActions: PopoverTriggerFlags.none,
|
triggerActions: PopoverTriggerFlags.none,
|
||||||
popupBuilder: (BuildContext context) {
|
popupBuilder: (BuildContext context) {
|
||||||
@ -56,8 +56,12 @@ class GridChecklistCellState extends GridCellState<GridChecklistCell> {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: GridSize.cellContentInsets,
|
padding: GridSize.cellContentInsets,
|
||||||
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
child: BlocBuilder<ChecklistCardCellBloc, ChecklistCellState>(
|
||||||
builder: (context, state) =>
|
builder: (context, state) {
|
||||||
ChecklistProgressBar(percent: state.percent),
|
if (state.allOptions.isEmpty) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
return ChecklistProgressBar(percent: state.percent);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -29,13 +29,23 @@ class ChecklistCardCellBloc
|
|||||||
_loadOptions();
|
_loadOptions();
|
||||||
},
|
},
|
||||||
didReceiveOptions: (data) {
|
didReceiveOptions: (data) {
|
||||||
emit(
|
if (data == null) {
|
||||||
state.copyWith(
|
emit(
|
||||||
allOptions: data.options,
|
const ChecklistCellState(
|
||||||
selectedOptions: data.selectedOptions,
|
allOptions: [],
|
||||||
percent: data.percentage,
|
selectedOptions: [],
|
||||||
),
|
percent: 0,
|
||||||
);
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
allOptions: data.options,
|
||||||
|
selectedOptions: data.selectedOptions,
|
||||||
|
percent: data.percentage,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -58,7 +68,7 @@ class ChecklistCardCellBloc
|
|||||||
_loadOptions();
|
_loadOptions();
|
||||||
},
|
},
|
||||||
onCellChanged: (data) {
|
onCellChanged: (data) {
|
||||||
if (!isClosed && data != null) {
|
if (!isClosed) {
|
||||||
add(ChecklistCellEvent.didReceiveOptions(data));
|
add(ChecklistCellEvent.didReceiveOptions(data));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -81,7 +91,7 @@ class ChecklistCardCellBloc
|
|||||||
class ChecklistCellEvent with _$ChecklistCellEvent {
|
class ChecklistCellEvent with _$ChecklistCellEvent {
|
||||||
const factory ChecklistCellEvent.initial() = _InitialCell;
|
const factory ChecklistCellEvent.initial() = _InitialCell;
|
||||||
const factory ChecklistCellEvent.didReceiveOptions(
|
const factory ChecklistCellEvent.didReceiveOptions(
|
||||||
ChecklistCellDataPB data,
|
ChecklistCellDataPB? data,
|
||||||
) = _DidReceiveCellUpdate;
|
) = _DidReceiveCellUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
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_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/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_cell_editor_bloc.dart';
|
||||||
import 'checklist_progress_bar.dart';
|
import 'checklist_progress_bar.dart';
|
||||||
|
|
||||||
class GridChecklistCellEditor extends StatefulWidget {
|
class GridChecklistCellEditor extends StatefulWidget {
|
||||||
final ChecklistCellController cellController;
|
final ChecklistCellController cellController;
|
||||||
const GridChecklistCellEditor({required this.cellController, Key? key})
|
const GridChecklistCellEditor({required this.cellController, super.key});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GridChecklistCellEditor> createState() =>
|
State<GridChecklistCellEditor> createState() =>
|
||||||
@ -23,167 +25,367 @@ class GridChecklistCellEditor extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||||
late ChecklistCellEditorBloc bloc;
|
late ChecklistCellEditorBloc _bloc;
|
||||||
late PopoverMutex popoverMutex;
|
|
||||||
|
/// 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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
popoverMutex = PopoverMutex();
|
|
||||||
bloc = ChecklistCellEditorBloc(cellController: widget.cellController);
|
|
||||||
bloc.add(const ChecklistCellEditorEvent.initial());
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
newTaskFocusNode = FocusNode();
|
||||||
|
_bloc = ChecklistCellEditorBloc(cellController: widget.cellController)
|
||||||
@override
|
..add(const ChecklistCellEditorEvent.initial());
|
||||||
void dispose() {
|
|
||||||
bloc.close();
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: bloc,
|
value: _bloc,
|
||||||
child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
child: BlocConsumer<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.allOptions.isEmpty) {
|
||||||
|
setState(() => _isAddingNewTask = true);
|
||||||
|
}
|
||||||
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final List<Widget> slivers = [
|
return Focus(
|
||||||
const SliverChecklistProgressBar(),
|
onKey: (node, event) {
|
||||||
SliverToBoxAdapter(
|
// don't hide new task text field if there are no tasks at all
|
||||||
child: ListView.separated(
|
if (state.allOptions.isNotEmpty &&
|
||||||
controller: ScrollController(),
|
event is RawKeyDownEvent &&
|
||||||
shrinkWrap: true,
|
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||||
itemCount: state.allOptions.length,
|
setState(() {
|
||||||
itemBuilder: (BuildContext context, int index) {
|
_isAddingNewTask = false;
|
||||||
return _ChecklistOptionCell(
|
});
|
||||||
option: state.allOptions[index],
|
return KeyEventResult.handled;
|
||||||
popoverMutex: popoverMutex,
|
}
|
||||||
);
|
return KeyEventResult.ignored;
|
||||||
},
|
},
|
||||||
separatorBuilder: (BuildContext context, int index) {
|
child: CustomScrollView(
|
||||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
shrinkWrap: true,
|
||||||
},
|
physics: StyledScrollPhysics(),
|
||||||
),
|
slivers: [
|
||||||
),
|
SliverToBoxAdapter(
|
||||||
];
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
return Padding(
|
child: state.allOptions.isEmpty
|
||||||
padding: const EdgeInsets.all(8.0),
|
? const SizedBox.shrink()
|
||||||
child: ScrollConfiguration(
|
: Padding(
|
||||||
behavior: const ScrollBehavior().copyWith(scrollbars: false),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 4),
|
||||||
child: CustomScrollView(
|
child: ChecklistProgressBar(
|
||||||
shrinkWrap: true,
|
percent: state.percent,
|
||||||
slivers: slivers,
|
),
|
||||||
controller: ScrollController(),
|
),
|
||||||
physics: StyledScrollPhysics(),
|
),
|
||||||
),
|
),
|
||||||
|
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 ChecklistSelectOption option;
|
||||||
final PopoverMutex popoverMutex;
|
final VoidCallback? onSubmitted;
|
||||||
const _ChecklistOptionCell({
|
const ChecklistItem({
|
||||||
required this.option,
|
required this.option,
|
||||||
required this.popoverMutex,
|
|
||||||
Key? key,
|
Key? key,
|
||||||
|
this.onSubmitted,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_ChecklistOptionCell> createState() => _ChecklistOptionCellState();
|
State<ChecklistItem> createState() => _ChecklistItemState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
|
class _ChecklistItemState extends State<ChecklistItem> {
|
||||||
late PopoverController _popoverController;
|
late final TextEditingController _textController;
|
||||||
|
late final FocusNode _focusNode;
|
||||||
|
bool _isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_popoverController = PopoverController();
|
|
||||||
super.initState();
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final icon = widget.option.isSelected
|
final icon = FlowySvg(
|
||||||
? const FlowySvg(
|
widget.option.isSelected ? FlowySvgs.check_filled_s : FlowySvgs.uncheck_s,
|
||||||
FlowySvgs.check_filled_s,
|
blendMode: BlendMode.dst,
|
||||||
blendMode: BlendMode.dst,
|
);
|
||||||
)
|
return MouseRegion(
|
||||||
: const FlowySvg(FlowySvgs.uncheck_s);
|
onEnter: (event) => setState(() => _isHovered = true),
|
||||||
return _wrapPopover(
|
onExit: (event) => setState(() => _isHovered = false),
|
||||||
SizedBox(
|
child: Container(
|
||||||
height: GridSize.popoverItemHeight,
|
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||||
child: Row(
|
constraints: BoxConstraints(minHeight: GridSize.popoverItemHeight),
|
||||||
children: [
|
child: DecoratedBox(
|
||||||
Expanded(
|
decoration: BoxDecoration(
|
||||||
child: FlowyButton(
|
color: _isHovered
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
? AFThemeExtension.of(context).lightGreyHover
|
||||||
text: FlowyText(
|
: Colors.transparent,
|
||||||
widget.option.data.name,
|
borderRadius: Corners.s6Border,
|
||||||
color: AFThemeExtension.of(context).textColor,
|
),
|
||||||
),
|
child: Row(
|
||||||
leftIcon: icon,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
onTap: () => context
|
children: [
|
||||||
.read<ChecklistCellEditorBloc>()
|
FlowyIconButton(
|
||||||
.add(ChecklistCellEditorEvent.selectOption(widget.option)),
|
width: 32,
|
||||||
|
icon: icon,
|
||||||
|
hoverColor: Colors.transparent,
|
||||||
|
onPressed: () => context.read<ChecklistCellEditorBloc>().add(
|
||||||
|
ChecklistCellEditorEvent.selectTask(widget.option.data),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
_disclosureButton(),
|
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() {
|
/// Creates a new task after entering the description and pressing enter.
|
||||||
return FlowyIconButton(
|
/// This can be cancelled by pressing escape
|
||||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
@visibleForTesting
|
||||||
width: 20,
|
class NewTaskItem extends StatefulWidget {
|
||||||
onPressed: () => _popoverController.show(),
|
final FocusNode focusNode;
|
||||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
const NewTaskItem({super.key, required this.focusNode});
|
||||||
icon: FlowySvg(
|
|
||||||
FlowySvgs.details_s,
|
@override
|
||||||
color: Theme.of(context).iconTheme.color,
|
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) {
|
@visibleForTesting
|
||||||
return AppFlowyPopover(
|
class ChecklistNewTaskButton extends StatelessWidget {
|
||||||
controller: _popoverController,
|
final VoidCallback onTap;
|
||||||
offset: const Offset(8, 0),
|
const ChecklistNewTaskButton({super.key, required this.onTap});
|
||||||
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),
|
|
||||||
);
|
|
||||||
|
|
||||||
_popoverController.close();
|
@override
|
||||||
},
|
Widget build(BuildContext context) {
|
||||||
onUpdated: (updatedOption) {
|
return Padding(
|
||||||
context.read<ChecklistCellEditorBloc>().add(
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
|
||||||
ChecklistCellEditorEvent.updateOption(updatedOption),
|
child: SizedBox(
|
||||||
);
|
height: 30,
|
||||||
},
|
child: FlowyButton(
|
||||||
showOptions: false,
|
text: FlowyText.medium(LocaleKeys.grid_checklist_addNew.tr()),
|
||||||
autoFocus: false,
|
margin: const EdgeInsets.all(6),
|
||||||
// Use ValueKey to refresh the UI, otherwise, it will remain the old value.
|
leftIcon: const FlowySvg(FlowySvgs.add_s),
|
||||||
key: ValueKey(
|
onTap: onTap,
|
||||||
widget.option.data.id,
|
),
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,13 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
|
|
||||||
part 'checklist_cell_editor_bloc.freezed.dart';
|
part 'checklist_cell_editor_bloc.freezed.dart';
|
||||||
|
|
||||||
|
class ChecklistSelectOption {
|
||||||
|
final bool isSelected;
|
||||||
|
final SelectOptionPB data;
|
||||||
|
|
||||||
|
ChecklistSelectOption(this.isSelected, this.data);
|
||||||
|
}
|
||||||
|
|
||||||
class ChecklistCellEditorBloc
|
class ChecklistCellEditorBloc
|
||||||
extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
|
extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
|
||||||
final ChecklistCellBackendService _checklistCellService;
|
final ChecklistCellBackendService _checklistCellService;
|
||||||
@ -31,33 +38,31 @@ class ChecklistCellEditorBloc
|
|||||||
_startListening();
|
_startListening();
|
||||||
_loadOptions();
|
_loadOptions();
|
||||||
},
|
},
|
||||||
didReceiveOptions: (data) {
|
didReceiveTasks: (data) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
allOptions: _makeChecklistSelectOptions(data, state.predicate),
|
allOptions: _makeChecklistSelectOptions(data),
|
||||||
percent: data.percentage,
|
percent: data?.percentage ?? 0,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
newOption: (optionName) {
|
newTask: (optionName) async {
|
||||||
_createOption(optionName);
|
await _createOption(optionName);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
createOption: Some(optionName),
|
createOption: Some(optionName),
|
||||||
predicate: '',
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
deleteOption: (option) {
|
deleteTask: (option) async {
|
||||||
_deleteOption([option]);
|
await _deleteOption([option]);
|
||||||
},
|
},
|
||||||
updateOption: (option) {
|
updateTaskName: (option, name) {
|
||||||
_updateOption(option);
|
_updateOption(option, name);
|
||||||
},
|
},
|
||||||
selectOption: (option) async {
|
selectTask: (option) async {
|
||||||
await _checklistCellService.select(optionId: option.data.id);
|
await _checklistCellService.select(optionId: option.id);
|
||||||
},
|
},
|
||||||
filterOption: (String predicate) {},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -69,22 +74,21 @@ class ChecklistCellEditorBloc
|
|||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _createOption(String name) async {
|
Future<void> _createOption(String name) async {
|
||||||
final result = await _checklistCellService.create(name: name);
|
final result = await _checklistCellService.create(name: name);
|
||||||
result.fold((l) => {}, (err) => Log.error(err));
|
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(
|
final result = await _checklistCellService.delete(
|
||||||
optionIds: options.map((e) => e.id).toList(),
|
optionIds: options.map((e) => e.id).toList(),
|
||||||
);
|
);
|
||||||
result.fold((l) => null, (err) => Log.error(err));
|
result.fold((l) => null, (err) => Log.error(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateOption(SelectOptionPB option) async {
|
void _updateOption(SelectOptionPB option, String name) async {
|
||||||
final result = await _checklistCellService.update(
|
final result =
|
||||||
option: option,
|
await _checklistCellService.updateName(option: option, name: name);
|
||||||
);
|
|
||||||
|
|
||||||
result.fold((l) => null, (err) => Log.error(err));
|
result.fold((l) => null, (err) => Log.error(err));
|
||||||
}
|
}
|
||||||
@ -94,7 +98,7 @@ class ChecklistCellEditorBloc
|
|||||||
if (isClosed) return;
|
if (isClosed) return;
|
||||||
|
|
||||||
return result.fold(
|
return result.fold(
|
||||||
(data) => add(ChecklistCellEditorEvent.didReceiveOptions(data)),
|
(data) => add(ChecklistCellEditorEvent.didReceiveTasks(data)),
|
||||||
(err) => Log.error(err),
|
(err) => Log.error(err),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -103,8 +107,8 @@ class ChecklistCellEditorBloc
|
|||||||
void _startListening() {
|
void _startListening() {
|
||||||
cellController.startListening(
|
cellController.startListening(
|
||||||
onCellChanged: ((data) {
|
onCellChanged: ((data) {
|
||||||
if (!isClosed && data != null) {
|
if (!isClosed) {
|
||||||
add(ChecklistCellEditorEvent.didReceiveOptions(data));
|
add(ChecklistCellEditorEvent.didReceiveTasks(data));
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
onCellFieldChanged: () {
|
onCellFieldChanged: () {
|
||||||
@ -117,20 +121,19 @@ class ChecklistCellEditorBloc
|
|||||||
@freezed
|
@freezed
|
||||||
class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
|
class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
|
||||||
const factory ChecklistCellEditorEvent.initial() = _Initial;
|
const factory ChecklistCellEditorEvent.initial() = _Initial;
|
||||||
const factory ChecklistCellEditorEvent.didReceiveOptions(
|
const factory ChecklistCellEditorEvent.didReceiveTasks(
|
||||||
ChecklistCellDataPB data,
|
ChecklistCellDataPB? data,
|
||||||
) = _DidReceiveOptions;
|
) = _DidReceiveTasks;
|
||||||
const factory ChecklistCellEditorEvent.newOption(String optionName) =
|
const factory ChecklistCellEditorEvent.newTask(String taskName) = _NewOption;
|
||||||
_NewOption;
|
const factory ChecklistCellEditorEvent.selectTask(
|
||||||
const factory ChecklistCellEditorEvent.selectOption(
|
SelectOptionPB option,
|
||||||
ChecklistSelectOption option,
|
) = _SelectTask;
|
||||||
) = _SelectOption;
|
const factory ChecklistCellEditorEvent.updateTaskName(
|
||||||
const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) =
|
SelectOptionPB option,
|
||||||
_UpdateOption;
|
String name,
|
||||||
const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) =
|
) = _UpdateTaskName;
|
||||||
_DeleteOption;
|
const factory ChecklistCellEditorEvent.deleteTask(SelectOptionPB option) =
|
||||||
const factory ChecklistCellEditorEvent.filterOption(String predicate) =
|
_DeleteTask;
|
||||||
_FilterOption;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
@ -139,24 +142,21 @@ class ChecklistCellEditorState with _$ChecklistCellEditorState {
|
|||||||
required List<ChecklistSelectOption> allOptions,
|
required List<ChecklistSelectOption> allOptions,
|
||||||
required Option<String> createOption,
|
required Option<String> createOption,
|
||||||
required double percent,
|
required double percent,
|
||||||
required String predicate,
|
|
||||||
}) = _ChecklistCellEditorState;
|
}) = _ChecklistCellEditorState;
|
||||||
|
|
||||||
factory ChecklistCellEditorState.initial(ChecklistCellController context) {
|
factory ChecklistCellEditorState.initial(ChecklistCellController context) {
|
||||||
final data = context.getCellData(loadIfNotExist: true);
|
final data = context.getCellData(loadIfNotExist: true);
|
||||||
|
|
||||||
return ChecklistCellEditorState(
|
return ChecklistCellEditorState(
|
||||||
allOptions: _makeChecklistSelectOptions(data, ''),
|
allOptions: _makeChecklistSelectOptions(data),
|
||||||
createOption: none(),
|
createOption: none(),
|
||||||
percent: data?.percentage ?? 0,
|
percent: data?.percentage ?? 0,
|
||||||
predicate: '',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||||
ChecklistCellDataPB? data,
|
ChecklistCellDataPB? data,
|
||||||
String predicate,
|
|
||||||
) {
|
) {
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
return [];
|
return [];
|
||||||
@ -164,9 +164,6 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
|||||||
|
|
||||||
final List<ChecklistSelectOption> options = [];
|
final List<ChecklistSelectOption> options = [];
|
||||||
final List<SelectOptionPB> allOptions = List.from(data.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();
|
final selectedOptionIds = data.selectedOptions.map((e) => e.id).toList();
|
||||||
|
|
||||||
for (final option in allOptions) {
|
for (final option in allOptions) {
|
||||||
@ -177,10 +174,3 @@ List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
|||||||
|
|
||||||
return options;
|
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/theme_extension.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:percent_indicator/percent_indicator.dart';
|
import 'package:percent_indicator/percent_indicator.dart';
|
||||||
|
|
||||||
class ChecklistProgressBar extends StatelessWidget {
|
class ChecklistProgressBar extends StatefulWidget {
|
||||||
final double percent;
|
final double percent;
|
||||||
const ChecklistProgressBar({required this.percent, Key? key})
|
const ChecklistProgressBar({required this.percent, Key? key})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
State<ChecklistProgressBar> createState() => _ChecklistProgressBarState();
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SliverChecklistProgressBar extends StatelessWidget {
|
class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
|
||||||
const SliverChecklistProgressBar({Key? key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverPersistentHeader(
|
return Row(
|
||||||
pinned: true,
|
children: [
|
||||||
delegate: _SliverChecklistProgressBarDelegate(),
|
Expanded(
|
||||||
);
|
child: LinearPercentIndicator(
|
||||||
}
|
lineHeight: 4.0,
|
||||||
}
|
percent: widget.percent,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
class _SliverChecklistProgressBarDelegate
|
progressColor: Theme.of(context).colorScheme.primary,
|
||||||
extends SliverPersistentHeaderDelegate {
|
backgroundColor: AFThemeExtension.of(context).progressBarBGColor,
|
||||||
_SliverChecklistProgressBarDelegate();
|
barRadius: const Radius.circular(5),
|
||||||
|
),
|
||||||
double fixHeight = 60;
|
),
|
||||||
|
SizedBox(
|
||||||
@override
|
width: 36,
|
||||||
Widget build(
|
child: Align(
|
||||||
BuildContext context,
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
double shrinkOffset,
|
child: FlowyText.regular(
|
||||||
bool overlapsContent,
|
"${(widget.percent * 100).round()}%",
|
||||||
) {
|
fontSize: 11,
|
||||||
return const _AutoFocusTextField();
|
color: Theme.of(context).hintColor,
|
||||||
}
|
|
||||||
|
|
||||||
@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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
typedef EntryMap = LinkedHashMap<PopoverState, OverlayEntryContext>;
|
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() onTap;
|
||||||
final void Function()? onExit;
|
|
||||||
final Decoration? decoration;
|
final Decoration? decoration;
|
||||||
|
|
||||||
const PopoverMask(
|
const PopoverMask({Key? key, required this.onTap, this.decoration})
|
||||||
{Key? key, required this.onTap, this.onExit, this.decoration})
|
|
||||||
: super(key: key);
|
: 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: widget.onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: widget.decoration,
|
decoration: decoration,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:appflowy_popover/src/layout.dart';
|
import 'package:appflowy_popover/src/layout.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'mask.dart';
|
import 'mask.dart';
|
||||||
import 'mutex.dart';
|
import 'mutex.dart';
|
||||||
|
|
||||||
@ -130,7 +131,6 @@ class PopoverState extends State<Popover> {
|
|||||||
}
|
}
|
||||||
_removeRootOverlay();
|
_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);
|
_rootEntry.addEntry(context, this, newEntry, widget.asBarrier);
|
||||||
}
|
}
|
||||||
@ -192,9 +202,9 @@ class PopoverState extends State<Popover> {
|
|||||||
showOverlay();
|
showOverlay();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Listener(
|
child: GestureDetector(
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
onPointerDown: (PointerDownEvent event) {
|
onTap: () {
|
||||||
if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
|
if (widget.triggerActions & PopoverTriggerFlags.click != 0) {
|
||||||
showOverlay();
|
showOverlay();
|
||||||
}
|
}
|
||||||
@ -240,14 +250,17 @@ class PopoverContainer extends StatefulWidget {
|
|||||||
class PopoverContainerState extends State<PopoverContainer> {
|
class PopoverContainerState extends State<PopoverContainer> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return CustomSingleChildLayout(
|
return Focus(
|
||||||
delegate: PopoverLayoutDelegate(
|
autofocus: true,
|
||||||
direction: widget.direction,
|
child: CustomSingleChildLayout(
|
||||||
link: widget.popoverLink,
|
delegate: PopoverLayoutDelegate(
|
||||||
offset: widget.offset,
|
direction: widget.direction,
|
||||||
windowPadding: widget.windowPadding,
|
link: widget.popoverLink,
|
||||||
|
offset: widget.offset,
|
||||||
|
windowPadding: widget.windowPadding,
|
||||||
|
),
|
||||||
|
child: widget.popupBuilder(context),
|
||||||
),
|
),
|
||||||
child: widget.popupBuilder(context),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,7 +386,7 @@
|
|||||||
"searchOption": "ابحث عن خيار"
|
"searchOption": "ابحث عن خيار"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "أضف عنصرًا"
|
"addNew": "أضف عنصرًا"
|
||||||
},
|
},
|
||||||
"menuName": "شبكة",
|
"menuName": "شبكة",
|
||||||
"referencedGridPrefix": "نظرا ل",
|
"referencedGridPrefix": "نظرا ل",
|
||||||
|
@ -402,7 +402,7 @@
|
|||||||
"searchOption": "Cerca una opció"
|
"searchOption": "Cerca una opció"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Afegeix un element"
|
"addNew": "Afegeix un element"
|
||||||
},
|
},
|
||||||
"menuName": "Quadrícula",
|
"menuName": "Quadrícula",
|
||||||
"referencedGridPrefix": "Vista de"
|
"referencedGridPrefix": "Vista de"
|
||||||
|
@ -411,7 +411,7 @@
|
|||||||
"searchOption": "Suchen Sie nach einer Option"
|
"searchOption": "Suchen Sie nach einer Option"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Fügen Sie einen Artikel hinzu"
|
"addNew": "Fügen Sie einen Artikel hinzu"
|
||||||
},
|
},
|
||||||
"menuName": "Netz",
|
"menuName": "Netz",
|
||||||
"referencedGridPrefix": "Sicht von"
|
"referencedGridPrefix": "Sicht von"
|
||||||
|
@ -477,7 +477,9 @@
|
|||||||
"orSelectOne": "Or select an option"
|
"orSelectOne": "Or select an option"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Add an item"
|
"taskHint": "Task description",
|
||||||
|
"addNew": "Add a new task",
|
||||||
|
"submitNewTask": "Create"
|
||||||
},
|
},
|
||||||
"menuName": "Grid",
|
"menuName": "Grid",
|
||||||
"referencedGridPrefix": "View of"
|
"referencedGridPrefix": "View of"
|
||||||
|
@ -377,7 +377,7 @@
|
|||||||
"addSort": "Agregar clasificación"
|
"addSort": "Agregar clasificación"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Agregar un elemento"
|
"addNew": "Agregar un elemento"
|
||||||
},
|
},
|
||||||
"referencedGridPrefix": "Vista de"
|
"referencedGridPrefix": "Vista de"
|
||||||
},
|
},
|
||||||
|
@ -387,7 +387,7 @@
|
|||||||
"searchOption": "Aukera bat bilatu"
|
"searchOption": "Aukera bat bilatu"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Gehitu elementu bat"
|
"addNew": "Gehitu elementu bat"
|
||||||
},
|
},
|
||||||
"menuName": "Sareta",
|
"menuName": "Sareta",
|
||||||
"deleteView": "Ziur ikuspegi hau ezabatu nahi duzula?",
|
"deleteView": "Ziur ikuspegi hau ezabatu nahi duzula?",
|
||||||
|
@ -439,7 +439,7 @@
|
|||||||
"searchOption": "جستجوی یک گزینه"
|
"searchOption": "جستجوی یک گزینه"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "یک مورد اضافه کنید"
|
"addNew": "یک مورد اضافه کنید"
|
||||||
},
|
},
|
||||||
"menuName": "شبکهای",
|
"menuName": "شبکهای",
|
||||||
"referencedGridPrefix": "نمایش"
|
"referencedGridPrefix": "نمایش"
|
||||||
|
@ -402,7 +402,7 @@
|
|||||||
"searchOption": "Rechercher une option"
|
"searchOption": "Rechercher une option"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Ajouter un article"
|
"addNew": "Ajouter un article"
|
||||||
},
|
},
|
||||||
"menuName": "Grille",
|
"menuName": "Grille",
|
||||||
"referencedGridPrefix": "Vue"
|
"referencedGridPrefix": "Vue"
|
||||||
|
@ -381,7 +381,7 @@
|
|||||||
"addSort": "Ajouter un tri"
|
"addSort": "Ajouter un tri"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Ajouter un élément"
|
"addNew": "Ajouter un élément"
|
||||||
},
|
},
|
||||||
"referencedGridPrefix": "Vue"
|
"referencedGridPrefix": "Vue"
|
||||||
},
|
},
|
||||||
|
@ -402,7 +402,7 @@
|
|||||||
"searchOption": "Keressen egy lehetőséget"
|
"searchOption": "Keressen egy lehetőséget"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Adjon hozzá egy elemet"
|
"addNew": "Adjon hozzá egy elemet"
|
||||||
},
|
},
|
||||||
"menuName": "Rács",
|
"menuName": "Rács",
|
||||||
"referencedGridPrefix": "Nézet"
|
"referencedGridPrefix": "Nézet"
|
||||||
|
@ -377,7 +377,7 @@
|
|||||||
"addSort": "Tambahkan semacam"
|
"addSort": "Tambahkan semacam"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Tambahkan item"
|
"addNew": "Tambahkan item"
|
||||||
},
|
},
|
||||||
"referencedGridPrefix": "Pemandangan dari"
|
"referencedGridPrefix": "Pemandangan dari"
|
||||||
},
|
},
|
||||||
|
@ -370,7 +370,7 @@
|
|||||||
"searchOption": "Cerca un'opzione"
|
"searchOption": "Cerca un'opzione"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Aggiungi un elemento"
|
"addNew": "Aggiungi un elemento"
|
||||||
},
|
},
|
||||||
"referencedGridPrefix": "Vista di"
|
"referencedGridPrefix": "Vista di"
|
||||||
},
|
},
|
||||||
|
@ -370,7 +370,7 @@
|
|||||||
"addSort": "並べ替えの追加"
|
"addSort": "並べ替えの追加"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "アイテムを追加する"
|
"addNew": "アイテムを追加する"
|
||||||
},
|
},
|
||||||
"menuName": "グリッド",
|
"menuName": "グリッド",
|
||||||
"referencedGridPrefix": "のビュー"
|
"referencedGridPrefix": "のビュー"
|
||||||
|
@ -383,7 +383,7 @@
|
|||||||
"addSort": "정렬 추가"
|
"addSort": "정렬 추가"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "항목 추가"
|
"addNew": "항목 추가"
|
||||||
},
|
},
|
||||||
"referencedGridPrefix": "관점"
|
"referencedGridPrefix": "관점"
|
||||||
},
|
},
|
||||||
|
@ -402,7 +402,7 @@
|
|||||||
"searchOption": "Wyszukaj opcję"
|
"searchOption": "Wyszukaj opcję"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Dodaj element"
|
"addNew": "Dodaj element"
|
||||||
},
|
},
|
||||||
"menuName": "Siatka",
|
"menuName": "Siatka",
|
||||||
"referencedGridPrefix": "Widok"
|
"referencedGridPrefix": "Widok"
|
||||||
|
@ -438,7 +438,7 @@
|
|||||||
"panelTitle": "Selecione uma opção ou crie uma"
|
"panelTitle": "Selecione uma opção ou crie uma"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Adicionar um item"
|
"addNew": "Adicionar um item"
|
||||||
},
|
},
|
||||||
"menuName": "Grade",
|
"menuName": "Grade",
|
||||||
"deleteView": "Tem certeza de que deseja excluir esta visualização?",
|
"deleteView": "Tem certeza de que deseja excluir esta visualização?",
|
||||||
|
@ -455,7 +455,7 @@
|
|||||||
"searchOption": "Pesquise uma opção"
|
"searchOption": "Pesquise uma opção"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Adicionar um item"
|
"addNew": "Adicionar um item"
|
||||||
},
|
},
|
||||||
"menuName": "Grade",
|
"menuName": "Grade",
|
||||||
"referencedGridPrefix": "Vista de"
|
"referencedGridPrefix": "Vista de"
|
||||||
|
@ -393,7 +393,7 @@
|
|||||||
"searchOption": "Поиск"
|
"searchOption": "Поиск"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Добавить элемент"
|
"addNew": "Добавить элемент"
|
||||||
},
|
},
|
||||||
"menuName": "Сетка",
|
"menuName": "Сетка",
|
||||||
"referencedGridPrefix": "Просмотр",
|
"referencedGridPrefix": "Просмотр",
|
||||||
|
@ -381,7 +381,7 @@
|
|||||||
"addSort": "Lägg till sortering"
|
"addSort": "Lägg till sortering"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "Lägg till ett objekt"
|
"addNew": "Lägg till ett objekt"
|
||||||
},
|
},
|
||||||
"referencedGridPrefix": "Utsikt över"
|
"referencedGridPrefix": "Utsikt över"
|
||||||
},
|
},
|
||||||
|
@ -402,7 +402,7 @@
|
|||||||
"searchOption": "Bir seçenek arayın"
|
"searchOption": "Bir seçenek arayın"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "öğe ekle"
|
"addNew": "öğe ekle"
|
||||||
},
|
},
|
||||||
"menuName": "Kafes",
|
"menuName": "Kafes",
|
||||||
"referencedGridPrefix": "görünümü"
|
"referencedGridPrefix": "görünümü"
|
||||||
|
@ -397,7 +397,7 @@
|
|||||||
"searchOption": "搜索标签"
|
"searchOption": "搜索标签"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "添加项"
|
"addNew": "添加项"
|
||||||
},
|
},
|
||||||
"menuName": "网格",
|
"menuName": "网格",
|
||||||
"referencedGridPrefix": "视图"
|
"referencedGridPrefix": "视图"
|
||||||
|
@ -386,7 +386,7 @@
|
|||||||
"searchOption": "搜尋選項"
|
"searchOption": "搜尋選項"
|
||||||
},
|
},
|
||||||
"checklist": {
|
"checklist": {
|
||||||
"panelTitle": "新增物件"
|
"addNew": "新增物件"
|
||||||
},
|
},
|
||||||
"menuName": "網格",
|
"menuName": "網格",
|
||||||
"deleteView": "您確定要刪除該視圖嗎?",
|
"deleteView": "您確定要刪除該視圖嗎?",
|
||||||
|
@ -42,7 +42,7 @@ impl ChecklistCellData {
|
|||||||
if total_options == 0 {
|
if total_options == 0 {
|
||||||
return 0.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 {
|
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.options.len(), 3);
|
||||||
assert_eq!(cell.selected_options.len(), 2);
|
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
|
// The number of groups should be 0 if there is no group by field in grid
|
||||||
|
Loading…
x
Reference in New Issue
Block a user