diff --git a/TEMPLATE_V2_GUIDE.md b/TEMPLATE_V2_GUIDE.md index 9b6c040..f2db0cc 100644 --- a/TEMPLATE_V2_GUIDE.md +++ b/TEMPLATE_V2_GUIDE.md @@ -12,7 +12,7 @@ A valid template-v2.svg file must contain: 2. **`` section** containing three required template elements: - `` - Defines how each month column is rendered - `` - Defines how each lane (epic/swimlane) is rendered - - `` - Defines how each task item is rendered + - `` - Defines how each task item is rendered 3. **Main content area** with `{{MONTHS}}` and `{{LANES}}` placeholders 4. **Optional styling** (gradients, filters, patterns, etc.) @@ -37,11 +37,11 @@ A valid template-v2.svg file must contain: font-family="Arial" font-size="14" font-weight="bold" fill="#212121">{{LANE_NAME}} - - @@ -80,16 +80,38 @@ A valid template-v2.svg file must contain: | `{{LABEL_Y}}` | Y position for lane label | 140 | | `{{LANE_NAME}}` | Lane name text (XML-escaped) | "Development" | -### Task Template Placeholders +### Item Template Placeholders | Placeholder | Description | Example Value | |------------|-------------|---------------| -| `{{TASK_X}}` | X position of task marker | 400 | -| `{{TASK_Y}}` | Y position of task marker | 150 | -| `{{TEXT_X}}` | X position for task text | 412 | -| `{{TEXT_Y}}` | Y position for task text | 154 | -| `{{TASK_ID}}` | Task ID (XML-escaped) | "T-123" | -| `{{TASK_TITLE}}` | Task title (XML-escaped) | "Implement feature" | +| `{{ITEM_X}}` | X position of item marker | 400 | +| `{{ITEM_Y}}` | Y position of item marker | 150 | +| `{{TEXT_X}}` | X position for item text | 412 | +| `{{TEXT_Y}}` | Y position for item text | 154 | +| `{{ITEM_ID}}` | Item ID (XML-escaped) | "T-123" | +| `{{ITEM_TITLE}}` | Item title (XML-escaped) | "Implement feature" | + +**Dynamic Data Placeholders:** + +The generator automatically creates placeholders for **all properties** in your CSV data using the naming convention: `ITEM_{PROPERTY_UPPERCASE}`. + +For example, if your `fieldMapping` includes: +```json +{ + "id": "ID", + "title": "Title", + "assignee": "Assignee", + "priority": "Priority" +} +``` + +The following placeholders become available: +- `{{ITEM_ID}}` - from the `id` field +- `{{ITEM_TITLE}}` - from the `title` field +- `{{ITEM_ASSIGNEE}}` - from the `assignee` field +- `{{ITEM_PRIORITY}}` - from the `priority` field + +**Note:** The `due` field is used for positioning and is not available as a placeholder (use `{{MONTH_LABEL}}` for date display). ### Global Placeholders @@ -107,7 +129,7 @@ These appear in the main template body (not in template elements): 1. Open template-v2.svg in Inkscape 2. Locate template elements in the Layers panel (inside ``) 3. Edit shapes, colors, fonts, etc. as needed -4. **Important**: Keep `id="month-template"`, `id="lane-template"`, `id="task-template"` unchanged +4. **Important**: Keep `id="month-template"`, `id="lane-template"`, `id="item-template"` unchanged 5. **Important**: Keep `{{PLACEHOLDER}}` text exactly as is - these are replaced at runtime 6. Save file (keep SVG format, avoid Inkscape-specific extensions) @@ -190,8 +212,8 @@ Use SVG filters: - @@ -229,7 +251,7 @@ To change the overall layout, you would need to modify generator.js. Templates c - Ensure template has all three required elements in ``: - `` - `` - - `` + - `` - Check that IDs are exactly as shown (case-sensitive) - Verify elements are inside `` section diff --git a/example/template-v2.svg b/example/template-v2.svg index cc4a6c2..e2e053c 100644 --- a/example/template-v2.svg +++ b/example/template-v2.svg @@ -36,12 +36,12 @@ font-size="14" font-weight="700">{{LANE_NAME}} - diff --git a/generator.js b/generator.js index 68cc9d0..d097b11 100644 --- a/generator.js +++ b/generator.js @@ -54,13 +54,13 @@ window.timelineGenerator = { validateTemplate(template) { const hasMonthTemplate = template.includes('id="month-template"'); const hasLaneTemplate = template.includes('id="lane-template"'); - const hasTaskTemplate = template.includes('id="task-template"'); + const hasItemTemplate = template.includes('id="item-template"'); - if (!hasMonthTemplate || !hasLaneTemplate || !hasTaskTemplate) { + if (!hasMonthTemplate || !hasLaneTemplate || !hasItemTemplate) { const missing = []; if (!hasMonthTemplate) missing.push('month-template'); if (!hasLaneTemplate) missing.push('lane-template'); - if (!hasTaskTemplate) missing.push('task-template'); + if (!hasItemTemplate) missing.push('item-template'); throw new Error( `Template is missing required elements: ${missing.join(', ')}. ` + @@ -97,7 +97,7 @@ window.timelineGenerator = { // Extract template elements (will throw if not found) const monthTemplate = this.extractTemplate(template, 'month-template'); const laneTemplate = this.extractTemplate(template, 'lane-template'); - const taskTemplate = this.extractTemplate(template, 'task-template'); + const itemTemplate = this.extractTemplate(template, 'item-template'); const monthLabelY = 90; const gridTop = top - 20; @@ -156,7 +156,7 @@ window.timelineGenerator = { laneElement = laneElement.replace(/id="lane-template"/, ''); laneBlocks += laneElement; - // Generate tasks for this lane + // Generate items for this lane const laneItems = laneMap.get(laneName).sort((a, b) => a.due - b.due); laneItems.forEach((it, idx) => { const mi = monthIndexForDate(it.due); @@ -164,19 +164,26 @@ window.timelineGenerator = { const cx = left + clampedMi * monthWidth + monthWidth * 0.5; const cy = laneY + 10 + idx * 18; - const taskValues = { - TASK_X: cx, - TASK_Y: cy, + // Start with layout placeholders + const itemValues = { + ITEM_X: cx, + ITEM_Y: cy, TEXT_X: cx + 12, - TEXT_Y: cy + 4, - TASK_ID: this.escapeXml(it.id || ""), - TASK_TITLE: this.escapeXml(it.title || "") + TEXT_Y: cy + 4 }; - let taskElement = this.replacePlaceholders(taskTemplate, taskValues); - taskElement = taskElement.replace(/style="display:\s*none;?"/, ''); - taskElement = taskElement.replace(/id="task-template"/, ''); - laneBlocks += taskElement; + // Dynamically add data placeholders from all item properties + for (const [key, value] of Object.entries(it)) { + if (key !== 'due') { // Skip due date as it's used for positioning + const placeholderName = `ITEM_${key.toUpperCase()}`; + itemValues[placeholderName] = this.escapeXml(value || ""); + } + } + + let itemElement = this.replacePlaceholders(itemTemplate, itemValues); + itemElement = itemElement.replace(/style="display:\s*none;?"/, ''); + itemElement = itemElement.replace(/id="item-template"/, ''); + laneBlocks += itemElement; }); }); @@ -193,7 +200,7 @@ window.timelineGenerator = { processedTemplate = processedTemplate .replace(/]*>[\s\S]*?<\/g>/, '') .replace(/]*>[\s\S]*?<\/g>/, '') - .replace(/]*>[\s\S]*?<\/g>/, ''); + .replace(/]*>[\s\S]*?<\/g>/, ''); // Add width and height attributes to the SVG element processedTemplate = processedTemplate.replace( diff --git a/my-project/template-v2.svg b/my-project/template-v2.svg index a05b2ed..96d4856 100644 --- a/my-project/template-v2.svg +++ b/my-project/template-v2.svg @@ -36,12 +36,12 @@ font-size="14" font-weight="700">{{LANE_NAME}} - diff --git a/test/generator.test.js b/test/generator.test.js index dc63a36..9632c56 100644 --- a/test/generator.test.js +++ b/test/generator.test.js @@ -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"') }) diff --git a/test/integration.test.js b/test/integration.test.js index f4593db..04d1e28 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -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()) diff --git a/test/testHelpers.js b/test/testHelpers.js index 6cff178..858dc2d 100644 --- a/test/testHelpers.js +++ b/test/testHelpers.js @@ -61,9 +61,9 @@ export const createSampleTemplate = () => ` - - {{TASK_ID}} {{TASK_TITLE}} + @@ -80,8 +80,8 @@ export const createMalformedTemplate = (missingElement = 'month-template') => { - `, - 'task-template': ` + 'item-template': `