feat(todo): a project laid out as a board, its columns its sections (ADR 0033, D17)

The Display's new Layout picks List or Board. The board has a column
for the tasks in no section, then one a section with its menu, each
task a card and each column ending with Add task, a last column adding
a section; grouped otherwise, a column a group. Narrower than a medium
width it pages one column at a time with dots below; from it on the
columns stand side by side. Cards drag within and between columns as
rows do (D16), every scrollable they are in scrolling at its edges.

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:51:15 +01:00
commit e4fbfb54d1
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -12,6 +12,7 @@ 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/project/widget/project_board.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';
@ -495,6 +496,122 @@ void main() {
await done(tester);
expect(find.byType(DraggableTask), findsNWidgets(3));
});
group('Board', () {
/// The Inbox as a board, with a section "Errands" holding "Stamps".
Future<String> boardOf(WidgetTester tester) async {
await inboxOf(tester);
final errands = await tester.runAsync(
() => SectionRepository(
sectionRows,
keys,
).add(SectionContent(name: 'Errands', projectId: inbox())),
);
sectionRows.bringRemoteChanges();
await seed(
tester,
TaskContent(
title: 'Stamps',
projectId: inbox(),
sectionId: errands,
),
);
await display(tester);
await tester.tap(find.text('Board'));
await tester.pumpAndSettle();
await done(tester);
return errands!;
}
testWidgets('a column for the tasks in no section and one a section, '
'paged on a phone with dots below; kept in the project', (
tester,
) async {
await boardOf(tester);
expect(find.text('(No section) 3'), findsOneWidget);
expect(find.byKey(ProjectBoard.dotsKey), findsOneWidget);
final projects = await tester.runAsync(
() => ProjectRepository(projectRows, keys).all(),
);
expect(
projects!
.singleWhere((project) => project.id == inbox())
.content
?.view
.layout,
ProjectLayout.board,
);
await tester.fling(
find.byType(PageView),
const Offset(-300, 0),
1000,
);
await tester.pumpAndSettle();
expect(find.text('Errands 1'), findsOneWidget);
expect(find.text('Stamps').hitTestable(), findsOneWidget);
});
testWidgets("a column's Add task adds into its section", (
tester,
) async {
final errands = await boardOf(tester);
await tester.fling(
find.byType(PageView),
const Offset(-300, 0),
1000,
);
await tester.pumpAndSettle();
await tester.tap(
find.widgetWithText(TextButton, 'Add task').hitTestable(),
);
await tester.pumpAndSettle();
await tester.enterText(find.byKey(quickAddTitleKey), 'Envelopes');
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
await tester.pumpAndSettle();
final all = await tester.runAsync(() => tasks().all());
expect(
all!
.singleWhere((task) => task.content?.title == 'Envelopes')
.content
?.sectionId,
errands,
);
});
testWidgets('on a wide window the columns stand side by side, and a '
'card dragged onto another column goes last in it', (
tester,
) async {
final errands = await boardOf(tester);
tester.view.physicalSize = const Size(1000, 900);
await tester.pumpAndSettle();
expect(find.byType(PageView), findsNothing);
expect(find.text('Errands 1'), findsOneWidget);
final from = tester.getCenter(row('Apple'));
final gesture = await tester.startGesture(from);
await tester.pump(
kLongPressTimeout + const Duration(milliseconds: 50),
);
final to = tester.getBottomLeft(row('Stamps')) + const Offset(40, 80);
for (var step = 1; step <= 10; step++) {
await gesture.moveTo(Offset.lerp(from, to, step / 10)!);
await tester.pump();
}
await gesture.up();
await tester.pumpAndSettle();
final all = await tester.runAsync(() => tasks().all());
final apple = all!.singleWhere((t) => t.content?.title == 'Apple');
expect(apple.content?.sectionId, errands);
expect(find.text('Errands 2'), findsOneWidget);
});
});
});
group('quick add reads what is typed (D15)', () {