Skip to content

Commit a6a2872

Browse files
committed
feat: reorganize HTML report structure to implement issue #49
- Implement hierarchical change category organization with collapsible type groups - Change from type-first to change-category-first organization - Add collapsible type groups that default to collapsed state - Fix summary navigation links to properly navigate to sections - Update HTML template structure: * Modify HtmlFormatterScriban.cs to use GroupChangesByType method * Update main-layout.scriban with collapsible type groups and navigation * Enhance styles.css with type group collapsibility and hover effects * Add scripts.js toggle functionality with session storage persistence - Make section headers fully clickable (not just arrow buttons) - Add consistent hover effects for section and type headers - Fix CLI workflow tests to expect correct exit codes - All 425 tests passing Resolves #49
1 parent 70a6209 commit a6a2872

File tree

8 files changed

+557
-84
lines changed

8 files changed

+557
-84
lines changed

src/DotNetApiDiff/Reporting/HtmlFormatterScriban.cs

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
172172
title = "Added Items",
173173
count = addedItems.Count,
174174
change_type = "added",
175-
grouped_changes = GroupChanges(addedItems)
175+
grouped_changes = GroupChangesByType(addedItems)
176176
});
177177
}
178178

@@ -185,7 +185,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
185185
title = "Removed Items",
186186
count = removedItems.Count,
187187
change_type = "removed",
188-
grouped_changes = GroupChanges(removedItems)
188+
grouped_changes = GroupChangesByType(removedItems)
189189
});
190190
}
191191

@@ -198,7 +198,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
198198
title = "Modified Items",
199199
count = modifiedItems.Count,
200200
change_type = "modified",
201-
grouped_changes = GroupChanges(modifiedItems)
201+
grouped_changes = GroupChangesByType(modifiedItems)
202202
});
203203
}
204204

@@ -211,7 +211,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
211211
title = "Moved Items",
212212
count = movedItems.Count,
213213
change_type = "moved",
214-
grouped_changes = GroupChanges(movedItems)
214+
grouped_changes = GroupChangesByType(movedItems)
215215
});
216216
}
217217

@@ -225,7 +225,7 @@ private object[] PrepareChangeSections(ComparisonResult result)
225225
count = excludedItems.Count,
226226
change_type = "excluded",
227227
description = "The following items were intentionally excluded from the comparison:",
228-
grouped_changes = GroupChanges(excludedItems)
228+
grouped_changes = GroupChangesByType(excludedItems)
229229
});
230230
}
231231

@@ -253,6 +253,85 @@ private object[] GroupChanges(List<ApiDifference> changes)
253253
}).ToArray();
254254
}
255255

