feat(todo): quick add suggests projects and labels as # and @ are typed (ADR 0033, D15)

Up to five projects or labels whose name, or a word of it, starts with
what follows the symbol show below the title; one tapped completes it.
None for a symbol inside a word (C#), nor once a name is typed whole.

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 17:32:52 +01:00
commit 7fe74ed0d5
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -8,7 +8,9 @@ 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/privacy/todo_keys.dart';
import 'package:halkyon_todo_flutter/src/feature/label/data/label_repository.dart';
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/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';
@ -403,6 +405,71 @@ void main() {
expect(content.labelIds, [labels.single.id]);
});
testWidgets('# and @ suggest projects and labels as they are typed; '
'one tapped completes the name', (tester) async {
await start(tester, usedBefore: true);
final shopping = await tester.runAsync(
() => ProjectRepository(
projectRows,
keys,
).save(const ProjectContent(name: 'Shopping', position: 1)),
);
projectRows.bringRemoteChanges();
final errand = await tester.runAsync(
() => LabelRepository(
labelRows,
keys,
).add(const LabelContent(name: 'errand')),
);
labelRows.bringRemoteChanges();
await goTab(tester, 'Inbox');
await open(tester, 'Buy milk #sho');
final suggestion = find.widgetWithText(ListTile, 'Shopping');
expect(suggestion, findsOneWidget);
await tester.tap(suggestion);
await tester.pumpAndSettle();
final title = tester
.widget<TextField>(find.byKey(quickAddTitleKey))
.controller!;
expect(title.text, 'Buy milk #Shopping ');
expect(suggestion, findsNothing, reason: 'done with');
await tester.enterText(
find.byKey(quickAddTitleKey),
'${title.text}@er',
);
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(ListTile, 'errand'));
await tester.pumpAndSettle();
await add(tester);
final stored = await tester.runAsync(() => tasks().all());
final content = stored!.single.content!;
expect(content.title, 'Buy milk');
expect(content.projectId, shopping);
expect(content.labelIds, [errand]);
});
testWidgets('nothing is suggested for a # inside a word, nor where no '
'name matches', (tester) async {
await start(tester, usedBefore: true);
await tester.runAsync(
() => ProjectRepository(
projectRows,
keys,
).save(const ProjectContent(name: 'Shopping', position: 1)),
);
projectRows.bringRemoteChanges();
await goTab(tester, 'Inbox');
await open(tester, 'Fix C#sho');
expect(find.widgetWithText(ListTile, 'Shopping'), findsNothing);
await tester.enterText(find.byKey(quickAddTitleKey), 'Fix #zz');
await tester.pumpAndSettle();
expect(find.byKey(quickAddSuggestionsKey), findsNothing);
});
testWidgets('a token tapped stays text', (tester) async {
await start(tester, usedBefore: true);
await goTab(tester, 'Inbox');