refactor: use abstract ITEM placeholders with dynamic property mapping

Changed from fixed TASK placeholders to flexible ITEM placeholders that
automatically map all CSV fields to template placeholders.

Key changes:
- Renamed task-template → item-template in all templates
- Changed TASK_* → ITEM_* placeholder naming
- Implemented dynamic placeholder generation from item properties
- Any field in fieldMapping now creates ITEM_{FIELD} placeholder
- Updated all tests and documentation

Naming convention: CSV field → item.property → ITEM_PROPERTY
Example: "assignee" → item.assignee → {{ITEM_ASSIGNEE}}

This enables users to add custom fields without modifying generator code:
- Add "assignee": "Assignee" to fieldMapping
- Use {{ITEM_ASSIGNEE}} in template
- No code changes required

Benefits:
- More flexible and extensible
- Clearer abstraction (items vs tasks)
- Consistent naming convention
- Better documented

All 56 tests passing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-27 23:17:34 +01:00
parent dd3ba4df58
commit 2bab447fa8
7 changed files with 86 additions and 57 deletions

View File

@@ -66,12 +66,12 @@ describe('Timeline Generator', () => {
}).toThrow('Template is missing required elements: lane-template')
})
it('should throw error when template is missing task-template', () => {
const malformedTemplate = createMalformedTemplate('task-template')
it('should throw error when template is missing item-template', () => {
const malformedTemplate = createMalformedTemplate('item-template')
expect(() => {
timelineGenerator.generate(items, config, malformedTemplate)
}).toThrow('Template is missing required elements: task-template')
}).toThrow('Template is missing required elements: item-template')
})
it('should validate template with proper error message', () => {
@@ -179,8 +179,8 @@ describe('Timeline Generator', () => {
expect(result).not.toContain('{{MONTH_X}}')
expect(result).not.toContain('{{LANE_Y}}')
expect(result).not.toContain('{{TASK_X}}')
expect(result).not.toContain('{{TASK_TITLE}}')
expect(result).not.toContain('{{ITEM_X}}')
expect(result).not.toContain('{{ITEM_TITLE}}')
})
it('should contain task data in generated SVG', () => {
@@ -248,7 +248,7 @@ describe('Timeline Generator', () => {
expect(result).not.toContain('id="month-template"')
expect(result).not.toContain('id="lane-template"')
expect(result).not.toContain('id="task-template"')
expect(result).not.toContain('id="item-template"')
expect(result).not.toContain('style="display:none"')
})

View File

@@ -58,7 +58,7 @@ describe('Timeline Integration', () => {
// Verify no template placeholders remain
expect(viewer.innerHTML).not.toContain('{{MONTH_X}}')
expect(viewer.innerHTML).not.toContain('{{LANE_Y}}')
expect(viewer.innerHTML).not.toContain('{{TASK_X}}')
expect(viewer.innerHTML).not.toContain('{{ITEM_X}}')
// Verify download button enabled
const downloadBtn = document.getElementById('downloadSvg')
@@ -257,9 +257,9 @@ describe('Timeline Integration', () => {
expect(viewer).toBeTruthy()
})
it('should reject malformed template-v2 (missing task-template)', async () => {
it('should reject malformed template-v2 (missing item-template)', async () => {
const config = createSampleProject()
const malformedTemplate = createMalformedTemplate('task-template')
const malformedTemplate = createMalformedTemplate('item-template')
mockFetch(malformedTemplate)
mockFetch(createSampleCSV())

View File

@@ -61,9 +61,9 @@ export const createSampleTemplate = () => `<svg xmlns="http://www.w3.org/2000/sv
</g>
<!-- Task template -->
<g id="task-template" style="display:none">
<circle cx="{{TASK_X}}" cy="{{TASK_Y}}" r="6" fill="#1976D2"/>
<text x="{{TEXT_X}}" y="{{TEXT_Y}}" font-family="Arial" font-size="11" fill="#424242">{{TASK_ID}} {{TASK_TITLE}}</text>
<g id="item-template" style="display:none">
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2"/>
<text x="{{TEXT_X}}" y="{{TEXT_Y}}" font-family="Arial" font-size="11" fill="#424242">{{ITEM_ID}} {{ITEM_TITLE}}</text>
</g>
</defs>
@@ -80,8 +80,8 @@ export const createMalformedTemplate = (missingElement = 'month-template') => {
<g id="lane-template" style="display:none">
<rect x="{{LANE_X}}" y="{{LANE_Y}}" width="{{LANE_WIDTH}}" height="{{LANE_HEIGHT}}" fill="#FAFAFA"/>
</g>
<g id="task-template" style="display:none">
<circle cx="{{TASK_X}}" cy="{{TASK_Y}}" r="6" fill="#1976D2"/>
<g id="item-template" style="display:none">
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2"/>
</g>
</defs>
{{MONTHS}}
@@ -92,14 +92,14 @@ export const createMalformedTemplate = (missingElement = 'month-template') => {
<g id="month-template" style="display:none">
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}" stroke="#E0E0E0"/>
</g>
<g id="task-template" style="display:none">
<circle cx="{{TASK_X}}" cy="{{TASK_Y}}" r="6" fill="#1976D2"/>
<g id="item-template" style="display:none">
<circle cx="{{ITEM_X}}" cy="{{ITEM_Y}}" r="6" fill="#1976D2"/>
</g>
</defs>
{{MONTHS}}
{{LANES}}
</svg>`,
'task-template': `<svg xmlns="http://www.w3.org/2000/svg">
'item-template': `<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<g id="month-template" style="display:none">
<line x1="{{MONTH_X}}" y1="{{GRID_TOP}}" x2="{{MONTH_X}}" y2="{{GRID_BOTTOM}}" stroke="#E0E0E0"/>