256+
private object[] GroupChangesByType(List<ApiDifference> changes)
257+
{
258+
// Group changes by the containing type first
259+
return changes
260+
.GroupBy(d => ExtractContainingType(d.ElementName, d.ElementType))
261+
.OrderBy(g => g.Key)
262+
.Select(typeGroup => new
263+
{
264+
key = GetTypeDisplayName(typeGroup.Key),
265+
full_type_name = typeGroup.Key,
266+
count = typeGroup.Count(),
267+
breaking_changes_count = typeGroup.Count(c => c.IsBreakingChange),
268+
has_breaking_changes = typeGroup.Any(c => c.IsBreakingChange),
269+
changes = typeGroup.OrderBy(c => c.ElementName).Select(c => new
270+
{
271+
element_name = GetMemberDisplayName(c.ElementName, c.ElementType),
272+
full_element_name = c.ElementName,
273+
element_type = c.ElementType.ToString(),
274+
description = c.Description,
275+
is_breaking_change = c.IsBreakingChange,
276+
has_signatures = !string.IsNullOrEmpty(c.OldSignature) || !string.IsNullOrEmpty(c.NewSignature),
277+
old_signature = HtmlEscape(c.OldSignature),
278+
new_signature = HtmlEscape(c.NewSignature),
279+
details_id = $"details-{Guid.NewGuid():N}",
280+
severity = c.Severity.ToString().ToLower()
281+
}).ToArray()
282+
}).ToArray();
283+
}
284+
285+
private string ExtractContainingType(string elementName, ApiElementType elementType)
286+
{
287+
// For type-level changes, the element name is the type name
288+
if (elementType == ApiElementType.Type)
289+
{
290+
return elementName;
291+
}
292+
293+
// For member-level changes, extract the type from the full name
294+
// Expected format: TypeName.MemberName or Namespace.TypeName.MemberName
295+
var lastDotIndex = elementName.LastIndexOf('.');
296+
if (lastDotIndex > 0)
297+
{
298+
var typePart = elementName.Substring(0, lastDotIndex);
299+
300+
// Handle nested types (Type+NestedType.Member)
301+
var plusIndex = typePart.LastIndexOf('+');
302+
if (plusIndex > 0)
303+
{
304+
return typePart; // Keep the full nested type name
305+
}
306+
307+
return typePart;
308+
}
309+
310+
// Fallback for malformed names
311+
return elementName;
312+
}
313+
314+
private string GetTypeDisplayName(string typeName)
315+
{
316+
// Extract just the type name without namespace for display
317+
var lastDotIndex = typeName.LastIndexOf('.');
318+
return lastDotIndex > 0 ? typeName.Substring(lastDotIndex + 1) : typeName;
319+
}
320+
321+
private string GetMemberDisplayName(string elementName, ApiElementType elementType)
322+
{
323+
// For type-level changes, return the simple type name
324+
if (elementType == ApiElementType.Type)
325+
{
326+
var lastDotIndex = elementName.LastIndexOf('.');
327+
return lastDotIndex > 0 ? elementName.Substring(lastDotIndex + 1) : elementName;
328+
}
329+
330+
// For member-level changes, return just the member name
331+
var memberDotIndex = elementName.LastIndexOf('.');
332+
return memberDotIndex > 0 ? elementName.Substring(memberDotIndex + 1) : elementName;
333+
}
334+
256335
private object[] PrepareBreakingChangesData(IEnumerable<ApiDifference> breakingChanges)
257336
{
258337
return breakingChanges.OrderBy(d => d.Severity)
Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,55 @@
11
{{for group in grouped_changes}}
2-
<div class="change-group">
3-
<h3>{{ group.key }} ({{ group.count }})</h3>
4-
<div class="change-items">
5-
{{for change in group.changes}}
6-
<div class="change-item {{ change_type }}">
7-
<div class="change-header">
8-
<div class="change-name">
9-
<code>{{ change.element_name }}</code>
10-
{{if change.is_breaking_change}}
11-
<span class="breaking-badge">BREAKING</span>
12-
{{end}}
2+
<div class="type-group">
3+
<div class="type-header">
4+
<h3 class="type-name">
5+
📁 {{ group.key }}
6+
<span class="type-summary">
7+
({{ group.count }} change{{ if group.count != 1 }}s{{ end }}{{ if group.has_breaking_changes }}, {{ group.breaking_changes_count }} breaking{{ end }})
8+
</span>
9+
</h3>
10+
{{if group.has_breaking_changes}}
11+
<span class="breaking-type-badge">⚠️ Breaking Changes</span>
12+
{{end}}
13+
</div>
14+
15+
<div class="type-changes">
16+
<div class="category-changes">
17+
{{for change in group.changes}}
18+
<div class="change-item {{ change_type }}">
19+
<div class="change-header">
20+
<div class="change-name">
21+
<span class="element-type">{{ change.element_type }}</span>
22+
<code>{{ change.element_name }}</code>
23+
{{if change.is_breaking_change}}
24+
<span class="breaking-badge">BREAKING</span>
25+
{{end}}
26+
</div>
27+
<div class="change-description">{{ change.description }}</div>
1328
</div>
14-
<div class="change-description">{{ change.description }}</div>
15-
</div>
16-
{{if change.has_signatures}}
17-
<div class="signature-toggle">
18-
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
19-
<span class="toggle-icon">▼</span> View Signature Details
20-
</button>
21-
</div>
22-
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
23-
{{if change.old_signature}}
24-
<div class="signature-section">
25-
<h4>Old Signature:</h4>
26-
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
29+
{{if change.has_signatures}}
30+
<div class="signature-toggle">
31+
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
32+
<span class="toggle-icon">▼</span> View Signature Details
33+
</button>
2734
</div>
28-
{{end}}
29-
{{if change.new_signature}}
30-
<div class="signature-section">
31-
<h4>New Signature:</h4>
32-
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
35+
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
36+
{{if change.old_signature}}
37+
<div class="signature-section">
38+
<h4>Old Signature:</h4>
39+
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
40+
</div>
41+
{{end}}
42+
{{if change.new_signature}}
43+
<div class="signature-section">
44+
<h4>New Signature:</h4>
45+
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
46+
</div>
47+
{{end}}
3348
</div>
3449
{{end}}
3550
</div>
3651
{{end}}
3752
</div>
38-
{{end}}
3953
</div>
4054
</div>
4155
{{end}}

src/DotNetApiDiff/Reporting/HtmlTemplates/main-layout.scriban

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,39 +56,18 @@
5656
<section class="summary">
5757
<h2>📈 Summary</h2>
5858
<div class="summary-cards">
59-
{{if result.summary.added_count > 0}}
6059
<div class="summary-card added clickable" onclick="navigateToSection('added-items')">
6160
<div class="card-number">{{ result.summary.added_count }}</div>
6261
<div class="card-label">Added</div>
6362
</div>
64-
{{else}}
65-
<div class="summary-card added">
66-
<div class="card-number">{{ result.summary.added_count }}</div>
67-
<div class="card-label">Added</div>
68-
</div>
69-
{{end}}
70-
{{if result.summary.removed_count > 0}}
7163
<div class="summary-card removed clickable" onclick="navigateToSection('removed-items')">
7264
<div class="card-number">{{ result.summary.removed_count }}</div>
7365
<div class="card-label">Removed</div>
7466
</div>
75-
{{else}}
76-
<div class="summary-card removed">
77-
<div class="card-number">{{ result.summary.removed_count }}</div>
78-
<div class="card-label">Removed</div>
79-
</div>
80-
{{end}}
81-
{{if result.summary.modified_count > 0}}
8267
<div class="summary-card modified clickable" onclick="navigateToSection('modified-items')">
8368
<div class="card-number">{{ result.summary.modified_count }}</div>
8469
<div class="card-label">Modified</div>
8570
</div>
86-
{{else}}
87-
<div class="summary-card modified">
88-
<div class="card-number">{{ result.summary.modified_count }}</div>
89-
<div class="card-label">Modified</div>
90-
</div>
91-
{{end}}
9271
{{if result.summary.breaking_changes_count > 0}}
9372
<div class="summary-card breaking clickable" onclick="navigateToSection('breaking-changes')">
9473
<div class="card-number">{{ result.summary.breaking_changes_count }}</div>
@@ -120,9 +99,9 @@
12099
<!-- Breaking changes section -->
121100
{{if result.has_breaking_changes}}
122101
<section class="breaking-changes" id="breaking-changes">
123-
<div class="section-header">
102+
<div class="section-header" onclick="toggleSection('breaking-changes')">
124103
<h2>⚠️ Breaking Changes</h2>
125-
<button class="section-toggle collapsed" onclick="toggleSection('breaking-changes')">
104+
<button class="section-toggle collapsed">
126105
<span class="toggle-icon">▶</span>
127106
</button>
128107
</div>
@@ -160,17 +139,77 @@
160139
<!-- Change sections -->
161140
{{for section in change_sections}}
162141
<section class="changes-section" id="{{ section.change_type }}-items">
163-
<div class="section-header">
142+
<div class="section-header" onclick="toggleSection('{{ section.change_type }}-items')">
164143
<h2>{{ section.icon }} {{ section.title }} ({{ section.count }})</h2>
165-
<button class="section-toggle collapsed" onclick="toggleSection('{{ section.change_type }}-items')">
144+
<button class="section-toggle collapsed">
166145
<span class="toggle-icon">▶</span>
167146
</button>
168147
</div>
169148
<div class="section-content collapsed" id="{{ section.change_type }}-items-content">
170149
{{if section.description}}
171150
<p class="section-description">{{ section.description }}</p>
172151
{{end}}
173-
{{ render_change_group section }}
152+
<!-- Type groups within this change category -->
153+
{{for group in section.grouped_changes}}
154+
<div class="type-group">
155+
<div class="type-header" onclick="toggleTypeGroup('{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}')">
156+
<h3 class="type-name">
157+
📁 {{ group.key }}
158+
<span class="type-summary">
159+
({{ group.count }} change{{ if group.count != 1 }}s{{ end }}{{ if group.has_breaking_changes }}, {{ group.breaking_changes_count }} breaking{{ end }})
160+
</span>
161+
</h3>
162+
<div class="type-header-controls">
163+
{{if group.has_breaking_changes}}
164+
<span class="breaking-type-badge">⚠️ Breaking Changes</span>
165+
{{end}}
166+
<button class="type-toggle collapsed">
167+
<span class="toggle-icon">▶</span>
168+
</button>
169+
</div>
170+
</div>
171+
172+
<div class="type-changes collapsed" id="{{ section.change_type }}-{{ group.key | string.replace " " "-" | string.replace "." "-" | string.replace "`" "-" }}-content">
173+
<div class="category-changes">
174+
{{for change in group.changes}}
175+
<div class="change-item {{ section.change_type }}">
176+
<div class="change-header">
177+
<div class="change-name">
178+
<span class="element-type">{{ change.element_type }}</span>
179+
<code>{{ change.element_name }}</code>
180+
{{if change.is_breaking_change}}
181+
<span class="breaking-badge">BREAKING</span>
182+
{{end}}
183+
</div>
184+
<div class="change-description">{{ change.description }}</div>
185+
</div>
186+
{{if change.has_signatures}}
187+
<div class="signature-toggle">
188+
<button class="toggle-btn" onclick="toggleSignature('{{ change.details_id }}')">
189+
<span class="toggle-icon">▼</span> View Signature Details
190+
</button>
191+
</div>
192+
<div id="{{ change.details_id }}" class="signature-details" style="display: none;">
193+
{{if change.old_signature}}
194+
<div class="signature-section">
195+
<h5>Old Signature:</h5>
196+
<pre><code class="csharp">{{ change.old_signature }}</code></pre>
197+
</div>
198+
{{end}}
199+
{{if change.new_signature}}
200+
<div class="signature-section">
201+
<h5>New Signature:</h5>
202+
<pre><code class="csharp">{{ change.new_signature }}</code></pre>
203+
</div>
204+
{{end}}
205+
</div>
206+
{{end}}
207+
</div>
208+
{{end}}
209+
</div>
210+
</div>
211+
</div>
212+
{{end}}
174213
</div>
175214
</section>
176215
{{end}}

0 commit comments

Comments
 (0)