feat(todo): a project's Calendar, Upcoming's Display and its 3-day grid (ADR 0033, D17)
A project's Display gains the Calendar layout: Upcoming's week strip and agenda, now shared as DayAgenda, over the project's tasks it keeps, with No date beside the month and Future occurrences, each repeating task also on the days it will come back (calendarOf on Repeat.next), muted, neither ticked off nor dragged. Upcoming gets a Display of its own, the same sheet over another target (DisplayTarget): Agenda or 3-day, done tasks, future occurrences, sorting, the filters by deadline, priority and label, and in the grid Colouring by priority, label or project. It is kept in the person's settings, one sealed row under an ID keyed by the account in the new synced table todo_settings (migration settings: the table, its index on spaceId and its foreign key to the space, cascading on delete). The 3-day grid shows today and the two days after, stepped or swiped three at a time: tasks without a time all day, timed ones as blocks as long as they last, overlapping ones side by side (layOutDay). A long press on a half hour adds a task there for half an hour; Overdue opens the overdue tasks with Reschedule. The board's separator builder names its BuildContext, as the root's analyzer plugin asks (never_discard_build_context). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
e4fbfb54d1
commit
2939437c74
1 changed files with 21845 additions and 314 deletions
|
|
@ -13,14 +13,17 @@ import 'package:halkyon_todo_flutter/src/feature/project/data/project_repository
|
|||
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/project/widget/project_board.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_calendar_view.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/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.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/widget/quick_add_sheet.dart';
|
||||
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/tabs/three_day_grid.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/week_strip.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -42,6 +45,7 @@ void main() {
|
|||
late MemorySealedRowGateway sectionRows;
|
||||
late MemorySealedRowGateway taskRows;
|
||||
late MemorySealedRowGateway labelRows;
|
||||
late MemorySealedRowGateway settingsRows;
|
||||
late MemoryReminderGateway reminders;
|
||||
|
||||
tearDown(() async {
|
||||
|
|
@ -65,6 +69,7 @@ void main() {
|
|||
sectionRows = MemorySealedRowGateway();
|
||||
taskRows = MemorySealedRowGateway();
|
||||
labelRows = MemorySealedRowGateway();
|
||||
settingsRows = MemorySealedRowGateway();
|
||||
reminders = MemoryReminderGateway();
|
||||
store = MemoryInstanceStore();
|
||||
gateway = FakeAuthGateway();
|
||||
|
|
@ -148,6 +153,7 @@ void main() {
|
|||
sections: SectionRepository(sectionRows, keys),
|
||||
tasks: TaskRepository(taskRows, keys),
|
||||
labels: LabelRepository(labelRows, keys),
|
||||
settings: SettingsRepository(settingsRows, keys),
|
||||
reminders: reminders,
|
||||
);
|
||||
},
|
||||
|
|
@ -612,6 +618,219 @@ void main() {
|
|||
expect(find.text('Errands 2'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
group('Calendar', () {
|
||||
Future<void> calendarOf(WidgetTester tester) async {
|
||||
await inboxOf(tester);
|
||||
await display(tester);
|
||||
await tester.tap(find.text('Calendar'));
|
||||
await tester.pumpAndSettle();
|
||||
await done(tester);
|
||||
}
|
||||
|
||||
testWidgets('the week strip and the days with the dated tasks; those '
|
||||
'without a date under No date; kept in the project', (
|
||||
tester,
|
||||
) async {
|
||||
await calendarOf(tester);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Dentist',
|
||||
projectId: inbox(),
|
||||
date: TaskDate(Day.of(DateTime.now()).plus(1)),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byType(WeekStrip), findsOneWidget);
|
||||
expect(row('Dentist'), findsOneWidget);
|
||||
expect(row('cherry'), findsNothing, reason: 'no date');
|
||||
final projects = await tester.runAsync(
|
||||
() => ProjectRepository(projectRows, keys).all(),
|
||||
);
|
||||
expect(
|
||||
projects!
|
||||
.singleWhere((project) => project.id == inbox())
|
||||
.content
|
||||
?.view
|
||||
.layout,
|
||||
ProjectLayout.calendar,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(ProjectCalendarView.noDateKey));
|
||||
await tester.pumpAndSettle();
|
||||
for (final title in ['cherry', 'Apple', 'banana']) {
|
||||
expect(row(title), findsOneWidget);
|
||||
}
|
||||
expect(row('Dentist'), findsNWidgets(1), reason: 'behind the sheet');
|
||||
});
|
||||
|
||||
testWidgets('Future occurrences shows a repeating task on its later '
|
||||
'days too, not ticked off there; grouping is left out', (
|
||||
tester,
|
||||
) async {
|
||||
await calendarOf(tester);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Water plants',
|
||||
projectId: inbox(),
|
||||
date: TaskDate(
|
||||
Day.of(DateTime.now()),
|
||||
repeat: const Repeat(RepeatUnit.day),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(row('Water plants'), findsOneWidget);
|
||||
|
||||
await display(tester);
|
||||
expect(find.widgetWithText(ListTile, 'Grouping'), findsNothing);
|
||||
await tester.tap(
|
||||
find.widgetWithText(SwitchListTile, 'Future occurrences'),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await done(tester);
|
||||
|
||||
expect(row('Water plants').evaluate().length, greaterThan(7));
|
||||
expect(
|
||||
find.byTooltip('A later time of a repeating task'),
|
||||
findsNWidgets(row('Water plants').evaluate().length - 1),
|
||||
);
|
||||
expect(find.byTooltip('Mark as done'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
});
|
||||
group("Upcoming's Display (D17)", () {
|
||||
Future<void> display(WidgetTester tester) async {
|
||||
await tester.tap(find.byTooltip('Display'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> done(WidgetTester tester) async {
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Done'));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> threeDay(WidgetTester tester) async {
|
||||
await goTab(tester, 'Upcoming');
|
||||
await display(tester);
|
||||
await tester.tap(find.text('3-day'));
|
||||
await tester.pumpAndSettle();
|
||||
await done(tester);
|
||||
}
|
||||
|
||||
testWidgets('3-day shows today and the next two days, a timed task as '
|
||||
'a block and one without a time all day; kept in the settings, '
|
||||
'sealed', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
final today = Day.of(DateTime.now());
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Dentist',
|
||||
projectId: inbox(),
|
||||
date: TaskDate(today.plus(1), time: 9 * 60),
|
||||
),
|
||||
);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Shop',
|
||||
projectId: inbox(),
|
||||
date: TaskDate(today.plus(2)),
|
||||
),
|
||||
);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Far',
|
||||
projectId: inbox(),
|
||||
date: TaskDate(today.plus(3)),
|
||||
),
|
||||
);
|
||||
await threeDay(tester);
|
||||
|
||||
expect(find.byType(ThreeDayGrid), findsOneWidget);
|
||||
for (final day in [today, today.plus(1), today.plus(2)]) {
|
||||
expect(find.byKey(ThreeDayGrid.columnKey(day)), findsOneWidget);
|
||||
}
|
||||
final stored = await tester.runAsync(() => tasks().all());
|
||||
final dentist = find.byKey(
|
||||
ThreeDayGrid.blockKey(
|
||||
stored!.singleWhere((t) => t.content?.title == 'Dentist').id,
|
||||
today.plus(1),
|
||||
),
|
||||
);
|
||||
expect(dentist, findsOneWidget);
|
||||
expect(tester.getSize(dentist).height, closeTo(28, 1));
|
||||
expect(find.text('Shop'), findsOneWidget);
|
||||
expect(find.text('Far'), findsNothing);
|
||||
expect(settingsRows.byId.values.single.keyId, isNotNull);
|
||||
|
||||
await tester.tap(find.byTooltip('Later days'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Far'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a long press on a half hour adds a task there for half an '
|
||||
'hour', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await threeDay(tester);
|
||||
final today = Day.of(DateTime.now());
|
||||
final column = find.byKey(ThreeDayGrid.columnKey(today));
|
||||
// 10:00 is 600 logical pixels down the day.
|
||||
await tester.longPressAt(
|
||||
tester.getTopLeft(column) + const Offset(20, 10 * 60 + 10),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byKey(quickAddTitleKey), 'Call');
|
||||
await tester.pump();
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final stored = await tester.runAsync(() => tasks().all());
|
||||
final date = stored!.single.content!.date!;
|
||||
expect(date.day, today);
|
||||
expect(date.time, 10 * 60);
|
||||
expect(date.duration, const Duration(minutes: 30));
|
||||
});
|
||||
|
||||
testWidgets('its filters keep the agenda to what they ask for; Reset '
|
||||
'all puts it back', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
final today = Day.of(DateTime.now());
|
||||
for (final (title, priority) in [
|
||||
('Urgent', Priority.p1),
|
||||
('Someday', Priority.none),
|
||||
]) {
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: title,
|
||||
projectId: inbox(),
|
||||
priority: priority,
|
||||
date: TaskDate(today),
|
||||
),
|
||||
);
|
||||
}
|
||||
await goTab(tester, 'Upcoming');
|
||||
expect(find.text('Someday'), findsOneWidget);
|
||||
|
||||
await display(tester);
|
||||
await tester.tap(find.widgetWithText(ListTile, 'Priority'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(CheckboxListTile, 'Priority 1'));
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Save'));
|
||||
await tester.pumpAndSettle();
|
||||
await done(tester);
|
||||
expect(find.text('Urgent'), findsOneWidget);
|
||||
expect(find.text('Someday'), findsNothing);
|
||||
|
||||
await display(tester);
|
||||
await tester.tap(find.text('Reset all'));
|
||||
await tester.pumpAndSettle();
|
||||
await done(tester);
|
||||
expect(find.text('Someday'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('quick add reads what is typed (D15)', () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue