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
4 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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/sealed/json_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/data/filter_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/model/saved_filter.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/label/data/label_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/label/model/label.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
||||
void main() {
|
||||
test('a filter comes back as it went', () {
|
||||
const filter = FilterContent(
|
||||
name: 'Urgent',
|
||||
query: 'today & p1',
|
||||
description: "What can't wait",
|
||||
color: PaletteColor.cobalt,
|
||||
favorite: true,
|
||||
position: 3,
|
||||
);
|
||||
|
||||
expect(decodeFilter(encodeFilter(filter)), filter);
|
||||
});
|
||||
|
||||
test('the optional parts stay out of a plain filter', () {
|
||||
const filter = FilterContent(name: 'Mine', query: '#Home');
|
||||
|
||||
expect(decodeFilter(encodeFilter(filter)), filter);
|
||||
});
|
||||
|
||||
test('a filter of a later schema is not read', () {
|
||||
expect(
|
||||
decodeFilter(
|
||||
encodeJson(filterSchemaVersion + 1, {'name': 'New', 'query': 'today'}),
|
||||
),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
|
||||
test('a label keeps being a favourite, and older labels are none', () {
|
||||
const label = LabelContent(name: 'errand', favorite: true);
|
||||
|
||||
expect(decodeLabel(encodeLabel(label)), label);
|
||||
expect(
|
||||
decodeLabel(encodeLabel(const LabelContent(name: 'old')))?.favorite,
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/filter/model/filter_query.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/repeat.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
||||
|
||||
/// Filters' language (ADR 0033, D35), on the device alone.
|
||||
void main() {
|
||||
const today = Day(2026, 11, 6);
|
||||
const world = FilterWorld(
|
||||
today: today,
|
||||
projects: {
|
||||
'work': (name: 'Halkyon Work', parentId: null),
|
||||
'docs': (name: 'Docs', parentId: 'work'),
|
||||
'home': (name: 'Zuhause', parentId: null),
|
||||
},
|
||||
sections: {'errands': 'Errands'},
|
||||
labels: {'l1': 'Einkauf', 'l2': 'waiting'},
|
||||
);
|
||||
|
||||
OpenTask task({
|
||||
String project = 'home',
|
||||
String? section,
|
||||
String? parent,
|
||||
String title = 'Task',
|
||||
String description = '',
|
||||
TaskDate? date,
|
||||
Day? deadline,
|
||||
Priority priority = Priority.none,
|
||||
List<String> labels = const [],
|
||||
DateTime? done,
|
||||
}) => (
|
||||
id: title,
|
||||
content: TaskContent(
|
||||
title: title,
|
||||
description: description,
|
||||
projectId: project,
|
||||
sectionId: section,
|
||||
parentId: parent,
|
||||
date: date,
|
||||
deadline: deadline,
|
||||
priority: priority,
|
||||
labelIds: labels,
|
||||
completedAt: done,
|
||||
),
|
||||
);
|
||||
|
||||
FilterQuery query(String text) => switch (FilterQuery.parse(text)) {
|
||||
FilterRead(:final query) => query,
|
||||
FilterUnread(:final near) => fail('$text not read, near "$near"'),
|
||||
};
|
||||
|
||||
bool hits(String text, OpenTask task) => query(text).matches(task, world);
|
||||
|
||||
group('dates', () {
|
||||
final due = task(date: const TaskDate(today));
|
||||
final tomorrow = task(date: TaskDate(today.plus(1)));
|
||||
final late = task(date: TaskDate(today.plus(-1)));
|
||||
final none = task();
|
||||
|
||||
test('today, tomorrow, yesterday', () {
|
||||
expect(hits('today', due), isTrue);
|
||||
expect(hits('today', tomorrow), isFalse);
|
||||
expect(hits('tomorrow', tomorrow), isTrue);
|
||||
expect(hits('yesterday', late), isTrue);
|
||||
});
|
||||
|
||||
test('overdue is what is dated before today', () {
|
||||
expect(hits('overdue', late), isTrue);
|
||||
expect(hits('od', late), isTrue);
|
||||
expect(hits('overdue', due), isFalse);
|
||||
expect(hits('overdue', none), isFalse);
|
||||
});
|
||||
|
||||
test('a number of days starts today, and leaves out the overdue', () {
|
||||
expect(hits('7 days', due), isTrue);
|
||||
expect(hits('7 days', task(date: TaskDate(today.plus(6)))), isTrue);
|
||||
expect(hits('7 days', task(date: TaskDate(today.plus(7)))), isFalse);
|
||||
expect(hits('next 7 days', late), isFalse);
|
||||
});
|
||||
|
||||
test('no date, and a day named', () {
|
||||
expect(hits('no date', none), isTrue);
|
||||
expect(hits('no date', due), isFalse);
|
||||
expect(hits('2026-11-06', due), isTrue);
|
||||
expect(hits('before: 2026-11-07', due), isTrue);
|
||||
expect(hits('after: 2026-11-06', due), isFalse);
|
||||
expect(hits('after: 2026-11-05', due), isTrue);
|
||||
expect(hits('before: 2026-11-07', none), isFalse);
|
||||
});
|
||||
|
||||
test('a time, and a recurrence', () {
|
||||
expect(hits('no time', due), isTrue);
|
||||
expect(hits('no time', none), isFalse);
|
||||
expect(
|
||||
hits('no time', task(date: const TaskDate(today, time: 60))),
|
||||
isFalse,
|
||||
);
|
||||
expect(hits('recurring', due), isFalse);
|
||||
expect(
|
||||
hits(
|
||||
'recurring',
|
||||
task(
|
||||
date: const TaskDate(
|
||||
today,
|
||||
repeat: Repeat(RepeatUnit.day),
|
||||
),
|
||||
),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('a deadline', () {
|
||||
expect(hits('deadline', task(deadline: today)), isTrue);
|
||||
expect(hits('deadline', none), isFalse);
|
||||
expect(hits('no deadline', none), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('what a task belongs to', () {
|
||||
test('priorities, p4 being none', () {
|
||||
expect(hits('p1', task(priority: Priority.p1)), isTrue);
|
||||
expect(hits('p1', task(priority: Priority.p2)), isFalse);
|
||||
expect(hits('p4', task()), isTrue);
|
||||
expect(hits('p4', task(priority: Priority.p3)), isFalse);
|
||||
});
|
||||
|
||||
test('a project by its name, with spaces, whatever the case', () {
|
||||
expect(hits('#halkyon work', task(project: 'work')), isTrue);
|
||||
expect(hits('#Halkyon Work', task(project: 'docs')), isFalse);
|
||||
});
|
||||
|
||||
test('## takes in its sub-projects', () {
|
||||
expect(hits('##Halkyon Work', task(project: 'docs')), isTrue);
|
||||
expect(hits('##Halkyon Work', task()), isFalse);
|
||||
});
|
||||
|
||||
test('a section, a label, and no labels', () {
|
||||
expect(hits('/errands', task(section: 'errands')), isTrue);
|
||||
expect(hits('/errands', task()), isFalse);
|
||||
expect(hits('@einkauf', task(labels: ['l1'])), isTrue);
|
||||
expect(hits('@einkauf', task(labels: ['l2'])), isFalse);
|
||||
expect(hits('no labels', task()), isTrue);
|
||||
expect(hits('no labels', task(labels: ['l1'])), isFalse);
|
||||
});
|
||||
|
||||
test('* stands for anything in a name', () {
|
||||
expect(hits('#Halkyon*', task(project: 'work')), isTrue);
|
||||
expect(hits('@*ing', task(labels: ['l2'])), isTrue);
|
||||
expect(hits('@*ing', task(labels: ['l1'])), isFalse);
|
||||
});
|
||||
|
||||
test('a name that is no project matches nothing, and is no mistake', () {
|
||||
expect(hits('#Nowhere', task()), isFalse);
|
||||
});
|
||||
|
||||
test('sub-tasks, and words in a task', () {
|
||||
expect(hits('subtask', task(parent: 'x')), isTrue);
|
||||
expect(hits('!subtask', task(parent: 'x')), isFalse);
|
||||
expect(hits('search: milk', task(title: 'Buy MILK')), isTrue);
|
||||
expect(hits('search: milk', task(description: 'milk')), isTrue);
|
||||
expect(hits('search: milk', task(title: 'Buy eggs')), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('putting them together', () {
|
||||
final urgent = task(priority: Priority.p1, date: const TaskDate(today));
|
||||
final later = task(priority: Priority.p1);
|
||||
|
||||
test('& and |, & first', () {
|
||||
expect(hits('today & p1', urgent), isTrue);
|
||||
expect(hits('today & p1', later), isFalse);
|
||||
expect(hits('today | p1', later), isTrue);
|
||||
expect(hits('p2 | today & p1', urgent), isTrue);
|
||||
expect(hits('p2 | today & p3', urgent), isFalse);
|
||||
});
|
||||
|
||||
test('! and parentheses', () {
|
||||
expect(hits('!today', later), isTrue);
|
||||
expect(hits('!today', urgent), isFalse);
|
||||
expect(hits('!!today', urgent), isTrue);
|
||||
expect(hits('(p2 | p1) & today', urgent), isTrue);
|
||||
expect(hits('!(p1 & today)', urgent), isFalse);
|
||||
});
|
||||
|
||||
test('never what is done', () {
|
||||
expect(
|
||||
hits('p1', task(priority: Priority.p1, done: DateTime(2026))),
|
||||
isFalse,
|
||||
);
|
||||
expect(hits('!p2', task(done: DateTime(2026))), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('German', () {
|
||||
test('its words read as the English ones', () {
|
||||
final due = task(date: const TaskDate(today));
|
||||
expect(hits('heute', due), isTrue);
|
||||
expect(hits('Morgen', task(date: TaskDate(today.plus(1)))), isTrue);
|
||||
expect(hits('überfällig', task(date: TaskDate(today.plus(-1)))), isTrue);
|
||||
expect(hits('ohne datum', task()), isTrue);
|
||||
expect(hits('7 tage', due), isTrue);
|
||||
expect(hits('nächste 7 tage', due), isTrue);
|
||||
expect(hits('ohne labels', task()), isTrue);
|
||||
expect(hits('wiederkehrend', due), isFalse);
|
||||
expect(hits('frist', task(deadline: today)), isTrue);
|
||||
expect(hits('suche: milch', task(title: 'Milch')), isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('what it refuses, saying where', () {
|
||||
String? near(String text) => switch (FilterQuery.parse(text)) {
|
||||
FilterRead() => null,
|
||||
FilterUnread(:final near) => near,
|
||||
};
|
||||
|
||||
test('a word it does not know', () {
|
||||
expect(near('today & someday'), 'someday');
|
||||
expect(near('banana'), 'banana');
|
||||
});
|
||||
|
||||
test('nothing at all, and what is left dangling', () {
|
||||
expect(near(''), '');
|
||||
expect(near(' '), '');
|
||||
expect(near('today &'), '');
|
||||
expect(near('| today'), '| today');
|
||||
expect(near('(today'), '');
|
||||
expect(near('today)'), ')');
|
||||
expect(near('#'), '#');
|
||||
expect(near('before: soon'), 'before: soon');
|
||||
expect(near('0 days'), '0 days');
|
||||
});
|
||||
});
|
||||
|
||||
group('the tasks a filter shows', () {
|
||||
test('dated first, the nearest first, then by priority', () {
|
||||
final tasks = [
|
||||
task(title: 'none', priority: Priority.p1),
|
||||
task(title: 'later', date: TaskDate(today.plus(2))),
|
||||
task(title: 'soon', date: const TaskDate(today)),
|
||||
];
|
||||
expect(
|
||||
[
|
||||
for (final task in tasksOfFilter(query('p1 | !p1'), tasks, world))
|
||||
task.id,
|
||||
],
|
||||
['soon', 'later', 'none'],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import 'package:clock/clock.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/halkyon_app.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/label/data/label_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/label/model/label.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/section/data/section_repository.dart';
|
||||
|
|
@ -501,5 +503,18 @@ void main() {
|
|||
LabelRepository(rows, await keysOf(null)).add(label),
|
||||
throwsA(isA<KeysMissing>()),
|
||||
);
|
||||
|
||||
final filters = FilterRepository(rows, await keysOf(alice));
|
||||
const filter = FilterContent(name: 'Urgent', query: 'p1');
|
||||
await filters.add(filter);
|
||||
expect((await filters.all()).last.content, filter);
|
||||
expect(
|
||||
(await FilterRepository(rows, await keysOf(bob)).all()).last.content,
|
||||
isNull,
|
||||
);
|
||||
await expectLater(
|
||||
FilterRepository(rows, await keysOf(null)).add(filter),
|
||||
throwsA(isA<KeysMissing>()),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue