feat(todo): each project's Display: completed tasks, grouping, sorting and filters, synced (ADR 0033, D17)

- A Display button beside a project's menu (the Inbox's too), a dot
  on it while the project shows otherwise than at first, opens its
  sheet: completed tasks, grouping (date, deadline, added date,
  priority, label), sorting (name, date, deadline, added date,
  priority), filters by date, deadline, priority and label, and
  Reset all.
- The choices live in the project's sealed payload (ProjectView,
  written only once set), so every device shows the project alike.
- Grouped, the groups stand in the sections' place under pinned
  headings; rows are dragged only while sorted by hand by sections.
- Show done tasks moves from the menu into Display.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-01 18:41:12 +01:00
commit 7d1de304c8
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -11,6 +11,7 @@ import 'package:halkyon_todo_flutter/src/feature/label/data/label_repository.dar
import 'package:halkyon_todo_flutter/src/feature/label/model/label.dart';
import 'package:halkyon_todo_flutter/src/feature/project/data/project_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/project/model/project.dart';
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/section/model/section.dart';
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
@ -358,6 +359,144 @@ void main() {
expect(taskRows.byId.values.single.keyId, isNotNull);
});
group("a project's Display (D17)", () {
Finder row(String title) => find.widgetWithText(ListTile, title);
Future<void> display(WidgetTester tester) async {
await tester.tap(find.byTooltip('Display'));
await tester.pumpAndSettle();
}
Future<void> pick(
WidgetTester tester,
String setting,
String value,
) async {
await tester.tap(find.widgetWithText(ListTile, setting));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(ListTile, value).last);
await tester.pumpAndSettle();
}
Future<void> done(WidgetTester tester) async {
await tester.tap(find.widgetWithText(FilledButton, 'Done'));
await tester.pumpAndSettle();
}
Future<void> inboxOf(WidgetTester tester) async {
await start(tester, usedBefore: true);
for (final (index, (title, priority)) in [
('cherry', Priority.none),
('Apple', Priority.p1),
('banana', Priority.p2),
].indexed) {
await seed(
tester,
TaskContent(
title: title,
projectId: inbox(),
position: index.toDouble(),
priority: priority,
),
);
}
await goTab(tester, 'Inbox');
}
List<String> order(WidgetTester tester) => ['cherry', 'Apple', 'banana']
..sort(
(a, b) => tester
.getCenter(row(a))
.dy
.compareTo(tester.getCenter(row(b)).dy),
);
testWidgets('sorting by name orders the list, kept in the project for '
'every device; a dot marks the button; Reset all puts it back', (
tester,
) async {
await inboxOf(tester);
expect(order(tester), ['cherry', 'Apple', 'banana']);
expect(
tester.widget<Badge>(find.byType(Badge).first).isLabelVisible,
isFalse,
);
await display(tester);
await pick(tester, 'Sorting', 'Name');
await done(tester);
expect(order(tester), ['Apple', 'banana', 'cherry']);
final projects = await tester.runAsync(
() => ProjectRepository(projectRows, keys).all(),
);
expect(
projects!
.singleWhere((project) => project.id == inbox())
.content
?.view
.sorting,
TaskSorting.name,
);
expect(
tester.widget<Badge>(find.byType(Badge).first).isLabelVisible,
isTrue,
);
await display(tester);
await tester.tap(find.text('Reset all'));
await tester.pumpAndSettle();
await done(tester);
expect(order(tester), ['cherry', 'Apple', 'banana']);
});
testWidgets('grouped by priority, each priority under its heading', (
tester,
) async {
await inboxOf(tester);
await display(tester);
await pick(tester, 'Grouping', 'Priority');
await done(tester);
expect(find.text('Priority 1 1'), findsOneWidget);
expect(find.text('Priority 2 1'), findsOneWidget);
expect(find.text('No priority 1'), findsOneWidget);
});
testWidgets('a filter by priority leaves out the others; none left, it '
'says so', (tester) async {
await inboxOf(tester);
await display(tester);
await tester.tap(find.widgetWithText(ListTile, 'Priority'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(CheckboxListTile, 'Priority 1'));
await tester.pump();
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
expect(find.widgetWithText(ListTile, 'Priority 1'), findsOneWidget);
await done(tester);
expect(row('Apple'), findsOneWidget);
expect(row('banana'), findsNothing);
expect(row('cherry'), findsNothing);
});
testWidgets('rows are dragged only while sorted by hand', (tester) async {
await inboxOf(tester);
await display(tester);
await pick(tester, 'Sorting', 'Priority');
await done(tester);
expect(find.byType(DraggableTask), findsNothing);
await display(tester);
await pick(tester, 'Sorting', 'Manual');
await done(tester);
expect(find.byType(DraggableTask), findsNWidgets(3));
});
});
group('quick add reads what is typed (D15)', () {
Future<void> open(WidgetTester tester, String text) async {
await tester.tap(find.byTooltip('Add task'));
@ -573,9 +712,11 @@ void main() {
await tester.tap(find.byTooltip('Mark as done'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('More'));
await tester.tap(find.byTooltip('Display'));
await tester.pumpAndSettle();
await tester.tap(find.text('Show done tasks'));
await tester.tap(find.text('Completed tasks'));
await tester.pumpAndSettle();
await tester.tap(find.text('Done'));
await tester.pumpAndSettle();
final title = tester.widget<Text>(find.text('Water plants'));
expect(title.style?.decoration, TextDecoration.lineThrough);