feat(todo): comments and descriptions in the editor (ADR 0033, D38)

Comments and the descriptions of tasks, projects, sections and filters
become the editor's document on halkyon_editor; names and titles stay
text. Rows written as text read as notes and become documents when next
saved, the server seeing only sealed bytes, so there is no migration.

Sections' descriptions can now be written, and a project's shows above
its tasks. Android answers whether a link can open only for what is
declared, so Todo declares http and https.

Whats-New: Comments and descriptions take rich text: headings, lists, checklists, links and more.
Whats-New-De: Kommentare und Beschreibungen können jetzt formatiert werden: Überschriften, Listen, Checklisten, Links und mehr.

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 21:36:05 +01:00
commit a25cca12b4
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -7,8 +7,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_app/testing.dart';
import 'package:halkyon_editor/halkyon_editor.dart';
import 'package:halkyon_todo_flutter/src/app/todo_app.dart';
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
import 'package:halkyon_todo_flutter/src/core/note/note.dart';
import 'package:halkyon_todo_flutter/src/core/note/note_fields.dart';
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
import 'package:halkyon_todo_flutter/src/feature/comment/data/comment_repository.dart';
import 'package:halkyon_todo_flutter/src/feature/comment/model/comment.dart';
@ -527,13 +530,45 @@ void main() {
await tester.pumpAndSettle();
}
/// The editor's change, as typing it makes: what a test can't do is
/// type into the editor, which reads the platform's text input.
void typeNote(WidgetTester tester, Finder within, String text) => tester
.widget<RichContentEditor>(
find.descendant(
of: within,
matching: find.byType(RichContentEditor),
),
)
.onChanged(noteOf(text));
testWidgets('a description is written in the whole window, kept as it '
'is left, and a task row shows its first line', (tester) async {
await start(tester, usedBefore: true);
await openTask(tester);
await tester.tap(find.text('Description'));
await tester.pumpAndSettle();
typeNote(
tester,
find.byType(Scaffold).last,
'Bring the mat\nAnd water',
);
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
taskRows.bringRemoteChanges();
await tester.pumpAndSettle();
expect(find.text('Bring the mat', findRichText: true), findsWidgets);
expect(find.text('And water', findRichText: true), findsOneWidget);
});
Future<void> openComments(WidgetTester tester) async {
await tester.tap(find.byKey(AddCommentBar.barKey));
await tester.pumpAndSettle();
}
Future<void> write(WidgetTester tester, String text) async {
await tester.enterText(find.byKey(commentFieldKey), text);
typeNote(tester, find.byKey(commentFieldKey), text);
await tester.pumpAndSettle();
await tester.tap(find.byKey(commentSendKey));
await tester.pumpAndSettle();
@ -549,7 +584,7 @@ void main() {
expect(find.byKey(AddCommentBar.barKey), findsOneWidget);
expect(find.byKey(commentFieldKey), findsNothing);
await openComments(tester);
await tester.enterText(find.byKey(commentFieldKey), ' ');
typeNote(tester, find.byKey(commentFieldKey), ' ');
await tester.pumpAndSettle();
expect(
tester.widget<IconButton>(find.byKey(commentSendKey)).onPressed,
@ -560,11 +595,8 @@ void main() {
expect(find.text('Ask the vet', findRichText: true), findsWidgets);
expect(
tester
.widget<TextField>(find.byKey(commentFieldKey))
.controller!
.text,
isEmpty,
tester.widget<IconButton>(find.byKey(commentSendKey)).onPressed,
isNull,
);
});
@ -595,14 +627,8 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(find.text('Edit comment'));
await tester.pumpAndSettle();
await tester.enterText(
find.descendant(
of: find.byType(AlertDialog),
matching: find.byType(TextField),
),
'Ask the nurse',
);
await tester.tap(find.text('Save'));
typeNote(tester, find.byType(Scaffold).last, 'Ask the nurse');
await tester.tap(find.byType(BackButton));
await tester.pumpAndSettle();
commentRows.bringRemoteChanges();
await tester.pumpAndSettle();
@ -643,7 +669,7 @@ void main() {
() => CommentRepository(commentRows, keys).add(
CommentContent(
taskId: 'none',
text: 'x',
text: noteOf('x'),
writtenAt: DateTime.utc(2026),
),
),
@ -657,7 +683,7 @@ void main() {
() => comments.add(
CommentContent(
taskId: id!.single.id,
text: 'Liked',
text: noteOf('Liked'),
writtenAt: DateTime.utc(2026),
reactions: const ['❤️'],
),
@ -3051,7 +3077,10 @@ void main() {
await tester.tap(find.byKey(quickAddTitleKey));
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(find.widgetWithText(TextField, 'Description'), findsOneWidget);
expect(
find.widgetWithText(NoteComposer, 'Description'),
findsOneWidget,
);
await tester.tap(find.byTooltip('More').last);
await tester.pumpAndSettle();
@ -3636,7 +3665,7 @@ void main() {
tester,
TaskContent(
title: title,
description: 'With a description',
description: noteOf('With a description'),
projectId: inbox(),
date: TaskDate(Day.of(DateTime.now()).plus(1)),
),
@ -3823,7 +3852,9 @@ void main() {
reason: 'no name yet',
);
await tester.enterText(find.byType(TextField).first, 'Roadmap');
await tester.enterText(find.byType(TextField).at(1), 'What comes');
tester
.widget<RichContentEditor>(find.byType(RichContentEditor))
.onChanged(noteOf('What comes'));
await tester.pump();
expect(find.text('7/120'), findsOneWidget);
await tester.tap(find.text('Parent project'));
@ -3841,7 +3872,7 @@ void main() {
await tester.pumpAndSettle();
final roadmap = (await stored(tester, 'Roadmap')).content!;
expect(roadmap.description, 'What comes');
expect(roadmap.description.text, 'What comes');
expect(roadmap.parentId, ids['Home']);
expect(roadmap.favorite, isTrue);
expect(roadmap.view, const ProjectView(layout: ProjectLayout.board));
@ -4699,7 +4730,7 @@ void main() {
TaskContent(
title: 'Buy oat milk',
projectId: inbox(),
description: 'From the Café',
description: noteOf('From the Café'),
),
);
await seed(tester, TaskContent(title: 'Call Anna', projectId: inbox()));