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
|
||||
|
||||
Status: Completed
|
||||
|
||||
Tasks:
|
||||
|
||||
Support these quick actions for flexible task cards:
|
||||
|
|
@ -29,6 +31,17 @@ Acceptance criteria:
|
|||
- Tests cover done and backlog state transitions.
|
||||
- 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
|
||||
|
||||
Recommended Codex level: high
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ export 'src/backlog.dart';
|
|||
export 'src/locked_time.dart';
|
||||
export 'src/quick_capture.dart';
|
||||
export 'src/scheduling_engine.dart';
|
||||
export 'src/task_actions.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);
|
||||
});
|
||||
|
||||
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', () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue