feat(todo): tasks dragged into place or under another, the add button dropped where the task goes (ADR 0033, D16)

- A project's row, held, is dragged before or after another, or a
  little to the right to become its sub-task, or onto a section's
  heading; its sub-tasks go with it, and Order changed offers Undo
  (TaskRepository.place). The list scrolls at its edges.
- The add button is dragged onto a project's list to add the task
  between two rows, onto a day in Upcoming for that day, onto Today
  for today; dropped back on its place, an ×, it gives up.
- Holding a row now drags it; a swipe to the right selects it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-01 17:28:37 +01:00
commit 36f1e9a25c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -177,6 +177,62 @@ void main() {
},
);
test('a task placed elsewhere in its project takes its sub-tasks into its '
'section; under another task it becomes its sub-task; restoring what '
'it returns undoes it', () async {
final tasks = await device(alice);
final pack = await tasks.add(
const TaskContent(title: 'Pack', projectId: 'inbox', position: 1),
);
final socks = await tasks.add(
TaskContent(title: 'Socks', projectId: 'inbox', parentId: pack),
);
final trip = await tasks.add(
const TaskContent(title: 'Trip', projectId: 'inbox', position: 2),
);
final before = await tasks.place(pack, sectionId: 's', position: 0.5);
var all = {for (final t in await tasks.all()) t.id: t.content!};
expect(all[pack]!.sectionId, 's');
expect(all[pack]!.position, 0.5);
expect(all[pack]!.parentId, isNull);
expect(all[socks]!.sectionId, 's', reason: 'its sub-task along');
expect(all[socks]!.parentId, pack);
expect(all[trip]!.sectionId, isNull, reason: 'the others stay');
await tasks.place(trip, sectionId: 's', parentId: pack, position: 3);
all = {for (final t in await tasks.all()) t.id: t.content!};
expect(all[trip]!.parentId, pack);
expect(all[trip]!.sectionId, 's');
await tasks.restore(before);
all = {for (final t in await tasks.all()) t.id: t.content!};
expect(all[pack]!.sectionId, isNull);
expect(all[pack]!.position, 1);
expect(all[socks]!.sectionId, isNull);
});
test('a task is never placed under itself or its own sub-task', () async {
final tasks = await device(alice);
final pack = await tasks.add(
const TaskContent(title: 'Pack', projectId: 'inbox'),
);
final socks = await tasks.add(
TaskContent(title: 'Socks', projectId: 'inbox', parentId: pack),
);
expect(
() => tasks.place(pack, sectionId: null, parentId: socks, position: 0),
throwsArgumentError,
);
expect(
() => tasks.place(pack, sectionId: null, parentId: pack, position: 0),
throwsArgumentError,
);
final all = {for (final t in await tasks.all()) t.id: t.content!};
expect(all[pack]!.parentId, isNull, reason: 'nothing written');
});
test('moving tasks to a project takes their sub-tasks along, out of any '
'section, and a sub-task moved without its parent becomes a task of '
'its own', () async {