forked from eva/focus-flow
feat(tasks): add child task entry defaults
This commit is contained in:
parent
92c05d8c1e
commit
eba197d5ae
4 changed files with 199 additions and 0 deletions
|
|
@ -47,6 +47,8 @@ Execution notes:
|
||||||
|
|
||||||
Recommended Codex level: medium
|
Recommended Codex level: medium
|
||||||
|
|
||||||
|
Status: Completed
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
Support row-style child entry data:
|
Support row-style child entry data:
|
||||||
|
|
@ -72,6 +74,16 @@ Acceptance criteria:
|
||||||
- Tests cover inherited project and overridden project.
|
- Tests cover inherited project and overridden project.
|
||||||
- Tests cover reward `not set` remaining distinct from very low reward.
|
- Tests cover reward `not set` remaining distinct from very low reward.
|
||||||
|
|
||||||
|
Execution notes:
|
||||||
|
|
||||||
|
- Added `ChildTaskEntry` for row-style child creation data.
|
||||||
|
- Child entries support title, nullable priority, reward, difficulty, duration, and optional project override.
|
||||||
|
- Child entries inherit the parent project when no override is supplied.
|
||||||
|
- Missing priority remains null so children without explicit priority can preserve row insertion order.
|
||||||
|
- Missing reward remains `RewardLevel.notSet`, distinct from `RewardLevel.veryLow`.
|
||||||
|
- Added tests for explicit child fields, project inheritance/override, reward not-set behavior, and no-priority row order.
|
||||||
|
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
|
||||||
|
|
||||||
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite.
|
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite.
|
||||||
|
|
||||||
## Chunk 7.3 — Child-task edge-case regression test suite
|
## Chunk 7.3 — Child-task edge-case regression test suite
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ Execution notes:
|
||||||
|
|
||||||
Recommended Codex level: medium
|
Recommended Codex level: medium
|
||||||
|
|
||||||
|
Status: Completed
|
||||||
|
|
||||||
Tasks:
|
Tasks:
|
||||||
|
|
||||||
Support row-style child entry data:
|
Support row-style child entry data:
|
||||||
|
|
@ -72,6 +74,16 @@ Acceptance criteria:
|
||||||
- Tests cover inherited project and overridden project.
|
- Tests cover inherited project and overridden project.
|
||||||
- Tests cover reward `not set` remaining distinct from very low reward.
|
- Tests cover reward `not set` remaining distinct from very low reward.
|
||||||
|
|
||||||
|
Execution notes:
|
||||||
|
|
||||||
|
- Added `ChildTaskEntry` for row-style child creation data.
|
||||||
|
- Child entries support title, nullable priority, reward, difficulty, duration, and optional project override.
|
||||||
|
- Child entries inherit the parent project when no override is supplied.
|
||||||
|
- Missing priority remains null so children without explicit priority can preserve row insertion order.
|
||||||
|
- Missing reward remains `RewardLevel.notSet`, distinct from `RewardLevel.veryLow`.
|
||||||
|
- Added tests for explicit child fields, project inheritance/override, reward not-set behavior, and no-priority row order.
|
||||||
|
- `dart format lib test`, `dart analyze`, `dart test`, and `git diff --check` passed.
|
||||||
|
|
||||||
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite.
|
BREAKPOINT: Stop here. Confirm `extra high` mode before implementing the child-task edge-case regression suite.
|
||||||
|
|
||||||
## Chunk 7.3 — Child-task edge-case regression test suite
|
## Chunk 7.3 — Child-task edge-case regression test suite
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,77 @@
|
||||||
|
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
|
||||||
|
/// Row-style input for creating one child task.
|
||||||
|
///
|
||||||
|
/// Priority is nullable on purpose. A null priority means the user did not set
|
||||||
|
/// one, so callers should preserve row insertion order instead of treating the
|
||||||
|
/// child as medium priority.
|
||||||
|
class ChildTaskEntry {
|
||||||
|
const ChildTaskEntry({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.createdAt,
|
||||||
|
this.priority,
|
||||||
|
this.reward = RewardLevel.notSet,
|
||||||
|
this.difficulty = DifficultyLevel.notSet,
|
||||||
|
this.durationMinutes,
|
||||||
|
this.projectId,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Caller-generated child id.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// User-entered child title.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Creation timestamp supplied by the caller.
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
/// Optional explicit child priority.
|
||||||
|
final PriorityLevel? priority;
|
||||||
|
|
||||||
|
/// Optional explicit child reward. Missing reward stays `notSet`.
|
||||||
|
final RewardLevel reward;
|
||||||
|
|
||||||
|
/// Optional explicit child difficulty.
|
||||||
|
final DifficultyLevel difficulty;
|
||||||
|
|
||||||
|
/// Optional child duration estimate.
|
||||||
|
final int? durationMinutes;
|
||||||
|
|
||||||
|
/// Optional project override. Null means inherit the parent project.
|
||||||
|
final String? projectId;
|
||||||
|
|
||||||
|
/// Optional update timestamp.
|
||||||
|
final DateTime? updatedAt;
|
||||||
|
|
||||||
|
/// Convert this entry into a child [Task] owned by [parent].
|
||||||
|
Task toTask({
|
||||||
|
required Task parent,
|
||||||
|
}) {
|
||||||
|
final trimmedTitle = title.trim();
|
||||||
|
if (trimmedTitle.isEmpty) {
|
||||||
|
throw ArgumentError.value(title, 'title', 'Title is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task(
|
||||||
|
id: id,
|
||||||
|
title: trimmedTitle,
|
||||||
|
projectId: projectId ?? parent.projectId,
|
||||||
|
type: TaskType.flexible,
|
||||||
|
status: TaskStatus.backlog,
|
||||||
|
priority: priority,
|
||||||
|
reward: reward,
|
||||||
|
difficulty: difficulty,
|
||||||
|
durationMinutes: durationMinutes,
|
||||||
|
parentTaskId: parent.id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt ?? createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Read-only parent/child projection over a task list.
|
/// Read-only parent/child projection over a task list.
|
||||||
///
|
///
|
||||||
/// This is intentionally not a scheduler. It answers ownership questions such
|
/// This is intentionally not a scheduler. It answers ownership questions such
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,110 @@ void main() {
|
||||||
expect(unchangedParent.scheduledStart, isNull);
|
expect(unchangedParent.scheduledStart, isNull);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Child task entry defaults', () {
|
||||||
|
final now = DateTime(2026, 6, 19, 12);
|
||||||
|
|
||||||
|
test('child creation supports explicit row fields', () {
|
||||||
|
final parent = task(
|
||||||
|
id: 'parent',
|
||||||
|
title: 'Clean kitchen',
|
||||||
|
createdAt: now,
|
||||||
|
).copyWith(projectId: 'home');
|
||||||
|
final child = ChildTaskEntry(
|
||||||
|
id: 'child',
|
||||||
|
title: ' Clear sink ',
|
||||||
|
createdAt: now,
|
||||||
|
priority: PriorityLevel.high,
|
||||||
|
reward: RewardLevel.high,
|
||||||
|
difficulty: DifficultyLevel.hard,
|
||||||
|
durationMinutes: 20,
|
||||||
|
).toTask(parent: parent);
|
||||||
|
|
||||||
|
expect(child.id, 'child');
|
||||||
|
expect(child.title, 'Clear sink');
|
||||||
|
expect(child.parentTaskId, parent.id);
|
||||||
|
expect(child.projectId, parent.projectId);
|
||||||
|
expect(child.priority, PriorityLevel.high);
|
||||||
|
expect(child.reward, RewardLevel.high);
|
||||||
|
expect(child.difficulty, DifficultyLevel.hard);
|
||||||
|
expect(child.durationMinutes, 20);
|
||||||
|
expect(child.status, TaskStatus.backlog);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('child inherits parent project unless project override is supplied',
|
||||||
|
() {
|
||||||
|
final parent = task(
|
||||||
|
id: 'parent',
|
||||||
|
title: 'Plan trip',
|
||||||
|
createdAt: now,
|
||||||
|
).copyWith(projectId: 'travel');
|
||||||
|
|
||||||
|
final inherited = ChildTaskEntry(
|
||||||
|
id: 'inherited',
|
||||||
|
title: 'Book train',
|
||||||
|
createdAt: now,
|
||||||
|
).toTask(parent: parent);
|
||||||
|
final overridden = ChildTaskEntry(
|
||||||
|
id: 'overridden',
|
||||||
|
title: 'Ask partner',
|
||||||
|
createdAt: now,
|
||||||
|
projectId: 'relationships',
|
||||||
|
).toTask(parent: parent);
|
||||||
|
|
||||||
|
expect(inherited.projectId, 'travel');
|
||||||
|
expect(overridden.projectId, 'relationships');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('missing reward remains not set and distinct from very low reward',
|
||||||
|
() {
|
||||||
|
final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);
|
||||||
|
final noReward = ChildTaskEntry(
|
||||||
|
id: 'no-reward',
|
||||||
|
title: 'Clear sink',
|
||||||
|
createdAt: now,
|
||||||
|
).toTask(parent: parent);
|
||||||
|
final veryLowReward = ChildTaskEntry(
|
||||||
|
id: 'very-low',
|
||||||
|
title: 'Wipe counter',
|
||||||
|
createdAt: now,
|
||||||
|
reward: RewardLevel.veryLow,
|
||||||
|
).toTask(parent: parent);
|
||||||
|
|
||||||
|
expect(noReward.reward, RewardLevel.notSet);
|
||||||
|
expect(veryLowReward.reward, RewardLevel.veryLow);
|
||||||
|
expect(noReward.reward, isNot(veryLowReward.reward));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('children without priority preserve row insertion order', () {
|
||||||
|
final parent = task(id: 'parent', title: 'Clean kitchen', createdAt: now);
|
||||||
|
final first = ChildTaskEntry(
|
||||||
|
id: 'first',
|
||||||
|
title: 'Clear sink',
|
||||||
|
createdAt: now,
|
||||||
|
).toTask(parent: parent);
|
||||||
|
final second = ChildTaskEntry(
|
||||||
|
id: 'second',
|
||||||
|
title: 'Wipe counter',
|
||||||
|
createdAt: now,
|
||||||
|
).toTask(parent: parent);
|
||||||
|
final third = ChildTaskEntry(
|
||||||
|
id: 'third',
|
||||||
|
title: 'Sweep floor',
|
||||||
|
createdAt: now,
|
||||||
|
).toTask(parent: parent);
|
||||||
|
final view = ChildTaskView(tasks: [parent, first, second, third]);
|
||||||
|
|
||||||
|
expect(first.priority, isNull);
|
||||||
|
expect(second.priority, isNull);
|
||||||
|
expect(third.priority, isNull);
|
||||||
|
expect(view.childrenOf(parent).map((child) => child.id), [
|
||||||
|
'first',
|
||||||
|
'second',
|
||||||
|
'third',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Task task({
|
Task task({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue