feat(todo): a task's sub-tasks held and dragged into another order in its sheet
As a project's rows are (D16): a long press on a sub-task in the task's sheet or pane lifts it, and dropped on the upper or lower half of another it goes there, still the task's sub-task (the developer's review, 10 Nov 2026). Whats-New: In a task's details, hold a sub-task to drag it into another order. Whats-New-De: In den Details einer Aufgabe hältst du eine Unteraufgabe gedrückt, um sie in eine andere Reihenfolge zu ziehen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
ca9ad6c69c
commit
607ac71233
3 changed files with 92 additions and 3 deletions
File diff suppressed because one or more lines are too long
|
|
@ -27,6 +27,7 @@ import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/date_picker.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/date_picker.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/quick_add_sheet.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/quick_add_sheet.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/repeat_picker.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/repeat_picker.dart';
|
||||||
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_drag.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_pickers.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_pickers.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.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/feature/task/widget/task_tile.dart';
|
||||||
|
|
@ -782,6 +783,10 @@ class _TaskSheetState() extends State<_TaskSheet> {
|
||||||
final today = Day.of(clock.now());
|
final today = Day.of(clock.now());
|
||||||
final project = state.projectOf(content.projectId);
|
final project = state.projectOf(content.projectId);
|
||||||
final subTasks = subTasksOf(state.tasks, task.id);
|
final subTasks = subTasksOf(state.tasks, task.id);
|
||||||
|
final shownSubTasks = [
|
||||||
|
for (final sub in subTasks)
|
||||||
|
if (!(content.hideDoneSubTasks && sub.content.completed)) sub,
|
||||||
|
];
|
||||||
final labels = {
|
final labels = {
|
||||||
for (final label in state.labels) label.id: label.content!.name,
|
for (final label in state.labels) label.id: label.content!.name,
|
||||||
};
|
};
|
||||||
|
|
@ -1009,13 +1014,17 @@ class _TaskSheetState() extends State<_TaskSheet> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (final sub in subTasks)
|
for (final (index, sub) in shownSubTasks.indexed)
|
||||||
if (!(content.hideDoneSubTasks && sub.content.completed))
|
_placedSubTask(
|
||||||
|
task,
|
||||||
|
shownSubTasks,
|
||||||
|
index,
|
||||||
TaskTile(
|
TaskTile(
|
||||||
task: sub,
|
task: sub,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
onOpen: (id) => _open(context, id),
|
onOpen: (id) => _open(context, id),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
icon: const Icon(LucideIcons.plus),
|
icon: const Icon(LucideIcons.plus),
|
||||||
|
|
@ -1036,6 +1045,39 @@ class _TaskSheetState() extends State<_TaskSheet> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The [index]th of [task]'s [subTasks] shown, as [child], held to be
|
||||||
|
/// dragged to another place among them and taking a drop above or below
|
||||||
|
/// it, staying [task]'s sub-task (the developer's review, 10 Nov 2026).
|
||||||
|
static Widget _placedSubTask(
|
||||||
|
OpenTask task,
|
||||||
|
List<OpenTask> subTasks,
|
||||||
|
int index,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
final sub = subTasks[index];
|
||||||
|
final sectionId = sub.content.sectionId;
|
||||||
|
return TaskDropTarget(
|
||||||
|
key: ValueKey('drop-${sub.id}'),
|
||||||
|
projectId: sub.content.projectId,
|
||||||
|
excluded: sub.id,
|
||||||
|
above: DropSpot(
|
||||||
|
sectionId: sectionId,
|
||||||
|
parentId: task.id,
|
||||||
|
after: index > 0 ? subTasks[index - 1].content.position : null,
|
||||||
|
before: sub.content.position,
|
||||||
|
),
|
||||||
|
below: DropSpot(
|
||||||
|
sectionId: sectionId,
|
||||||
|
parentId: task.id,
|
||||||
|
after: sub.content.position,
|
||||||
|
before: index + 1 < subTasks.length
|
||||||
|
? subTasks[index + 1].content.position
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: DraggableTask(task: sub, child: child),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Asks for the task's reminders and writes them, asking first to
|
/// Asks for the task's reminders and writes them, asking first to
|
||||||
/// remind where nothing was to be reminded of before.
|
/// remind where nothing was to be reminded of before.
|
||||||
Future<void> _pickReminders(BuildContext context, TaskContent content) async {
|
Future<void> _pickReminders(BuildContext context, TaskContent content) async {
|
||||||
|
|
|
||||||
|
|
@ -5614,6 +5614,53 @@ void main() {
|
||||||
.compareTo(tester.getCenter(row(b)).dy),
|
.compareTo(tester.getCenter(row(b)).dy),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets("in a task's sheet, a sub-task held and dropped on the "
|
||||||
|
'upper half of another goes before it, still its sub-task (the '
|
||||||
|
"developer's review, 10 Nov 2026)", (tester) async {
|
||||||
|
await start(tester, usedBefore: true);
|
||||||
|
final pack = (await tester.runAsync(
|
||||||
|
() => tasks().add(TaskContent(title: 'Pack', projectId: inbox())),
|
||||||
|
))!;
|
||||||
|
for (final (index, title) in ['Alpha', 'Bravo', 'Charlie'].indexed) {
|
||||||
|
await seed(
|
||||||
|
tester,
|
||||||
|
TaskContent(
|
||||||
|
title: title,
|
||||||
|
projectId: inbox(),
|
||||||
|
parentId: pack,
|
||||||
|
position: index.toDouble(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await goTab(tester, 'Inbox');
|
||||||
|
await tester.tap(find.text('Pack'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.drag(
|
||||||
|
find.byKey(taskSheetHandleKey),
|
||||||
|
const Offset(0, -700),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
Finder inSheet(String title) => find.descendant(
|
||||||
|
of: find.byType(DraggableScrollableSheet),
|
||||||
|
matching: row(title),
|
||||||
|
);
|
||||||
|
|
||||||
|
await dragFrom(
|
||||||
|
tester,
|
||||||
|
tester.getCenter(inSheet('Charlie')),
|
||||||
|
tester.getTopLeft(inSheet('Alpha')) + const Offset(100, 6),
|
||||||
|
);
|
||||||
|
|
||||||
|
final after = await stored(tester);
|
||||||
|
expect(after['Charlie']!.position, lessThan(after['Alpha']!.position));
|
||||||
|
expect(after['Charlie']!.parentId, pack);
|
||||||
|
final ys = [
|
||||||
|
for (final title in ['Charlie', 'Alpha', 'Bravo'])
|
||||||
|
tester.getCenter(inSheet(title)).dy,
|
||||||
|
];
|
||||||
|
expect(ys, orderedEquals([...ys]..sort()));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('a row held and dropped on the upper half of another goes '
|
testWidgets('a row held and dropped on the upper half of another goes '
|
||||||
'before it, which can be undone', (tester) async {
|
'before it, which can be undone', (tester) async {
|
||||||
await inboxOf(tester, ['Alpha', 'Bravo', 'Charlie']);
|
await inboxOf(tester, ['Alpha', 'Bravo', 'Charlie']);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue