Skip to content

Commit e902cbc

Browse files
authored
Merge pull request #60 from jamiefdhurst/sub-tasks
feat: support sub-tasks at multiple levels
2 parents 5c008ef + 8144bc2 commit e902cbc

File tree

8 files changed

+600
-37
lines changed

8 files changed

+600
-37
lines changed

src/__tests__/tasks/data-view-collection.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,126 @@ describe('DataView task collection', () => {
244244
'## Header 1\n\n- [ ] Task 1\n- [ ] Task 2\n\n## Header 2\n\n- [ ] Task 3\n- [ ] Task 4 [due:: 2025-01-01]\n- [ ] [>] Task 5\n\n'
245245
);
246246
});
247+
248+
describe('sub-tasks', () => {
249+
it('parses sub-tasks with tab indentation', () => {
250+
sut = new DataViewTaskCollection(
251+
'## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task 1\n\t- [ ] Child task 2\n'
252+
);
253+
254+
const tasks = sut.getAllTasks();
255+
expect(tasks.length).toEqual(1);
256+
expect(tasks[0].getName()).toEqual('Parent task');
257+
expect(tasks[0].hasChildren()).toBe(true);
258+
expect(tasks[0].getChildren().length).toEqual(2);
259+
expect(tasks[0].getChildren()[0].getName()).toEqual('Child task 1');
260+
expect(tasks[0].getChildren()[1].getName()).toEqual('Child task 2');
261+
});
262+
263+
it('parses sub-tasks with space indentation', () => {
264+
sut = new DataViewTaskCollection(
265+
'## Header 1\n\n- [ ] Parent task\n - [ ] Child task 1\n - [ ] Child task 2\n'
266+
);
267+
268+
const tasks = sut.getAllTasks();
269+
expect(tasks.length).toEqual(1);
270+
expect(tasks[0].hasChildren()).toBe(true);
271+
expect(tasks[0].getChildren().length).toEqual(2);
272+
});
273+
274+
it('parses deeply nested sub-tasks', () => {
275+
sut = new DataViewTaskCollection(
276+
'## Header 1\n\n- [ ] Level 0\n\t- [ ] Level 1\n\t\t- [ ] Level 2\n\t\t\t- [ ] Level 3\n'
277+
);
278+
279+
const tasks = sut.getAllTasks();
280+
expect(tasks.length).toEqual(1);
281+
expect(tasks[0].getName()).toEqual('Level 0');
282+
expect(tasks[0].getChildren()[0].getName()).toEqual('Level 1');
283+
expect(tasks[0].getChildren()[0].getChildren()[0].getName()).toEqual('Level 2');
284+
expect(tasks[0].getChildren()[0].getChildren()[0].getChildren()[0].getName()).toEqual(
285+
'Level 3'
286+
);
287+
});
288+
289+
it('parses sub-tasks with metadata', () => {
290+
sut = new DataViewTaskCollection(
291+
'## Header 1\n\n- [ ] Parent task [due:: 2024-01-01]\n\t- [ ] Child task [due:: 2024-01-02]\n'
292+
);
293+
294+
const tasks = sut.getAllTasks();
295+
expect(tasks[0].getDueDate()).toEqual('2024-01-01');
296+
expect(tasks[0].getChildren()[0].getDueDate()).toEqual('2024-01-02');
297+
});
298+
299+
it('filters incomplete children correctly', () => {
300+
sut = new DataViewTaskCollection(
301+
'## Header 1\n\n- [ ] Parent task\n\t- [x] Complete child\n\t- [ ] Incomplete child\n'
302+
);
303+
304+
const tasks = sut.getAllTasks();
305+
const parent = tasks[0];
306+
expect(parent.getChildren().length).toEqual(2);
307+
308+
parent.filterIncompleteChildren();
309+
310+
expect(parent.getChildren().length).toEqual(1);
311+
expect(parent.getChildren()[0].getName()).toEqual('Incomplete child');
312+
});
313+
314+
it('outputs sub-tasks with correct indentation', () => {
315+
sut = new DataViewTaskCollection(
316+
'## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task 1\n\t- [x] Child task 2\n'
317+
);
318+
319+
const result = sut.toString();
320+
321+
expect(result).toContain('- [ ] Parent task');
322+
expect(result).toContain('\t- [ ] Child task 1');
323+
expect(result).toContain('\t- [x] Child task 2');
324+
});
325+
326+
it('preserves space indentation pattern when outputting', () => {
327+
// Use 2-space indentation (2 spaces = level 1, 4 spaces = level 2)
328+
sut = new DataViewTaskCollection(
329+
'## Header 1\n\n- [ ] Parent\n - [ ] Child level 1\n - [ ] Child level 2\n'
330+
);
331+
332+
const tasks = sut.getAllTasks();
333+
expect(tasks.length).toEqual(1);
334+
expect(tasks[0].hasChildren()).toBe(true);
335+
336+
const result = tasks[0].toString();
337+
// Should preserve the 2-space pattern
338+
expect(result).toContain('- [ ] Parent');
339+
expect(result).toContain(' - [ ] Child level 1');
340+
expect(result).toContain(' - [ ] Child level 2');
341+
});
342+
343+
it('handles orphaned sub-tasks when parent indent is skipped', () => {
344+
// Task at level 2 without a level 1 parent - should be added as top-level
345+
sut = new DataViewTaskCollection(
346+
'## Header 1\n\n- [ ] Parent\n\t\t- [ ] Orphaned child at level 2\n'
347+
);
348+
349+
const tasks = sut.getAllTasks();
350+
// The orphaned task should be added as a top-level task since there's no level 1 parent
351+
expect(tasks.length).toEqual(2);
352+
expect(tasks[0].getName()).toEqual('Parent');
353+
expect(tasks[1].getName()).toEqual('Orphaned child at level 2');
354+
});
355+
356+
it('handles task with due date in toString when dueDate is set', () => {
357+
sut = new DataViewTaskCollection('## Header 1\n\n- [ ] Task with due [due:: 2024-01-15]\n');
358+
359+
const tasks = sut.getAllTasks();
360+
const task = tasks[0];
361+
362+
// Call isDue to set the dueDate property
363+
task.isDue();
364+
365+
const result = task.toString();
366+
expect(result).toContain('[due:: 2024-01-15]');
367+
});
368+
});
247369
});

src/__tests__/tasks/emoji-collection.test.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,183 @@ describe('Emoji task collection', () => {
241241
'## Header 1\n\n- [ ] Task 1\n- [ ] Task 2\n\n## Header 2\n\n- [ ] Task 3\n- [ ] Task 4\n- [ ] [>] Task 5\n\n'
242242
);
243243
});
244+
245+
describe('sub-tasks', () => {
246+
it('parses sub-tasks with tab indentation', () => {
247+
sut = new EmojiTaskCollection(
248+
'## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task 1\n\t- [ ] Child task 2\n'
249+
);
250+
251+
const tasks = sut.getAllTasks();
252+
expect(tasks.length).toEqual(1);
253+
expect(tasks[0].getName()).toEqual('Parent task');
254+
expect(tasks[0].hasChildren()).toBe(true);
255+
expect(tasks[0].getChildren().length).toEqual(2);
256+
expect(tasks[0].getChildren()[0].getName()).toEqual('Child task 1');
257+
expect(tasks[0].getChildren()[1].getName()).toEqual('Child task 2');
258+
});
259+
260+
it('parses sub-tasks with space indentation', () => {
261+
sut = new EmojiTaskCollection(
262+
'## Header 1\n\n- [ ] Parent task\n - [ ] Child task 1\n - [ ] Child task 2\n'
263+
);
264+
265+
const tasks = sut.getAllTasks();
266+
expect(tasks.length).toEqual(1);
267+
expect(tasks[0].hasChildren()).toBe(true);
268+
expect(tasks[0].getChildren().length).toEqual(2);
269+
});
270+
271+
it('parses deeply nested sub-tasks', () => {
272+
sut = new EmojiTaskCollection(
273+
'## Header 1\n\n- [ ] Level 0\n\t- [ ] Level 1\n\t\t- [ ] Level 2\n\t\t\t- [ ] Level 3\n'
274+
);
275+
276+
const tasks = sut.getAllTasks();
277+
expect(tasks.length).toEqual(1);
278+
expect(tasks[0].getName()).toEqual('Level 0');
279+
expect(tasks[0].getChildren()[0].getName()).toEqual('Level 1');
280+
expect(tasks[0].getChildren()[0].getChildren()[0].getName()).toEqual('Level 2');
281+
expect(tasks[0].getChildren()[0].getChildren()[0].getChildren()[0].getName()).toEqual(
282+
'Level 3'
283+
);
284+
});
285+
286+
it('parses multiple parent tasks with their own sub-tasks', () => {
287+
sut = new EmojiTaskCollection(
288+
'## Header 1\n\n- [ ] Parent 1\n\t- [ ] Child 1a\n- [ ] Parent 2\n\t- [ ] Child 2a\n\t- [ ] Child 2b\n'
289+
);
290+
291+
const tasks = sut.getAllTasks();
292+
expect(tasks.length).toEqual(2);
293+
expect(tasks[0].getName()).toEqual('Parent 1');
294+
expect(tasks[0].getChildren().length).toEqual(1);
295+
expect(tasks[1].getName()).toEqual('Parent 2');
296+
expect(tasks[1].getChildren().length).toEqual(2);
297+
});
298+
299+
it('outputs sub-tasks with correct indentation', () => {
300+
sut = new EmojiTaskCollection(
301+
'## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task 1\n\t- [x] Child task 2\n'
302+
);
303+
304+
const result = sut.toString();
305+
306+
expect(result).toContain('- [ ] Parent task');
307+
expect(result).toContain('\t- [ ] Child task 1');
308+
expect(result).toContain('\t- [x] Child task 2');
309+
});
310+
311+
it('parses sub-tasks with metadata', () => {
312+
sut = new EmojiTaskCollection(
313+
'## Header 1\n\n- [ ] Parent task 📅 2024-01-01\n\t- [ ] Child task 📅 2024-01-02\n'
314+
);
315+
316+
const tasks = sut.getAllTasks();
317+
expect(tasks[0].getDueDate()).toEqual('2024-01-01');
318+
expect(tasks[0].getChildren()[0].getDueDate()).toEqual('2024-01-02');
319+
});
320+
321+
it('filters incomplete children correctly', () => {
322+
sut = new EmojiTaskCollection(
323+
'## Header 1\n\n- [ ] Parent task\n\t- [x] Complete child\n\t- [ ] Incomplete child\n'
324+
);
325+
326+
const tasks = sut.getAllTasks();
327+
const parent = tasks[0];
328+
expect(parent.getChildren().length).toEqual(2);
329+
330+
parent.filterIncompleteChildren();
331+
332+
expect(parent.getChildren().length).toEqual(1);
333+
expect(parent.getChildren()[0].getName()).toEqual('Incomplete child');
334+
});
335+
336+
it('filters incomplete children recursively', () => {
337+
sut = new EmojiTaskCollection(
338+
'## Header 1\n\n- [ ] Parent\n\t- [ ] Child\n\t\t- [x] Complete grandchild\n\t\t- [ ] Incomplete grandchild\n'
339+
);
340+
341+
const tasks = sut.getAllTasks();
342+
const parent = tasks[0];
343+
344+
parent.filterIncompleteChildren();
345+
346+
expect(parent.getChildren()[0].getChildren().length).toEqual(1);
347+
expect(parent.getChildren()[0].getChildren()[0].getName()).toEqual('Incomplete grandchild');
348+
});
349+
350+
it('marks children as carried over when parent is marked', () => {
351+
settings.carryOverPrefix = '[>]';
352+
sut = new EmojiTaskCollection('## Header 1\n\n- [ ] Parent task\n\t- [ ] Child task\n');
353+
354+
const tasks = sut.getAllTasks();
355+
const parent = tasks[0];
356+
parent.markCarriedOver();
357+
358+
const result = parent.toString();
359+
expect(result).toContain('- [ ] [>] Parent task');
360+
expect(result).toContain('\t- [ ] [>] Child task');
361+
});
362+
363+
it('resets indent levels when setIndentLevel is called', () => {
364+
sut = new EmojiTaskCollection(
365+
'## Header 1\n\n- [ ] Parent\n\t- [ ] Child\n\t\t- [ ] Grandchild\n'
366+
);
367+
368+
const tasks = sut.getAllTasks();
369+
const parent = tasks[0];
370+
371+
// Reset to level 0 (simulating carry-over)
372+
parent.setIndentLevel(0);
373+
374+
const result = parent.toString();
375+
expect(result).toContain('- [ ] Parent');
376+
expect(result).toContain('\t- [ ] Child');
377+
expect(result).toContain('\t\t- [ ] Grandchild');
378+
});
379+
380+
it('preserves space indentation pattern when outputting', () => {
381+
// Use 2-space indentation (2 spaces = level 1, 4 spaces = level 2)
382+
sut = new EmojiTaskCollection(
383+
'## Header 1\n\n- [ ] Parent\n - [ ] Child level 1\n - [ ] Child level 2\n'
384+
);
385+
386+
const tasks = sut.getAllTasks();
387+
expect(tasks.length).toEqual(1);
388+
expect(tasks[0].hasChildren()).toBe(true);
389+
390+
const result = tasks[0].toString();
391+
// Should preserve the 2-space pattern
392+
expect(result).toContain('- [ ] Parent');
393+
expect(result).toContain(' - [ ] Child level 1');
394+
expect(result).toContain(' - [ ] Child level 2');
395+
});
396+
397+
it('handles orphaned sub-tasks when parent indent is skipped', () => {
398+
// Task at level 2 without a level 1 parent - should be added as top-level
399+
sut = new EmojiTaskCollection(
400+
'## Header 1\n\n- [ ] Parent\n\t\t- [ ] Orphaned child at level 2\n'
401+
);
402+
403+
const tasks = sut.getAllTasks();
404+
// The orphaned task should be added as a top-level task since there's no level 1 parent
405+
expect(tasks.length).toEqual(2);
406+
expect(tasks[0].getName()).toEqual('Parent');
407+
expect(tasks[1].getName()).toEqual('Orphaned child at level 2');
408+
});
409+
410+
it('handles task with due date in toString when dueDate is set', () => {
411+
sut = new EmojiTaskCollection('## Header 1\n\n- [ ] Task with due 📅 2024-01-15\n');
412+
413+
const tasks = sut.getAllTasks();
414+
const task = tasks[0];
415+
416+
// Call isDue to set the dueDate property
417+
task.isDue();
418+
419+
const result = task.toString();
420+
expect(result).toContain('📅 2024-01-15');
421+
});
422+
});
244423
});

0 commit comments

Comments
 (0)