feat(todo): a project as a table on wide windows (ADR 0033, D36)

Task, date, deadline, priority and labels in columns under a heading that
stays. A phone isn't offered it and shows a project kept as one as a list.

Whats-New: Table: on a wide window a project can show its tasks in columns, through Display.
Whats-New-De: Tabelle: In einem breiten Fenster zeigt ein Projekt seine Aufgaben in Spalten, über Anzeige.

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-06 19:49:15 +01:00
commit 6385d84cff
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 441 additions and 10 deletions

View file

@ -25,6 +25,7 @@ import 'package:halkyon_todo_flutter/src/feature/project/model/stored_project.da
import 'package:halkyon_todo_flutter/src/feature/project/widget/month_grid.dart';
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_board.dart';
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_calendar_view.dart';
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_table.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/settings/data/settings_repository.dart';
@ -854,6 +855,71 @@ void main() {
expect(find.byType(DraggableTask), findsNWidgets(3));
});
group('Table (D36)', () {
testWidgets('a wide window offers it and shows columns; the row opens '
'its task', (tester) async {
await inboxOf(tester);
await seed(
tester,
TaskContent(
title: 'Pay rent',
projectId: inbox(),
priority: Priority.p3,
),
);
tester.view.physicalSize = const Size(1400, 900);
await tester.pumpAndSettle();
await display(tester);
await tester.tap(find.text('Table'));
await tester.pumpAndSettle();
await done(tester);
expect(find.byKey(ProjectTable.headingKey), findsOneWidget);
for (final column in ['Task', 'Date', 'Deadline', 'Labels']) {
expect(
find.descendant(
of: find.byKey(ProjectTable.headingKey),
matching: find.text(column),
),
findsOneWidget,
);
}
expect(find.text('Priority 3'), findsOneWidget);
await tester.tap(find.text('Pay rent'));
await tester.pumpAndSettle();
expect(find.text('Pay rent'), findsNWidgets(2));
});
testWidgets('a phone is not offered it, and shows a table kept '
'elsewhere as a list', (tester) async {
await inboxOf(tester);
await display(tester);
expect(find.text('Table'), findsNothing);
await done(tester);
final inboxContent = await tester.runAsync(
() => ProjectRepository(projectRows, keys).all(),
);
await tester.runAsync(
() => ProjectRepository(projectRows, keys).update(
inbox(),
inboxContent!
.firstWhere((project) => project.id == inbox())
.content!
.copyWith(
view: const ProjectView(layout: ProjectLayout.table),
),
),
);
projectRows.bringRemoteChanges();
await tester.pumpAndSettle();
expect(find.byKey(ProjectTable.headingKey), findsNothing);
expect(find.byType(TaskTile), findsWidgets);
});
});
group('Board', () {
/// The Inbox as a board, with a section "Errands" holding "Stamps".
Future<String> boardOf(WidgetTester tester) async {

View file

@ -69,6 +69,10 @@ void main() {
}),
);
expect(read?.view, const ProjectView(completed: true));
expect(
decodeView(encodeView(const ProjectView(layout: ProjectLayout.table))),
const ProjectView(layout: ProjectLayout.table),
);
});
test('the payload names its schema and the colour by its name', () {