feat(todo): filters in Todoist's language, and a view of each label (ADR 0033, D35)
A filter is a query read on the device, as the server sees no task, kept sealed in `saved_filter`. Filters & labels is a screen of its own under Browse; a label opens its tasks, and favourite filters and labels join Browse's Favourites. D36 records what of Todoist's Pro stays out. Whats-New: Filters: keep views like "today & p1", and open a label to see its tasks, under Browse in Filters & labels. Whats-New-De: Filter: Speichere Ansichten wie „heute & p1“ und öffne ein Label, um seine Aufgaben zu sehen, in der Übersicht unter Filter & Labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
a4c1fe298d
commit
b1f2078357
1 changed files with 21688 additions and 71 deletions
|
|
@ -9,6 +9,10 @@ import 'package:halkyon_app/testing.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/privacy/todo_keys.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/data/filter_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/model/saved_filter.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/widget/filter_editor.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/widget/filters_screen.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/location/data/location_reminder_gateway.dart';
|
||||
|
|
@ -32,6 +36,7 @@ import 'package:halkyon_todo_flutter/src/feature/task/widget/quick_add_sheet.dar
|
|||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_drag.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_sheet.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_tile.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/browse_tab.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/hour_grid.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/plan_panel.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/today_icon.dart';
|
||||
|
|
@ -63,6 +68,7 @@ void main() {
|
|||
late MemorySealedRowGateway sectionRows;
|
||||
late MemorySealedRowGateway taskRows;
|
||||
late MemorySealedRowGateway labelRows;
|
||||
late MemorySealedRowGateway filterRows;
|
||||
late MemorySealedRowGateway settingsRows;
|
||||
late MemoryReminderGateway reminders;
|
||||
late MemoryLocationGateway locationGateway;
|
||||
|
|
@ -93,6 +99,7 @@ void main() {
|
|||
sectionRows = MemorySealedRowGateway();
|
||||
taskRows = MemorySealedRowGateway();
|
||||
labelRows = MemorySealedRowGateway();
|
||||
filterRows = MemorySealedRowGateway();
|
||||
settingsRows = MemorySealedRowGateway();
|
||||
reminders = MemoryReminderGateway();
|
||||
locationGateway = MemoryLocationGateway();
|
||||
|
|
@ -195,6 +202,7 @@ void main() {
|
|||
(await settings.read()).reminders.automatic,
|
||||
),
|
||||
labels: LabelRepository(labelRows, keys),
|
||||
filters: FilterRepository(filterRows, keys),
|
||||
settings: settings,
|
||||
reminders: reminders,
|
||||
locationReminders: locationGateway,
|
||||
|
|
@ -391,9 +399,159 @@ void main() {
|
|||
expect(find.text('Nothing to do here'), findsOneWidget);
|
||||
await goTab(tester, 'Browse');
|
||||
expect(find.text('No projects of your own yet.'), findsOneWidget);
|
||||
await tester.tap(find.byKey(filtersEntryKey));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('No filters yet.'), findsOneWidget);
|
||||
expect(find.text('No labels yet.'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('filters and labels (D35)', () {
|
||||
Future<void> openFilters(WidgetTester tester) async {
|
||||
await goTab(tester, 'Browse');
|
||||
await tester.tap(find.byKey(filtersEntryKey));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> tasksOf(
|
||||
WidgetTester tester,
|
||||
Map<String, ({Priority priority, List<String> labels})> all,
|
||||
) async {
|
||||
await tester.runAsync(() async {
|
||||
final inbox = await ProjectRepository(
|
||||
projectRows,
|
||||
keys,
|
||||
).ensureInbox();
|
||||
final repository = TaskRepository(taskRows, keys);
|
||||
for (final MapEntry(key: title, value: task) in all.entries) {
|
||||
await repository.add(
|
||||
TaskContent(
|
||||
title: title,
|
||||
projectId: inbox,
|
||||
priority: task.priority,
|
||||
labelIds: task.labels,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
projectRows.bringRemoteChanges();
|
||||
taskRows.bringRemoteChanges();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets('a new filter says what it finds as it is typed, and '
|
||||
"can't be kept while it isn't understood", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await tasksOf(tester, const {
|
||||
'Pay rent': (priority: Priority.p1, labels: []),
|
||||
'Water plants': (priority: Priority.none, labels: []),
|
||||
});
|
||||
await openFilters(tester);
|
||||
|
||||
await tester.tap(find.byKey(filterAddKey));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byKey(filterNameKey), 'Urgent');
|
||||
await tester.enterText(find.byKey(filterQueryKey), 'p1 & (');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('This query ends too soon'), findsOneWidget);
|
||||
expect(
|
||||
tester.widget<FilledButton>(find.byKey(filterSaveKey)).onPressed,
|
||||
isNull,
|
||||
);
|
||||
|
||||
await tester.enterText(find.byKey(filterQueryKey), 'p1');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('1 task matches'), findsOneWidget);
|
||||
await tester.tap(find.byKey(filterSaveKey));
|
||||
await tester.pumpAndSettle();
|
||||
filterRows.bringRemoteChanges();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Urgent'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Pay rent'), findsOneWidget);
|
||||
expect(find.text('Water plants'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a favourite filter and a favourite label show in Browse; '
|
||||
'others stay on their screen', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await tester.runAsync(() async {
|
||||
final filters = FilterRepository(filterRows, keys);
|
||||
await filters.add(
|
||||
const FilterContent(name: 'Kept', query: 'p1', favorite: true),
|
||||
);
|
||||
await filters.add(const FilterContent(name: 'Plain', query: 'p2'));
|
||||
await LabelRepository(
|
||||
labelRows,
|
||||
keys,
|
||||
).add(const LabelContent(name: 'errand', favorite: true));
|
||||
});
|
||||
filterRows.bringRemoteChanges();
|
||||
labelRows.bringRemoteChanges();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await goTab(tester, 'Browse');
|
||||
expect(find.text('Kept'), findsOneWidget);
|
||||
expect(find.text('errand'), findsOneWidget);
|
||||
expect(find.text('Plain'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("a label's screen lists its open tasks, those alone", (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
final errand = await tester.runAsync(
|
||||
() => LabelRepository(
|
||||
labelRows,
|
||||
keys,
|
||||
).add(const LabelContent(name: 'errand')),
|
||||
);
|
||||
labelRows.bringRemoteChanges();
|
||||
await tasksOf(tester, {
|
||||
'Post office': (priority: Priority.none, labels: [errand!]),
|
||||
'Read a book': (priority: Priority.none, labels: const []),
|
||||
});
|
||||
|
||||
await openFilters(tester);
|
||||
await tester.tap(find.text('errand'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Post office'), findsOneWidget);
|
||||
expect(find.text('Read a book'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a filter deleted is gone, after asking', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await tester.runAsync(
|
||||
() => FilterRepository(
|
||||
filterRows,
|
||||
keys,
|
||||
).add(const FilterContent(name: 'Urgent', query: 'p1')),
|
||||
);
|
||||
filterRows.bringRemoteChanges();
|
||||
await tester.pumpAndSettle();
|
||||
await openFilters(tester);
|
||||
await tester.tap(find.text('Urgent'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.byTooltip('Filter menu'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete filter'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete filter').last);
|
||||
await tester.pumpAndSettle();
|
||||
filterRows.bringRemoteChanges();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
await tester.runAsync(
|
||||
() => FilterRepository(filterRows, keys).all(),
|
||||
),
|
||||
isEmpty,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('the tabs wash down from the top in strawberry', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue