feat(tasks): add flexible quick actions
This commit is contained in:
parent
4a307cc522
commit
f0f8553042
4 changed files with 194 additions and 0 deletions
|
|
@ -8,6 +8,8 @@ Purpose: Define and implement the safe, low-friction actions available from task
|
||||||
|
|
||||||
Recommended Codex level: medium
|
Recommended Codex level: medium
|
||||||
|
|
||||||
|
Status: Completed
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
Support these quick actions for flexible task cards:
|
Support these quick actions for flexible task cards:
|
||||||
|
|
@ -29,6 +31,17 @@ Acceptance criteria:
|
||||||
- Tests cover done and backlog state transitions.
|
- Tests cover done and backlog state transitions.
|
||||||
- Push destination selection is represented in domain layer.
|
- Push destination selection is represented in domain layer.
|
||||||
|
|
||||||
|
Execution notes:
|
||||||
|
|
||||||
|
- Added `FlexibleTaskActionService` for flexible card quick actions.
|
||||||
|
- Added `FlexibleTaskQuickAction` and `PushDestination` domain enums.
|
||||||
|
- Done marks a flexible task completed without changing its schedule placement.
|
||||||
|
- Backlog uses the scheduling engine backlog transition and clears schedule placement.
|
||||||
|
- Push returns explicit domain destinations: next available slot, tomorrow/top of queue, and backlog.
|
||||||
|
- Break up returns an explicit child-task-flow intent without creating children yet.
|
||||||
|
- Added tests for done, backlog, push destination representation, and break-up flow intent.
|
||||||
|
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
|
||||||
|
|
||||||
## Chunk 6.2 — Push destination behavior
|
## Chunk 6.2 — Push destination behavior
|
||||||
|
|
||||||
Recommended Codex level: high
|
Recommended Codex level: high
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ export 'src/backlog.dart';
|
||||||
export 'src/locked_time.dart';
|
export 'src/locked_time.dart';
|
||||||
export 'src/quick_capture.dart';
|
export 'src/quick_capture.dart';
|
||||||
export 'src/scheduling_engine.dart';
|
export 'src/scheduling_engine.dart';
|
||||||
|
export 'src/task_actions.dart';
|
||||||
export 'src/task_statistics.dart';
|
export 'src/task_statistics.dart';
|
||||||
|
|
|
||||||
86
lib/src/task_actions.dart
Normal file
86
lib/src/task_actions.dart
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import 'models.dart';
|
||||||
|
import 'scheduling_engine.dart';
|
||||||
|
|
||||||
|
/// Quick actions available from a flexible task card.
|
||||||
|
enum FlexibleTaskQuickAction {
|
||||||
|
done,
|
||||||
|
push,
|
||||||
|
backlog,
|
||||||
|
breakUp,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Explicit push destinations shown after choosing the push quick action.
|
||||||
|
enum PushDestination {
|
||||||
|
nextAvailableSlot,
|
||||||
|
tomorrowTopOfQueue,
|
||||||
|
backlog,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Domain result for a flexible task quick action.
|
||||||
|
class FlexibleTaskActionResult {
|
||||||
|
const FlexibleTaskActionResult({
|
||||||
|
required this.action,
|
||||||
|
required this.task,
|
||||||
|
this.pushDestinations = const <PushDestination>[],
|
||||||
|
this.startsChildTaskFlow = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FlexibleTaskQuickAction action;
|
||||||
|
final Task task;
|
||||||
|
final List<PushDestination> pushDestinations;
|
||||||
|
final bool startsChildTaskFlow;
|
||||||
|
|
||||||
|
bool get changedTask => !startsChildTaskFlow && pushDestinations.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Applies low-friction quick actions for flexible task cards.
|
||||||
|
class FlexibleTaskActionService {
|
||||||
|
const FlexibleTaskActionService({
|
||||||
|
this.schedulingEngine = const SchedulingEngine(),
|
||||||
|
});
|
||||||
|
|
||||||
|
final SchedulingEngine schedulingEngine;
|
||||||
|
|
||||||
|
FlexibleTaskActionResult apply({
|
||||||
|
required Task task,
|
||||||
|
required FlexibleTaskQuickAction action,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
}) {
|
||||||
|
if (!task.isFlexible) {
|
||||||
|
throw ArgumentError.value(
|
||||||
|
task.type, 'task.type', 'Task must be flexible.');
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case FlexibleTaskQuickAction.done:
|
||||||
|
return FlexibleTaskActionResult(
|
||||||
|
action: action,
|
||||||
|
task: task.copyWith(
|
||||||
|
status: TaskStatus.completed,
|
||||||
|
updatedAt: updatedAt ?? DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case FlexibleTaskQuickAction.push:
|
||||||
|
return FlexibleTaskActionResult(
|
||||||
|
action: action,
|
||||||
|
task: task,
|
||||||
|
pushDestinations: const [
|
||||||
|
PushDestination.nextAvailableSlot,
|
||||||
|
PushDestination.tomorrowTopOfQueue,
|
||||||
|
PushDestination.backlog,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
case FlexibleTaskQuickAction.backlog:
|
||||||
|
return FlexibleTaskActionResult(
|
||||||
|
action: action,
|
||||||
|
task: schedulingEngine.moveToBacklog(task, updatedAt: updatedAt),
|
||||||
|
);
|
||||||
|
case FlexibleTaskQuickAction.breakUp:
|
||||||
|
return FlexibleTaskActionResult(
|
||||||
|
action: action,
|
||||||
|
task: task,
|
||||||
|
startsChildTaskFlow: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -792,6 +792,100 @@ void main() {
|
||||||
|
|
||||||
expect(minutes, 60);
|
expect(minutes, 60);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('flexible quick action done marks task completed', () {
|
||||||
|
const service = FlexibleTaskActionService();
|
||||||
|
final task = Task.quickCapture(
|
||||||
|
id: 'task-1',
|
||||||
|
title: 'Handle paperwork',
|
||||||
|
createdAt: now,
|
||||||
|
).copyWith(
|
||||||
|
status: TaskStatus.planned,
|
||||||
|
scheduledStart: DateTime(2026, 6, 19, 13),
|
||||||
|
scheduledEnd: DateTime(2026, 6, 19, 13, 30),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = service.apply(
|
||||||
|
task: task,
|
||||||
|
action: FlexibleTaskQuickAction.done,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.action, FlexibleTaskQuickAction.done);
|
||||||
|
expect(result.task.status, TaskStatus.completed);
|
||||||
|
expect(result.task.scheduledStart, DateTime(2026, 6, 19, 13));
|
||||||
|
expect(result.task.updatedAt, now);
|
||||||
|
expect(result.changedTask, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flexible quick action backlog clears schedule placement', () {
|
||||||
|
const service = FlexibleTaskActionService();
|
||||||
|
final task = Task.quickCapture(
|
||||||
|
id: 'task-1',
|
||||||
|
title: 'Handle paperwork',
|
||||||
|
createdAt: now,
|
||||||
|
).copyWith(
|
||||||
|
status: TaskStatus.planned,
|
||||||
|
scheduledStart: DateTime(2026, 6, 19, 13),
|
||||||
|
scheduledEnd: DateTime(2026, 6, 19, 13, 30),
|
||||||
|
);
|
||||||
|
|
||||||
|
final result = service.apply(
|
||||||
|
task: task,
|
||||||
|
action: FlexibleTaskQuickAction.backlog,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.action, FlexibleTaskQuickAction.backlog);
|
||||||
|
expect(result.task.status, TaskStatus.backlog);
|
||||||
|
expect(result.task.scheduledStart, isNull);
|
||||||
|
expect(result.task.scheduledEnd, isNull);
|
||||||
|
expect(result.task.stats.movedToBacklogCount, 1);
|
||||||
|
expect(result.changedTask, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flexible quick action push exposes domain destinations', () {
|
||||||
|
const service = FlexibleTaskActionService();
|
||||||
|
final task = Task.quickCapture(
|
||||||
|
id: 'task-1',
|
||||||
|
title: 'Handle paperwork',
|
||||||
|
createdAt: now,
|
||||||
|
).copyWith(status: TaskStatus.planned);
|
||||||
|
|
||||||
|
final result = service.apply(
|
||||||
|
task: task,
|
||||||
|
action: FlexibleTaskQuickAction.push,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.task, same(task));
|
||||||
|
expect(result.pushDestinations, const [
|
||||||
|
PushDestination.nextAvailableSlot,
|
||||||
|
PushDestination.tomorrowTopOfQueue,
|
||||||
|
PushDestination.backlog,
|
||||||
|
]);
|
||||||
|
expect(result.pushDestinations, isNot(contains('laterToday')));
|
||||||
|
expect(result.changedTask, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('flexible quick action break up starts child task flow', () {
|
||||||
|
const service = FlexibleTaskActionService();
|
||||||
|
final task = Task.quickCapture(
|
||||||
|
id: 'task-1',
|
||||||
|
title: 'Handle paperwork',
|
||||||
|
createdAt: now,
|
||||||
|
).copyWith(status: TaskStatus.planned);
|
||||||
|
|
||||||
|
final result = service.apply(
|
||||||
|
task: task,
|
||||||
|
action: FlexibleTaskQuickAction.breakUp,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.task, same(task));
|
||||||
|
expect(result.startsChildTaskFlow, isTrue);
|
||||||
|
expect(result.changedTask, isFalse);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('SchedulingEngine starter behavior', () {
|
group('SchedulingEngine starter behavior', () {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue