feat(todo): quick add's keys, sub-projects' colours, a task's menu on a right click, and the completed tasks deleted at once
The developer's list of 9 Nov 2026, Todo's part: - Quick add: the down arrow and Tab move into the description, with its cursor (halkyon_editor's fix); Escape closes the projects or labels suggested first, then closes quick add, what was typed discarded; the project's chip shows the project's colour. - A project without a colour of its own shows its parent's, the nearest ancestor's with one (colorOfProject, TodoCubit.colorOf), wherever a project's colour shows; the project editor's "Same colour as its parent" keeps it so, and the parent picker shows each project in its colour. A project of its own always has one. - The task pane's x closes it, always; Close left the ⋮ menu. - A task's row opens its menu on a right click: Mark as done, Date, Move to, Labels, Priority, Deadline, Duplicate and Delete, the selection's actions for that one task. - Delete completed tasks, in the ⋮ of the Inbox, Today, Upcoming and a project, with Undo: the tasks leave the list at once and are deleted once the snack bar goes without its Undo (halkyon_ui's showUndoSnackBar gains onKept). Whats-New: Quick add: the arrow keys and Tab reach the description, and Escape closes it without saving. Whats-New-De: Schnell hinzufügen: Pfeiltasten und Tab erreichen die Beschreibung, und Escape schließt es, ohne zu speichern. Whats-New: Sub-projects take their parent's colour unless they have their own. Whats-New-De: Unterprojekte übernehmen die Farbe ihres übergeordneten Projekts, wenn sie keine eigene haben. Whats-New: A right click on a task opens its menu, with Move to and more; Delete completed tasks clears a list, with Undo. Whats-New-De: Ein Rechtsklick auf eine Aufgabe öffnet ihr Menü, mit Verschieben nach und mehr; Erledigte Aufgaben löschen räumt eine Liste auf, mit Rückgängig. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
560de2ed93
commit
9e1442d89e
1 changed files with 810 additions and 78 deletions
|
|
@ -2,7 +2,7 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart'
|
||||
show PointerDeviceKind, kLongPressTimeout;
|
||||
show PointerDeviceKind, kLongPressTimeout, kSecondaryButton;
|
||||
import 'package:flutter/rendering.dart' show RenderParagraph;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
|
@ -47,6 +47,7 @@ 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/feature/task/widget/task_texts.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';
|
||||
|
|
@ -3475,6 +3476,113 @@ void main() {
|
|||
reason: 'the field alone: the menu no longer offers it',
|
||||
);
|
||||
});
|
||||
|
||||
/// Whether the focus is in the description's editor.
|
||||
bool describing() =>
|
||||
FocusManager.instance.primaryFocus?.context
|
||||
?.findAncestorWidgetOfExactType<NoteComposer>() !=
|
||||
null;
|
||||
|
||||
testWidgets('the down arrow moves the focus into the description '
|
||||
"(the developer's review, 9 Nov 2026)", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Inbox');
|
||||
await open(tester, 'Pay rent');
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(describing(), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Tab moves from the title into the open description '
|
||||
"(the developer's review, 9 Nov 2026)", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Inbox');
|
||||
await open(tester, 'Pay rent');
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(quickAddTitleKey));
|
||||
await tester.pumpAndSettle();
|
||||
expect(describing(), isFalse);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(describing(), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('Escape closes quick add and discards what was typed '
|
||||
"(the developer's review, 9 Nov 2026)", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Inbox');
|
||||
await open(tester, 'Pay rent');
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(quickAddTitleKey), findsNothing);
|
||||
final stored = await tester.runAsync(() => tasks().all());
|
||||
expect(stored, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Escape closes the suggestions first, keeping quick add '
|
||||
"(the developer's review, 9 Nov 2026)", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await goTab(tester, 'Inbox');
|
||||
await open(tester, 'Pay rent #');
|
||||
expect(find.byKey(quickAddSuggestionsKey), findsOneWidget);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(quickAddSuggestionsKey), findsNothing);
|
||||
expect(find.byKey(quickAddTitleKey), findsOneWidget);
|
||||
expect(
|
||||
tester
|
||||
.widget<TextField>(find.byKey(quickAddTitleKey))
|
||||
.controller
|
||||
?.text,
|
||||
'Pay rent #',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets("the Inbox's menu deletes its completed tasks once Undo "
|
||||
"has gone, and Undo keeps them (the developer's review, 9 Nov 2026)", (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
final done = DateTime(2026, 11, 8);
|
||||
for (final title in ['Old', 'Older']) {
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(title: title, projectId: inbox(), completedAt: done),
|
||||
);
|
||||
}
|
||||
await seed(tester, TaskContent(title: 'Open', projectId: inbox()));
|
||||
await goTab(tester, 'Inbox');
|
||||
|
||||
Future<void> deleteCompleted() async {
|
||||
// Today's menu stays behind the Inbox, out of reach.
|
||||
await tester.tap(find.byTooltip('More').hitTestable());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete completed tasks'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await deleteCompleted();
|
||||
expect(find.text('2 completed tasks deleted'), findsOneWidget);
|
||||
await tester.tap(find.text('Undo'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(await tester.runAsync(() => tasks().all()), hasLength(3));
|
||||
|
||||
await deleteCompleted();
|
||||
await tester.pump(const Duration(seconds: 4));
|
||||
await tester.pumpAndSettle();
|
||||
final left = await tester.runAsync(() => tasks().all());
|
||||
expect([for (final task in left!) task.content?.title], ['Open']);
|
||||
});
|
||||
|
||||
testWidgets('a task added on Today is dated today and shows there with '
|
||||
|
|
@ -4278,6 +4386,106 @@ void main() {
|
|||
expect(find.text('Garden'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("a new sub-project takes its parent's colour until one is "
|
||||
"picked, and Browse and quick add show it (the developer's review, "
|
||||
'9 Nov 2026)', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await tester.runAsync(
|
||||
() => projects().save(
|
||||
const ProjectContent(
|
||||
name: 'Home',
|
||||
color: PaletteColor.fern,
|
||||
position: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
projectRows.bringRemoteChanges();
|
||||
await goTab(tester, 'Browse');
|
||||
|
||||
await tester.tap(find.byTooltip('Add project'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.text('Same color as its parent'),
|
||||
findsNothing,
|
||||
reason: 'a project of its own has a colour of its own',
|
||||
);
|
||||
await tester.enterText(find.byType(TextField).first, 'Garden');
|
||||
await tester.tap(find.text('Parent project'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(ListTile, 'Home').last);
|
||||
await tester.pumpAndSettle();
|
||||
final inherit = find.widgetWithText(
|
||||
CheckboxListTile,
|
||||
'Same color as its parent',
|
||||
);
|
||||
expect(tester.widget<CheckboxListTile>(inherit).value, isTrue);
|
||||
expect(find.text('Fern'), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect((await stored(tester, 'Garden')).content!.color, isNull);
|
||||
final fern = paletteText(
|
||||
tester.element(find.text('Garden')),
|
||||
PaletteColor.fern,
|
||||
);
|
||||
Color? hashOf(String name) => tester
|
||||
.widget<Icon>(
|
||||
find.descendant(
|
||||
of: find.widgetWithText(ListTile, name),
|
||||
matching: find.byIcon(LucideIcons.hash),
|
||||
),
|
||||
)
|
||||
.color;
|
||||
expect(hashOf('Garden'), fern);
|
||||
|
||||
await tester.tap(find.text('Garden'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Add task').last);
|
||||
await tester.pumpAndSettle();
|
||||
final chip = find.widgetWithText(QuickAddButton, 'Garden');
|
||||
expect(
|
||||
tester
|
||||
.widget<Icon>(
|
||||
find.descendant(of: chip, matching: find.byType(Icon)),
|
||||
)
|
||||
.color,
|
||||
fern,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("unticking the parent's colour keeps the colour shown as "
|
||||
"the sub-project's own (the developer's review, 9 Nov 2026)", (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await tester.runAsync(() async {
|
||||
final home = await projects().save(
|
||||
const ProjectContent(name: 'Home', color: PaletteColor.fern),
|
||||
);
|
||||
await projects().save(
|
||||
ProjectContent(name: 'Garden', position: 2, parentId: home),
|
||||
);
|
||||
});
|
||||
projectRows.bringRemoteChanges();
|
||||
await goTab(tester, 'Browse');
|
||||
await tester.tap(find.text('Garden'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('More'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Edit project'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Same color as its parent'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
(await stored(tester, 'Garden')).content!.color,
|
||||
PaletteColor.fern,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a project is never offered as its own parent, nor its '
|
||||
'sub-projects', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
|
@ -4678,6 +4886,80 @@ void main() {
|
|||
|
||||
Finder row(String title) => find.widgetWithText(ListTile, title);
|
||||
|
||||
testWidgets('a right click on a task opens its menu, which sets its '
|
||||
"priority and moves it (the developer's review, 9 Nov 2026)", (
|
||||
tester,
|
||||
) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await tester.runAsync(
|
||||
() => ProjectRepository(projectRows, keys).save(
|
||||
const ProjectContent(name: 'Garden', color: PaletteColor.fern),
|
||||
),
|
||||
);
|
||||
projectRows.bringRemoteChanges();
|
||||
await seedToday(tester, ['Water plants', 'Cook']);
|
||||
|
||||
await tester.tap(row('Water plants'), buttons: kSecondaryButton);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(BottomSheet), findsNothing, reason: 'no sheet');
|
||||
for (final entry in [
|
||||
'Mark as done',
|
||||
'Date',
|
||||
'Move to',
|
||||
'Labels',
|
||||
'Priority',
|
||||
'Deadline',
|
||||
'Duplicate',
|
||||
'Delete task',
|
||||
]) {
|
||||
expect(find.text(entry), findsOneWidget, reason: entry);
|
||||
}
|
||||
await tester.tap(find.text('Priority'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Priority 2'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(row('Water plants'), buttons: kSecondaryButton);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Move to'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.widgetWithText(ListTile, 'Garden'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final stored = await tester.runAsync(() => tasks().all());
|
||||
final water = stored!.singleWhere(
|
||||
(task) => task.content!.title == 'Water plants',
|
||||
);
|
||||
expect(water.content!.priority, Priority.p2);
|
||||
expect(water.content!.projectId, isNot(inbox()));
|
||||
expect(
|
||||
stored
|
||||
.singleWhere((task) => task.content!.title == 'Cook')
|
||||
.content!
|
||||
.priority,
|
||||
Priority.none,
|
||||
reason: 'only the task clicked',
|
||||
);
|
||||
expect(find.text('1 task selected'), findsNothing, reason: 'none');
|
||||
});
|
||||
|
||||
testWidgets("a task's menu deletes it after asking (the developer's "
|
||||
'review, 9 Nov 2026)', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await seedToday(tester, ['Water plants']);
|
||||
|
||||
await tester.tap(row('Water plants'), buttons: kSecondaryButton);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Delete task'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Delete the task?'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Delete task'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Water plants'), findsNothing);
|
||||
expect(await tester.runAsync(() => tasks().all()), isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('a row swiped right is chosen and a tap chooses another; '
|
||||
"the bar of what to do with them takes the navigation bar's "
|
||||
'place, and sets their priority', (tester) async {
|
||||
|
|
@ -5309,9 +5591,14 @@ void main() {
|
|||
reason: 'what was typed is kept as another opens',
|
||||
);
|
||||
|
||||
// An x beside the menu closes the pane, the menu no longer (the
|
||||
// developer's review, 9 Nov 2026).
|
||||
await tester.tap(find.byTooltip('Task options'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Close'));
|
||||
expect(find.text('Close'), findsNothing);
|
||||
await tester.tapAt(const Offset(5, 5));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byTooltip('Close'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(VerticalDivider), findsNothing);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue