feat(todo): each project's Display: completed tasks, grouping, sorting and filters, synced (ADR 0033, D17)
- A Display button beside a project's menu (the Inbox's too), a dot on it while the project shows otherwise than at first, opens its sheet: completed tasks, grouping (date, deadline, added date, priority, label), sorting (name, date, deadline, added date, priority), filters by date, deadline, priority and label, and Reset all. - The choices live in the project's sealed payload (ProjectView, written only once set), so every device shows the project alike. - Grouped, the groups stand in the sections' place under pinned headings; rows are dragged only while sorted by hand by sections. - Show done tasks moves from the menu into Display. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4898d0a04a
commit
7d1de304c8
2 changed files with 1878 additions and 81 deletions
|
|
@ -4,6 +4,8 @@ import 'dart:typed_data';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/data/project_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
||||
/// A project's payload (ADR 0033, D6): UTF-8 JSON with its schema's version,
|
||||
|
|
@ -26,6 +28,45 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test("a project's Display reads back as it was set, by names; left "
|
||||
'as at first, nothing of it is written (D17)', () {
|
||||
const view = ProjectView(
|
||||
completed: true,
|
||||
grouping: TaskGrouping.label,
|
||||
sorting: TaskSorting.priority,
|
||||
date: DayRange.next7Days,
|
||||
deadline: DayRange.none,
|
||||
priorities: {Priority.p1, Priority.none},
|
||||
labelIds: {'l1'},
|
||||
);
|
||||
const home = ProjectContent(name: 'Home', view: view);
|
||||
expect(decodeProject(encodeProject(home)), home);
|
||||
|
||||
final written = jsonDecode(
|
||||
utf8.decode(Uint8List.sublistView(encodeProject(home))),
|
||||
) as Map<String, Object?>;
|
||||
expect(written['view'], {
|
||||
'completed': true,
|
||||
'grouping': 'label',
|
||||
'sorting': 'priority',
|
||||
'date': 'next7Days',
|
||||
'deadline': 'none',
|
||||
'priorities': ['p1', 'none'],
|
||||
'labelIds': ['l1'],
|
||||
});
|
||||
});
|
||||
|
||||
test('a Display value this version does not know reads as at first', () {
|
||||
final read = decodeProject(
|
||||
json({
|
||||
'schemaVersion': 1,
|
||||
'name': 'Home',
|
||||
'view': {'layout': 'timeline', 'sorting': 'mood', 'completed': true},
|
||||
}),
|
||||
);
|
||||
expect(read?.view, const ProjectView(completed: true));
|
||||
});
|
||||
|
||||
test('the payload names its schema and the colour by its name', () {
|
||||
final written = jsonDecode(
|
||||
utf8.decode(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,232 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project_view.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
||||
|
||||
/// A project's Display (ADR 0033, D17): what it filters, how it sorts and
|
||||
/// groups.
|
||||
void main() {
|
||||
// Sunday, 1 Nov 2026; weeks from Monday.
|
||||
const today = Day(2026, 11, 1);
|
||||
const weekStart = Day(2026, 10, 26);
|
||||
|
||||
OpenTask task(
|
||||
String id, {
|
||||
double position = 0,
|
||||
Day? day,
|
||||
int? time,
|
||||
Day? deadline,
|
||||
Priority priority = Priority.none,
|
||||
List<String> labels = const [],
|
||||
String? section,
|
||||
}) => (
|
||||
id: id,
|
||||
content: TaskContent(
|
||||
title: id,
|
||||
projectId: 'p',
|
||||
sectionId: section,
|
||||
position: position,
|
||||
date: day == null ? null : TaskDate(day, time: time),
|
||||
deadline: deadline,
|
||||
priority: priority,
|
||||
labelIds: labels,
|
||||
),
|
||||
);
|
||||
|
||||
List<String> ids(List<OpenTask> tasks) => [for (final t in tasks) t.id];
|
||||
|
||||
List<String> filtered(List<OpenTask> tasks, ProjectView view) => ids([
|
||||
for (final t in tasks)
|
||||
if (view.shows(t.content, today: today, weekStart: weekStart)) t,
|
||||
]);
|
||||
|
||||
test('the default view filters nothing, sorts by hand, groups by '
|
||||
'sections, done tasks hidden', () {
|
||||
const view = ProjectView();
|
||||
expect(view.isDefault, isTrue);
|
||||
expect(view.layout, ProjectLayout.list);
|
||||
expect(view.completed, isFalse);
|
||||
expect(view.grouping, TaskGrouping.none);
|
||||
expect(view.sorting, TaskSorting.manual);
|
||||
expect(view.canReorder, isTrue);
|
||||
expect(view.copyWith(sorting: TaskSorting.name).canReorder, isFalse);
|
||||
expect(view.copyWith(grouping: TaskGrouping.date).canReorder, isFalse);
|
||||
expect(view.copyWith(completed: true).isDefault, isFalse);
|
||||
});
|
||||
|
||||
group('filters by date', () {
|
||||
final tasks = [
|
||||
task('none'),
|
||||
task('past', day: today.plus(-1)),
|
||||
task('today', day: today),
|
||||
task('in 3', day: today.plus(3)),
|
||||
task('in 10', day: today.plus(10)),
|
||||
task('in 29', day: today.plus(29)),
|
||||
task('in 31', day: today.plus(31)),
|
||||
];
|
||||
|
||||
for (final (range, expected) in [
|
||||
(
|
||||
DayRange.all,
|
||||
['none', 'past', 'today', 'in 3', 'in 10', 'in 29', 'in 31'],
|
||||
),
|
||||
(DayRange.today, ['today']),
|
||||
(DayRange.thisWeek, ['past', 'today']),
|
||||
(DayRange.next7Days, ['today', 'in 3']),
|
||||
(DayRange.thisMonth, ['today', 'in 3', 'in 10', 'in 29']),
|
||||
(DayRange.next30Days, ['today', 'in 3', 'in 10', 'in 29']),
|
||||
(DayRange.none, ['none']),
|
||||
]) {
|
||||
test(range.name, () {
|
||||
expect(filtered(tasks, ProjectView(date: range)), expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('filters by deadline, priority and label, all at once', () {
|
||||
final tasks = [
|
||||
task('a', deadline: today.plus(2), priority: Priority.p1, labels: ['x']),
|
||||
task('b', deadline: today.plus(2), priority: Priority.p2, labels: ['x']),
|
||||
task('c', priority: Priority.p1, labels: ['x']),
|
||||
task('d', deadline: today, priority: Priority.p1, labels: ['y']),
|
||||
];
|
||||
expect(
|
||||
filtered(tasks, const ProjectView(deadline: DayRange.next7Days)),
|
||||
['a', 'b', 'd'],
|
||||
);
|
||||
expect(filtered(tasks, const ProjectView(deadline: DayRange.none)), ['c']);
|
||||
expect(
|
||||
filtered(tasks, const ProjectView(priorities: {Priority.p1})),
|
||||
['a', 'c', 'd'],
|
||||
);
|
||||
expect(
|
||||
filtered(tasks, const ProjectView(priorities: {Priority.none})),
|
||||
isEmpty,
|
||||
);
|
||||
expect(filtered(tasks, const ProjectView(labelIds: {'y'})), ['d']);
|
||||
expect(
|
||||
filtered(
|
||||
tasks,
|
||||
const ProjectView(
|
||||
deadline: DayRange.next7Days,
|
||||
priorities: {Priority.p1},
|
||||
labelIds: {'x'},
|
||||
),
|
||||
),
|
||||
['a'],
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'sorts by name, date, deadline, priority; ties and the rest by hand',
|
||||
() {
|
||||
final tasks = [
|
||||
task('cherry', position: 1, day: today, time: 9 * 60),
|
||||
task('Apple', position: 2, deadline: today.plus(1)),
|
||||
task('banana', position: 3, day: today, priority: Priority.p1),
|
||||
task('date', day: today.plus(1), priority: Priority.p2),
|
||||
];
|
||||
List<String> sorted(TaskSorting sorting) =>
|
||||
ids([...tasks]..sort(ProjectView(sorting: sorting).compare));
|
||||
expect(sorted(TaskSorting.manual), ['date', 'cherry', 'Apple', 'banana']);
|
||||
expect(sorted(TaskSorting.name), ['Apple', 'banana', 'cherry', 'date']);
|
||||
expect(
|
||||
sorted(TaskSorting.date),
|
||||
['banana', 'cherry', 'date', 'Apple'],
|
||||
reason: 'a day without a time first on it, none last',
|
||||
);
|
||||
expect(sorted(TaskSorting.deadline), [
|
||||
'Apple',
|
||||
'date',
|
||||
'cherry',
|
||||
'banana',
|
||||
]);
|
||||
expect(sorted(TaskSorting.priority), [
|
||||
'banana',
|
||||
'date',
|
||||
'cherry',
|
||||
'Apple',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
group('groups', () {
|
||||
test('by date: overdue, each day, no date last', () {
|
||||
final groups = groupTasks(
|
||||
[
|
||||
task('later', day: today.plus(3)),
|
||||
task('none'),
|
||||
task('past', day: today.plus(-2)),
|
||||
task('now', day: today),
|
||||
],
|
||||
TaskGrouping.date,
|
||||
today: today,
|
||||
);
|
||||
expect(
|
||||
[for (final g in groups) g.key],
|
||||
[
|
||||
const OverdueGroup(),
|
||||
const DayGroup(today),
|
||||
DayGroup(today.plus(3)),
|
||||
const DayGroup(null),
|
||||
],
|
||||
);
|
||||
expect(ids(groups.first.tasks), ['past']);
|
||||
});
|
||||
|
||||
test('by priority, P1 first; by label, a task under each of its '
|
||||
'labels, those without last', () {
|
||||
final tasks = [
|
||||
task('low'),
|
||||
task('high', priority: Priority.p1, labels: ['b', 'a']),
|
||||
task('mid', priority: Priority.p3, labels: ['a']),
|
||||
];
|
||||
expect(
|
||||
[
|
||||
for (final g in groupTasks(
|
||||
tasks,
|
||||
TaskGrouping.priority,
|
||||
today: today,
|
||||
))
|
||||
(g.key, ids(g.tasks).join(',')),
|
||||
],
|
||||
[
|
||||
(const PriorityGroup(Priority.p1), 'high'),
|
||||
(const PriorityGroup(Priority.p3), 'mid'),
|
||||
(const PriorityGroup(Priority.none), 'low'),
|
||||
],
|
||||
);
|
||||
expect(
|
||||
[
|
||||
for (final g in groupTasks(
|
||||
tasks,
|
||||
TaskGrouping.label,
|
||||
today: today,
|
||||
labelOrder: ['a', 'b'],
|
||||
))
|
||||
(g.key, ids(g.tasks).join(',')),
|
||||
],
|
||||
[
|
||||
(const LabelGroup('a'), 'high,mid'),
|
||||
(const LabelGroup('b'), 'high'),
|
||||
(const LabelGroup(null), 'low'),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('by deadline, as by date', () {
|
||||
final groups = groupTasks(
|
||||
[task('a', deadline: today), task('b')],
|
||||
TaskGrouping.deadline,
|
||||
today: today,
|
||||
);
|
||||
expect(
|
||||
[for (final g in groups) g.key],
|
||||
[
|
||||
const DayGroup(today),
|
||||
const DayGroup(null),
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue