feat: add parent / child feature
This commit is contained in:
@@ -77,7 +77,9 @@ describe('adapt steps to textarea value', () => {
|
|||||||
|
|
||||||
const [step] = adaptTextareaToSteps(stepInTextarea)
|
const [step] = adaptTextareaToSteps(stepInTextarea)
|
||||||
|
|
||||||
expect(step.id).toMatchInlineSnapshot(`"66f312736335fce1df9a8b95c7be3fce-1"`)
|
expect(step.id).toMatchInlineSnapshot(
|
||||||
|
`"66f312736335fce1df9a8b95c7be3fce-1"`
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates generated ids based on title and estimation and indexes when duplicated', () => {
|
it('creates generated ids based on title and estimation and indexes when duplicated', () => {
|
||||||
@@ -129,4 +131,131 @@ describe('adapt steps to textarea value', () => {
|
|||||||
|
|
||||||
expect(adaptTextareaToSteps(stepInTextarea)).toEqual([expectedStep])
|
expect(adaptTextareaToSteps(stepInTextarea)).toEqual([expectedStep])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('subtask support', () => {
|
||||||
|
it('flattens indented subtasks with parent prefix', () => {
|
||||||
|
const stepInTextarea = `- Parent task | 5
|
||||||
|
- Child task 1 | 3
|
||||||
|
- Child task 2 | 2`
|
||||||
|
|
||||||
|
const expectedSteps = [
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: '(Parent task) - Child task 1',
|
||||||
|
estimation: 3
|
||||||
|
}),
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: '(Parent task) - Child task 2',
|
||||||
|
estimation: 2
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(adaptTextareaToSteps(stepInTextarea)).toEqual(expectedSteps)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores parent estimation when it has children', () => {
|
||||||
|
const stepInTextarea = `- Parent | 10
|
||||||
|
- Child | 3`
|
||||||
|
|
||||||
|
const steps = adaptTextareaToSteps(stepInTextarea)
|
||||||
|
|
||||||
|
expect(steps).toHaveLength(1)
|
||||||
|
expect(steps[0].title).toBe('(Parent) - Child')
|
||||||
|
expect(steps[0].estimation).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats standalone items (no children) as regular steps', () => {
|
||||||
|
const stepInTextarea = `- Standalone task | 4`
|
||||||
|
|
||||||
|
const expectedSteps = [
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: 'Standalone task',
|
||||||
|
estimation: 4
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(adaptTextareaToSteps(stepInTextarea)).toEqual(expectedSteps)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles mixed subtasks and standalone items', () => {
|
||||||
|
const stepInTextarea = `- Parent task | 5
|
||||||
|
- Child task 1 | 3
|
||||||
|
- Child task 2 | 2
|
||||||
|
- Standalone | 4`
|
||||||
|
|
||||||
|
const expectedSteps = [
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: '(Parent task) - Child task 1',
|
||||||
|
estimation: 3
|
||||||
|
}),
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: '(Parent task) - Child task 2',
|
||||||
|
estimation: 2
|
||||||
|
}),
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: 'Standalone',
|
||||||
|
estimation: 4
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(adaptTextareaToSteps(stepInTextarea)).toEqual(expectedSteps)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles tab indentation', () => {
|
||||||
|
const stepInTextarea = `- Parent | 5
|
||||||
|
\t- Child | 3`
|
||||||
|
|
||||||
|
const steps = adaptTextareaToSteps(stepInTextarea)
|
||||||
|
|
||||||
|
expect(steps).toHaveLength(1)
|
||||||
|
expect(steps[0].title).toBe('(Parent) - Child')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('treats orphan indented line as regular step', () => {
|
||||||
|
const stepInTextarea = ` - Orphan indented | 3`
|
||||||
|
|
||||||
|
const expectedSteps = [
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: 'Orphan indented',
|
||||||
|
estimation: 3
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(adaptTextareaToSteps(stepInTextarea)).toEqual(expectedSteps)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('handles multiple parents with their children', () => {
|
||||||
|
const stepInTextarea = `- Parent 1 | 5
|
||||||
|
- Child 1a | 2
|
||||||
|
- Child 1b | 3
|
||||||
|
- Parent 2 | 4
|
||||||
|
- Child 2a | 1`
|
||||||
|
|
||||||
|
const expectedSteps = [
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: '(Parent 1) - Child 1a',
|
||||||
|
estimation: 2
|
||||||
|
}),
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: '(Parent 1) - Child 1b',
|
||||||
|
estimation: 3
|
||||||
|
}),
|
||||||
|
fixtureStep({
|
||||||
|
id: expect.any(String),
|
||||||
|
title: '(Parent 2) - Child 2a',
|
||||||
|
estimation: 1
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(adaptTextareaToSteps(stepInTextarea)).toEqual(expectedSteps)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import type { Stepable } from '../interfaces/stepable'
|
|||||||
export const adaptStepsToTextarea = (steps: Stepable[]) =>
|
export const adaptStepsToTextarea = (steps: Stepable[]) =>
|
||||||
steps.map((step) => `- ${step.title} | ${step.estimation}`).join('\n')
|
steps.map((step) => `- ${step.title} | ${step.estimation}`).join('\n')
|
||||||
|
|
||||||
|
const isIndented = (line: string): boolean => /^[\t ]+/.test(line)
|
||||||
|
|
||||||
const extractTitleAndEstimationFromStep = (
|
const extractTitleAndEstimationFromStep = (
|
||||||
rawStep: string
|
rawStep: string
|
||||||
): [string, number] => {
|
): [string, number] => {
|
||||||
@@ -23,22 +25,58 @@ const extractTitleAndEstimationFromStep = (
|
|||||||
return [title, estimation]
|
return [title, estimation]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const adaptTextareaToSteps = (textareaValue: string): Stepable[] =>
|
export const adaptTextareaToSteps = (textareaValue: string): Stepable[] => {
|
||||||
textareaValue
|
const lines = textareaValue.split('\n')
|
||||||
.split('\n')
|
const result: Array<{ title: string; estimation: number }> = []
|
||||||
.map((rawStep) => {
|
|
||||||
const [title, estimation] = extractTitleAndEstimationFromStep(rawStep)
|
|
||||||
|
|
||||||
if (!title) {
|
let currentParent: { title: string; estimation: number } | null = null
|
||||||
return null
|
let parentHasChildren = false
|
||||||
|
|
||||||
|
const flushParent = () => {
|
||||||
|
if (currentParent && !parentHasChildren) {
|
||||||
|
result.push(currentParent)
|
||||||
|
}
|
||||||
|
currentParent = null
|
||||||
|
parentHasChildren = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return { id: generateId(`${title}-${estimation}`), title, estimation }
|
for (const line of lines) {
|
||||||
})
|
const [title, estimation] = extractTitleAndEstimationFromStep(line)
|
||||||
.filter((step): step is Stepable => step !== null)
|
|
||||||
|
if (!title) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIndented(line)) {
|
||||||
|
// Indented line - flatten with parent prefix if parent exists
|
||||||
|
if (currentParent && currentParent.title) {
|
||||||
|
const flattenedTitle = `(${currentParent.title}) - ${title}`
|
||||||
|
result.push({ title: flattenedTitle, estimation })
|
||||||
|
parentHasChildren = true
|
||||||
|
} else {
|
||||||
|
// Orphan indented line - treat as regular step
|
||||||
|
result.push({ title, estimation })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-indented line - potential parent
|
||||||
|
flushParent()
|
||||||
|
currentParent = { title, estimation }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush any remaining parent
|
||||||
|
flushParent()
|
||||||
|
|
||||||
|
return result
|
||||||
|
.map(({ title, estimation }) => ({
|
||||||
|
id: generateId(`${title}-${estimation}`),
|
||||||
|
title,
|
||||||
|
estimation
|
||||||
|
}))
|
||||||
.map((step, index, steps) => {
|
.map((step, index, steps) => {
|
||||||
const subSteps = steps.slice(0, index + 1)
|
const subSteps = steps.slice(0, index + 1)
|
||||||
const duplicates = subSteps.filter((s) => s.id === step.id).length
|
const duplicates = subSteps.filter((s) => s.id === step.id).length
|
||||||
|
|
||||||
return { ...step, id: `${step.id}-${duplicates}` }
|
return { ...step, id: `${step.id}-${duplicates}` }
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user