feat(todo): Today, Upcoming, Inbox, projects, quick add and the task sheet (ADR 0033, D8, D9)

The tabs hold the person's tasks: Today what is overdue (Reschedule
moving it all to a day, times kept) and what is due today; Upcoming the
overdue ones and the next four weeks day by day; Inbox and each
project's screen from Browse the tasks above and in their sections, done
ones on asking; Browse the projects with their open counts and the
labels. A task's row shows its check in its priority's colour, its date
in the colour of how near it is, its deadline, its sub-tasks done and
its labels; quick add takes its title, date, priority, project, labels
and description and stays for the next; its sheet edits all of it, adds
sub-tasks and deletes it with them after asking. Colours are the
palette's, cerise for overdue and P1 against the strawberry wash.

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 10:25:34 +01:00
commit 030c62ddde
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -8,6 +8,7 @@ import 'package:halkyon_todo_flutter/src/feature/label/data/label_repository.dar
import 'package:halkyon_todo_flutter/src/feature/project/data/project_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
@ -120,10 +121,13 @@ void main() {
'https://code.halkyon.app/project-halkyon/halkyon',
openPerson: (person) async {
opened.add(person);
final projects = ProjectRepository(projectRows, keys);
// As the app does when it opens the person's data.
await projects.ensureInbox();
return TodoPerson(
person,
sync: sync,
projects: ProjectRepository(projectRows, keys),
projects: projects,
sections: SectionRepository(sectionRows, keys),
tasks: TaskRepository(taskRows, keys),
labels: LabelRepository(labelRows, keys),
@ -210,19 +214,17 @@ void main() {
expect(find.widgetWithText(AppBar, 'Today'), findsOneWidget);
});
testWidgets('each tab says tasks come with a later version until they '
'do', (tester) async {
testWidgets('each tab says what it holds while it has nothing', (
tester,
) async {
await start(tester, usedBefore: true);
for (final tab in ['Today', 'Upcoming', 'Inbox', 'Browse']) {
await goTab(tester, tab);
expect(find.widgetWithText(AppBar, tab), findsOneWidget, reason: tab);
expect(
find.text('Tasks come with a later version of Halkyon Todo.'),
findsOneWidget,
reason: tab,
);
}
expect(find.text('Nothing due today'), findsOneWidget);
await goTab(tester, 'Inbox');
expect(find.text('Nothing to do here'), findsOneWidget);
await goTab(tester, 'Browse');
expect(find.text('No projects of your own yet.'), findsOneWidget);
expect(find.text('No labels yet.'), findsOneWidget);
});
testWidgets('the tabs wash down from the top in strawberry', (
@ -289,6 +291,229 @@ void main() {
});
});
group('tasks (ADR 0033, D8)', () {
/// The person's tasks, read past the app as another device would.
TaskRepository tasks() => TaskRepository(taskRows, keys);
/// The Inbox's ID, as the app made it.
String inbox() => ProjectRepository(projectRows, keys).inboxId;
/// Adds [title] through quick add, from the current tab's button.
Future<void> quickAdd(WidgetTester tester, String title) async {
await tester.tap(find.byTooltip('Add task'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).first, title);
// Add is on once there is a title.
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
// The sheet stays for the next one; its close button closes it.
await tester.tap(find.byTooltip('Close').last);
await tester.pumpAndSettle();
}
/// Writes [content] as another device of the person's would, the sync
/// bringing it.
Future<void> seed(WidgetTester tester, TaskContent content) async {
await tester.runAsync(() => tasks().add(content));
taskRows.bringRemoteChanges();
await tester.pumpAndSettle();
}
testWidgets('a task added in the Inbox shows there, sealed on its way', (
tester,
) async {
await start(tester, usedBefore: true);
await goTab(tester, 'Inbox');
await quickAdd(tester, 'Buy milk');
expect(find.text('Buy milk'), findsOneWidget);
final stored = await tester.runAsync(() => tasks().all());
expect(stored!.single.content?.title, 'Buy milk');
expect(stored.single.content?.projectId, inbox());
expect(taskRows.byId.values.single.keyId, isNotNull);
});
testWidgets('a task added on Today is dated today and shows there with '
'its project', (tester) async {
await start(tester, usedBefore: true);
await quickAdd(tester, 'Call Anna');
expect(find.text('Call Anna'), findsOneWidget);
expect(
find.descendant(
of: find.widgetWithText(ListTile, 'Call Anna'),
matching: find.text('Inbox'),
),
findsOneWidget,
);
final stored = await tester.runAsync(() => tasks().all());
expect(stored!.single.content?.date?.day, Day.of(DateTime.now()));
});
testWidgets('done, a task leaves its list and can be undone; shown, it '
'is struck through', (tester) async {
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(title: 'Water plants', projectId: inbox()),
);
await goTab(tester, 'Inbox');
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Mark as done'));
await tester.pumpAndSettle();
expect(find.text('Water plants'), findsNothing);
expect(find.text('Task done'), findsOneWidget);
await tester.tap(find.text('Undo'));
await tester.pumpAndSettle();
expect(find.text('Water plants'), findsOneWidget);
await tester.tap(find.byTooltip('Mark as done'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('More'));
await tester.pumpAndSettle();
await tester.tap(find.text('Show done tasks'));
await tester.pumpAndSettle();
final title = tester.widget<Text>(find.text('Water plants'));
expect(title.style?.decoration, TextDecoration.lineThrough);
});
testWidgets('Today puts what is overdue first; Reschedule moves it to '
'the day picked', (tester) async {
final today = Day.of(DateTime.now());
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Pay the bill',
projectId: inbox(),
date: TaskDate(today.plus(-3), time: 8 * 60),
),
);
await tester.pumpAndSettle();
expect(find.text('Overdue'), findsOneWidget);
expect(find.text('Pay the bill'), findsOneWidget);
await tester.tap(find.text('Reschedule'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(ListTile, 'Today').first);
await tester.pumpAndSettle();
expect(find.text('Overdue'), findsNothing);
expect(find.text('Pay the bill'), findsOneWidget);
final stored = await tester.runAsync(() => tasks().all());
expect(
stored!.single.content?.date,
TaskDate(today, time: 8 * 60),
reason: 'its time kept',
);
});
testWidgets('a project made in Browse opens its screen; its task stays '
'out of the Inbox; deleting it asks, and takes its tasks', (
tester,
) async {
await start(tester, usedBefore: true);
await goTab(tester, 'Browse');
await tester.tap(find.byTooltip('New project'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'Home');
await tester.tap(find.text('Save'));
await tester.pumpAndSettle();
expect(find.text('Home'), findsOneWidget);
await tester.tap(find.text('Home'));
await tester.pumpAndSettle();
expect(find.widgetWithText(AppBar, 'Home'), findsOneWidget);
await quickAdd(tester, 'Fix the tap');
expect(find.text('Fix the tap'), findsOneWidget);
await goTab(tester, 'Inbox');
expect(find.text('Fix the tap'), findsNothing);
await goTab(tester, 'Browse');
// Browse keeps the screen it was left on.
expect(find.widgetWithText(AppBar, 'Home'), findsOneWidget);
await tester.tap(find.byTooltip('More'));
await tester.pumpAndSettle();
await tester.tap(find.text('Delete project'));
await tester.pumpAndSettle();
expect(find.text('Delete the project?'), findsOneWidget);
await tester.tap(find.text('Delete project').last);
await tester.pumpAndSettle();
expect(find.text('No projects of your own yet.'), findsOneWidget);
expect(await tester.runAsync(() => tasks().all()), isEmpty);
});
testWidgets("a task's sheet sets its priority, adds a sub-task the row "
'counts, and deletes it with its sub-tasks after asking', (
tester,
) async {
await start(tester, usedBefore: true);
await seed(tester, TaskContent(title: 'Pack', projectId: inbox()));
await goTab(tester, 'Inbox');
await tester.pumpAndSettle();
await tester.tap(find.text('Pack'));
await tester.pumpAndSettle();
await tester.tap(find.text('No priority'));
await tester.pumpAndSettle();
await tester.tap(find.text('Priority 1'));
await tester.pumpAndSettle();
expect(find.text('Priority 1'), findsOneWidget);
await tester.tap(find.text('Add sub-task'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).last, 'Socks');
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
await tester.tap(find.byTooltip('Close').last);
await tester.pumpAndSettle();
expect(find.text('Sub-tasks 0/1'), findsOneWidget);
await tester.tap(find.byTooltip('Delete task'));
await tester.pumpAndSettle();
expect(
find.text('"Pack" is deleted with its sub-task, on all your devices.'),
findsOneWidget,
);
await tester.tap(find.text('Delete task').last);
await tester.pumpAndSettle();
expect(find.text('Pack'), findsNothing);
expect(await tester.runAsync(() => tasks().all()), isEmpty);
});
testWidgets('Upcoming lists the days ahead, the empty ones too', (
tester,
) async {
final today = Day.of(DateTime.now());
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Dentist',
projectId: inbox(),
date: TaskDate(today.plus(2)),
),
);
await goTab(tester, 'Upcoming');
await tester.pumpAndSettle();
expect(find.textContaining('Today'), findsWidgets);
expect(find.textContaining('Tomorrow'), findsWidgets);
expect(find.text('Dentist'), findsOneWidget);
});
});
testWidgets('signed out, it asks how to start, with an account to create '
'where passwords are offered (ADR 0017), and no GitHub (ADR 0020, '
'D36)', (tester) async {