chore: release v2.4
- Add theme system (Dark/Light palettes, ModernTheme, ThemeManager) - Add InputDialog, Spinner common view - Add DuplicatesCsvExportService - Refresh views, dialogs, and view models across tabs - Update localization strings (en/fr) - Tweak services (transfer, permissions, search, user access, ownership elevation, bulk operations) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using SharepointToolbox.Core.Models;
|
||||
using SharepointToolbox.Localization;
|
||||
using System.Text;
|
||||
|
||||
namespace SharepointToolbox.Services.Export;
|
||||
@@ -10,17 +11,23 @@ namespace SharepointToolbox.Services.Export;
|
||||
/// </summary>
|
||||
public class DuplicatesHtmlExportService
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a self-contained HTML string rendering one collapsible card per
|
||||
/// <see cref="DuplicateGroup"/>. The document ships with inline CSS and a
|
||||
/// tiny JS toggle so no external assets are needed.
|
||||
/// </summary>
|
||||
public string BuildHtml(IReadOnlyList<DuplicateGroup> groups, ReportBranding? branding = null)
|
||||
{
|
||||
var T = TranslationSource.Instance;
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("<!DOCTYPE html>");
|
||||
sb.AppendLine("<html lang=\"en\">");
|
||||
sb.AppendLine("<head>");
|
||||
sb.AppendLine("<meta charset=\"UTF-8\">");
|
||||
sb.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
|
||||
sb.AppendLine($"<title>{T["report.title.duplicates"]}</title>");
|
||||
sb.AppendLine("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SharePoint Duplicate Detection Report</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', Arial, sans-serif; font-size: 13px; margin: 20px; background: #f5f5f5; }
|
||||
h1 { color: #0078d4; }
|
||||
@@ -54,11 +61,9 @@ public class DuplicatesHtmlExportService
|
||||
<body>
|
||||
""");
|
||||
sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding));
|
||||
sb.AppendLine("""
|
||||
<h1>Duplicate Detection Report</h1>
|
||||
""");
|
||||
sb.AppendLine($"<h1>{T["report.title.duplicates_short"]}</h1>");
|
||||
|
||||
sb.AppendLine($"<p class=\"summary\">{groups.Count:N0} duplicate group(s) found.</p>");
|
||||
sb.AppendLine($"<p class=\"summary\">{groups.Count:N0} {T["report.text.duplicate_groups_found"]}</p>");
|
||||
|
||||
for (int i = 0; i < groups.Count; i++)
|
||||
{
|
||||
@@ -70,19 +75,19 @@ public class DuplicatesHtmlExportService
|
||||
<div class="group-card">
|
||||
<div class="group-header" onclick="toggleGroup({i})">
|
||||
<span class="group-name">{H(g.Name)}</span>
|
||||
<span class="badge {badgeClass}">{count} copies</span>
|
||||
<span class="badge {badgeClass}">{count} {T["report.text.copies"]}</span>
|
||||
</div>
|
||||
<div class="group-body" id="gb-{i}">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Library</th>
|
||||
<th>Path</th>
|
||||
<th>Size</th>
|
||||
<th>Created</th>
|
||||
<th>Modified</th>
|
||||
<th>{T["report.col.number"]}</th>
|
||||
<th>{T["report.col.name"]}</th>
|
||||
<th>{T["report.col.library"]}</th>
|
||||
<th>{T["report.col.path"]}</th>
|
||||
<th>{T["report.col.size"]}</th>
|
||||
<th>{T["report.col.created"]}</th>
|
||||
<th>{T["report.col.modified"]}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -116,18 +121,59 @@ public class DuplicatesHtmlExportService
|
||||
""");
|
||||
}
|
||||
|
||||
sb.AppendLine($"<p class=\"generated\">Generated: {DateTime.Now:yyyy-MM-dd HH:mm}</p>");
|
||||
sb.AppendLine($"<p class=\"generated\">{T["report.text.generated_colon"]} {DateTime.Now:yyyy-MM-dd HH:mm}</p>");
|
||||
sb.AppendLine("</body></html>");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>Writes the HTML report to the specified file path using UTF-8.</summary>
|
||||
public async Task WriteAsync(IReadOnlyList<DuplicateGroup> groups, string filePath, CancellationToken ct, ReportBranding? branding = null)
|
||||
{
|
||||
var html = BuildHtml(groups, branding);
|
||||
await System.IO.File.WriteAllTextAsync(filePath, html, Encoding.UTF8, ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes one or more HTML reports depending on <paramref name="splitMode"/> and
|
||||
/// <paramref name="layout"/>. Single → one file. BySite + SeparateFiles → one
|
||||
/// file per site. BySite + SingleTabbed → one file with per-site iframe tabs.
|
||||
/// </summary>
|
||||
public async Task WriteAsync(
|
||||
IReadOnlyList<DuplicateGroup> groups,
|
||||
string basePath,
|
||||
ReportSplitMode splitMode,
|
||||
HtmlSplitLayout layout,
|
||||
CancellationToken ct,
|
||||
ReportBranding? branding = null)
|
||||
{
|
||||
if (splitMode != ReportSplitMode.BySite)
|
||||
{
|
||||
await WriteAsync(groups, basePath, ct, branding);
|
||||
return;
|
||||
}
|
||||
|
||||
var partitions = DuplicatesCsvExportService.PartitionBySite(groups).ToList();
|
||||
|
||||
if (layout == HtmlSplitLayout.SingleTabbed)
|
||||
{
|
||||
var parts = partitions
|
||||
.Select(p => (p.Label, Html: BuildHtml(p.Partition, branding)))
|
||||
.ToList();
|
||||
var T = TranslationSource.Instance;
|
||||
var tabbed = ReportSplitHelper.BuildTabbedHtml(parts, T["report.title.duplicates_short"]);
|
||||
await System.IO.File.WriteAllTextAsync(basePath, tabbed, Encoding.UTF8, ct);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var partition in partitions)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var path = ReportSplitHelper.BuildPartitionPath(basePath, partition.Label);
|
||||
await WriteAsync(partition.Partition, path, ct, branding);
|
||||
}
|
||||
}
|
||||
|
||||
private static string H(string value) =>
|
||||
System.Net.WebUtility.HtmlEncode(value ?? string.Empty);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user