diff --git a/SharepointToolbox/Localization/Strings.fr.resx b/SharepointToolbox/Localization/Strings.fr.resx index e9b942f..c85398d 100644 --- a/SharepointToolbox/Localization/Strings.fr.resx +++ b/SharepointToolbox/Localization/Strings.fr.resx @@ -536,7 +536,10 @@ Erreur Horodatage # +<<<<<<< HEAD Groupe +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 Taille totale (Mo) Taille des versions (Mo) Taille (Mo) @@ -559,6 +562,7 @@ priv. élevé Stockage par type de fichier Détails des bibliothèques +<<<<<<< HEAD Sélectionner les sites Filtre : @@ -650,4 +654,6 @@ utilisateur(s) fichiers sites +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 diff --git a/SharepointToolbox/Localization/Strings.resx b/SharepointToolbox/Localization/Strings.resx index b0f2c92..fbf62a2 100644 --- a/SharepointToolbox/Localization/Strings.resx +++ b/SharepointToolbox/Localization/Strings.resx @@ -536,7 +536,10 @@ Error Timestamp # +<<<<<<< HEAD Group +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 Total Size (MB) Version Size (MB) Size (MB) @@ -559,6 +562,7 @@ high-priv Storage by File Type Library Details +<<<<<<< HEAD Select Sites Filter: @@ -650,4 +654,6 @@ user(s) files sites +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 diff --git a/SharepointToolbox/Services/Export/DuplicatesCsvExportService.cs b/SharepointToolbox/Services/Export/DuplicatesCsvExportService.cs index f4320aa..7a4111d 100644 --- a/SharepointToolbox/Services/Export/DuplicatesCsvExportService.cs +++ b/SharepointToolbox/Services/Export/DuplicatesCsvExportService.cs @@ -12,12 +12,16 @@ namespace SharepointToolbox.Services.Export; /// public class DuplicatesCsvExportService { +<<<<<<< HEAD /// Writes the CSV to with UTF-8 BOM (Excel-compatible). +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 public async Task WriteAsync( IReadOnlyList groups, string filePath, CancellationToken ct) { +<<<<<<< HEAD var csv = BuildCsv(groups); await ExportFileWriter.WriteCsvAsync(filePath, csv, ct); } @@ -59,6 +63,8 @@ public class DuplicatesCsvExportService /// public string BuildCsv(IReadOnlyList groups) { +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 var T = TranslationSource.Instance; var sb = new StringBuilder(); @@ -72,9 +78,14 @@ public class DuplicatesCsvExportService sb.AppendLine(string.Join(",", new[] { Csv(T["report.col.number"]), +<<<<<<< HEAD Csv(T["report.col.group"]), Csv(T["report.text.copies"]), Csv(T["report.col.site"]), +======= + Csv("Group"), + Csv(T["report.text.copies"]), +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 Csv(T["report.col.name"]), Csv(T["report.col.library"]), Csv(T["report.col.path"]), @@ -83,6 +94,10 @@ public class DuplicatesCsvExportService Csv(T["report.col.modified"]), })); +<<<<<<< HEAD +======= + // Rows +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 foreach (var g in groups) { int i = 0; @@ -94,7 +109,10 @@ public class DuplicatesCsvExportService Csv(i.ToString()), Csv(g.Name), Csv(g.Items.Count.ToString()), +<<<<<<< HEAD Csv(item.SiteTitle), +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 Csv(item.Name), Csv(item.Library), Csv(item.Path), @@ -105,8 +123,20 @@ public class DuplicatesCsvExportService } } +<<<<<<< HEAD return sb.ToString(); } private static string Csv(string value) => CsvSanitizer.Escape(value); +======= + await File.WriteAllTextAsync(filePath, sb.ToString(), + new UTF8Encoding(encoderShouldEmitUTF8Identifier: true), ct); + } + + private static string Csv(string value) + { + if (string.IsNullOrEmpty(value)) return "\"\""; + return $"\"{value.Replace("\"", "\"\"")}\""; + } +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 } diff --git a/SharepointToolbox/Services/Export/HtmlExportService.cs b/SharepointToolbox/Services/Export/HtmlExportService.cs index cd68a43..84175f8 100644 --- a/SharepointToolbox/Services/Export/HtmlExportService.cs +++ b/SharepointToolbox/Services/Export/HtmlExportService.cs @@ -2,7 +2,10 @@ using System.IO; using System.Text; using SharepointToolbox.Core.Models; using SharepointToolbox.Localization; +<<<<<<< HEAD using static SharepointToolbox.Services.Export.PermissionHtmlFragments; +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 namespace SharepointToolbox.Services.Export; @@ -30,6 +33,7 @@ public class HtmlExportService IReadOnlyDictionary>? groupMembers = null) { var T = TranslationSource.Instance; +<<<<<<< HEAD var (totalEntries, uniquePermSets, distinctUsers) = ComputeStats( entries.Count, entries.Select(e => e.PermissionLevels), @@ -43,6 +47,84 @@ public class HtmlExportService AppendStatsCards(sb, totalEntries, uniquePermSets, distinctUsers); AppendFilterInput(sb); AppendTableOpen(sb); +======= + // Compute stats + var totalEntries = entries.Count; + var uniquePermSets = entries.Select(e => e.PermissionLevels).Distinct().Count(); + var distinctUsers = entries + .SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries)) + .Select(u => u.Trim()) + .Where(u => u.Length > 0) + .Distinct() + .Count(); + + var sb = new StringBuilder(); + + // ── HTML HEAD ────────────────────────────────────────────────────────── + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine($"{T["report.title.permissions"]}"); + sb.AppendLine(""); + sb.AppendLine(""); + + // ── BODY ─────────────────────────────────────────────────────────────── + sb.AppendLine(""); + sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); + sb.AppendLine($"

{T["report.title.permissions"]}

"); + + // Stats cards + sb.AppendLine("
"); + sb.AppendLine($"
{totalEntries}
{T["report.stat.total_entries"]}
"); + sb.AppendLine($"
{uniquePermSets}
{T["report.stat.unique_permission_sets"]}
"); + sb.AppendLine($"
{distinctUsers}
{T["report.stat.distinct_users_groups"]}
"); + sb.AppendLine("
"); + + // Filter input + sb.AppendLine("
"); + sb.AppendLine($" "); + sb.AppendLine("
"); + + // Table + sb.AppendLine("
"); + sb.AppendLine(""); +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 sb.AppendLine(""); sb.AppendLine($" "); sb.AppendLine(""); @@ -55,9 +137,52 @@ public class HtmlExportService var uniqueCss = entry.HasUniquePermissions ? "badge unique" : "badge inherited"; var uniqueLbl = entry.HasUniquePermissions ? T["report.badge.unique"] : T["report.badge.inherited"]; +<<<<<<< HEAD var (pills, subRows) = BuildUserPillsCell( entry.UserLogins, entry.Users, entry.PrincipalType, groupMembers, colSpan: 7, grpMemIdx: ref grpMemIdx); +======= + // Build user pills: zip UserLogins and Users (both semicolon-delimited) + var logins = entry.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries); + var names = entry.Users.Split(';', StringSplitOptions.RemoveEmptyEntries); + var pillsBuilder = new StringBuilder(); + var memberSubRows = new StringBuilder(); + + for (int i = 0; i < logins.Length; i++) + { + var login = logins[i].Trim(); + var name = i < names.Length ? names[i].Trim() : login; + var isExt = login.Contains("#EXT#", StringComparison.OrdinalIgnoreCase); + + bool isExpandableGroup = entry.PrincipalType == "SharePointGroup" + && groupMembers != null + && groupMembers.TryGetValue(name, out var members); + + if (isExpandableGroup && groupMembers != null && groupMembers.TryGetValue(name, out var resolvedMembers)) + { + var grpId = $"grpmem{grpMemIdx}"; + pillsBuilder.Append($"{HtmlEncode(name)} ▼"); + + string memberContent; + if (resolvedMembers.Count > 0) + { + var memberParts = resolvedMembers.Select(m => $"{HtmlEncode(m.DisplayName)} <{HtmlEncode(m.Login)}>"); + memberContent = string.Join(" • ", memberParts); + } + else + { + memberContent = $"{T["report.text.members_unavailable"]}"; + } + memberSubRows.AppendLine($""); + grpMemIdx++; + } + else + { + var pillCss = isExt ? "user-pill external-user" : "user-pill"; + pillsBuilder.Append($"{HtmlEncode(name)}"); + } + } +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 sb.AppendLine(""); sb.AppendLine($" "); @@ -291,4 +416,232 @@ public class HtmlExportService RiskLevel.ReadOnly => ("#DBEAFE", "#1E40AF", "#BFDBFE"), _ => ("#F3F4F6", "#374151", "#E5E7EB") }; +<<<<<<< HEAD +======= + + /// + /// Builds a self-contained HTML string from simplified permission entries. + /// Includes risk-level summary cards, color-coded rows, and simplified labels column. + /// When is provided, SharePoint group pills become expandable. + /// + public string BuildHtml(IReadOnlyList entries, ReportBranding? branding = null, + IReadOnlyDictionary>? groupMembers = null) + { + var T = TranslationSource.Instance; + var summaries = PermissionSummaryBuilder.Build(entries); + + var totalEntries = entries.Count; + var uniquePermSets = entries.Select(e => e.PermissionLevels).Distinct().Count(); + var distinctUsers = entries + .SelectMany(e => e.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries)) + .Select(u => u.Trim()) + .Where(u => u.Length > 0) + .Distinct() + .Count(); + + var sb = new StringBuilder(); + + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine($"{T["report.title.permissions_simplified"]}"); + sb.AppendLine(""); + sb.AppendLine(""); + + sb.AppendLine(""); + sb.Append(BrandingHtmlHelper.BuildBrandingHeader(branding)); + sb.AppendLine($"

{T["report.title.permissions_simplified"]}

"); + + // Stats cards + sb.AppendLine("
"); + sb.AppendLine($"
{totalEntries}
{T["report.stat.total_entries"]}
"); + sb.AppendLine($"
{uniquePermSets}
{T["report.stat.unique_permission_sets"]}
"); + sb.AppendLine($"
{distinctUsers}
{T["report.stat.distinct_users_groups"]}
"); + sb.AppendLine("
"); + + // Risk-level summary cards + sb.AppendLine("
"); + foreach (var summary in summaries) + { + var (bg, text, border) = RiskLevelColors(summary.RiskLevel); + sb.AppendLine($"
"); + sb.AppendLine($"
{summary.Count}
"); + sb.AppendLine($"
{HtmlEncode(summary.Label)}
"); + sb.AppendLine($"
{summary.DistinctUsers} user(s)
"); + sb.AppendLine("
"); + } + sb.AppendLine("
"); + + // Filter input + sb.AppendLine("
"); + sb.AppendLine($" "); + sb.AppendLine("
"); + + // Table with simplified columns + sb.AppendLine("
"); + sb.AppendLine("
{T["report.col.object"]}{T["report.col.title"]}{T["report.col.url"]}{T["report.badge.unique"]}{T["report.col.users_groups"]}{T["report.col.permission_level"]}{T["report.col.granted_through"]}
{memberContent}
{HtmlEncode(entry.ObjectType)}
"); + sb.AppendLine(""); + sb.AppendLine($" "); + sb.AppendLine(""); + sb.AppendLine(""); + + int grpMemIdx = 0; + foreach (var entry in entries) + { + var typeCss = ObjectTypeCss(entry.ObjectType); + var uniqueCss = entry.HasUniquePermissions ? "badge unique" : "badge inherited"; + var uniqueLbl = entry.HasUniquePermissions ? T["report.badge.unique"] : T["report.badge.inherited"]; + var (riskBg, riskText, riskBorder) = RiskLevelColors(entry.RiskLevel); + + var logins = entry.UserLogins.Split(';', StringSplitOptions.RemoveEmptyEntries); + var names = entry.Inner.Users.Split(';', StringSplitOptions.RemoveEmptyEntries); + var pillsBuilder = new StringBuilder(); + var memberSubRows = new StringBuilder(); + + for (int i = 0; i < logins.Length; i++) + { + var login = logins[i].Trim(); + var name = i < names.Length ? names[i].Trim() : login; + var isExt = login.Contains("#EXT#", StringComparison.OrdinalIgnoreCase); + + bool isExpandableGroup = entry.Inner.PrincipalType == "SharePointGroup" + && groupMembers != null + && groupMembers.TryGetValue(name, out _); + + if (isExpandableGroup && groupMembers != null && groupMembers.TryGetValue(name, out var resolvedMembers)) + { + var grpId = $"grpmem{grpMemIdx}"; + pillsBuilder.Append($"{HtmlEncode(name)} ▼"); + + string memberContent; + if (resolvedMembers.Count > 0) + { + var memberParts = resolvedMembers.Select(m => $"{HtmlEncode(m.DisplayName)} <{HtmlEncode(m.Login)}>"); + memberContent = string.Join(" • ", memberParts); + } + else + { + memberContent = $"{T["report.text.members_unavailable"]}"; + } + memberSubRows.AppendLine($""); + grpMemIdx++; + } + else + { + var pillCss = isExt ? "user-pill external-user" : "user-pill"; + pillsBuilder.Append($"{HtmlEncode(name)}"); + } + } + + sb.AppendLine(""); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine($" "); + sb.AppendLine(""); + if (memberSubRows.Length > 0) + sb.Append(memberSubRows); + } + + sb.AppendLine(""); + sb.AppendLine("
{T["report.col.object"]}{T["report.col.title"]}{T["report.col.url"]}{T["report.badge.unique"]}{T["report.col.users_groups"]}{T["report.col.permission_level"]}{T["report.col.simplified"]}{T["report.col.risk"]}{T["report.col.granted_through"]}
{memberContent}
{HtmlEncode(entry.ObjectType)}{HtmlEncode(entry.Title)}{T["report.text.link"]}{uniqueLbl}{pillsBuilder}{HtmlEncode(entry.PermissionLevels)}{HtmlEncode(entry.SimplifiedLabels)}{HtmlEncode(entry.RiskLevel.ToString())}{HtmlEncode(entry.GrantedThrough)}
"); + sb.AppendLine("
"); + + sb.AppendLine(""); + sb.AppendLine(""); + sb.AppendLine(""); + + return sb.ToString(); + } + + /// + /// Writes the simplified HTML report to the specified file path. + /// + public async Task WriteAsync(IReadOnlyList entries, string filePath, CancellationToken ct, + ReportBranding? branding = null, + IReadOnlyDictionary>? groupMembers = null) + { + var html = BuildHtml(entries, branding, groupMembers); + await File.WriteAllTextAsync(filePath, html, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), ct); + } + + /// Returns the CSS class for the object-type badge. + private static string ObjectTypeCss(string t) => t switch + { + "Site Collection" => "badge site-coll", + "Site" => "badge site", + "List" => "badge list", + "Folder" => "badge folder", + _ => "badge" + }; + + /// Minimal HTML encoding for text content and attribute values. + private static string HtmlEncode(string value) + { + if (string.IsNullOrEmpty(value)) return string.Empty; + return value + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """) + .Replace("'", "'"); + } +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 } diff --git a/SharepointToolbox/Services/Export/StorageHtmlExportService.cs b/SharepointToolbox/Services/Export/StorageHtmlExportService.cs index 391f652..aae3aa5 100644 --- a/SharepointToolbox/Services/Export/StorageHtmlExportService.cs +++ b/SharepointToolbox/Services/Export/StorageHtmlExportService.cs @@ -180,7 +180,11 @@ public class StorageHtmlExportService var totalFiles = fileTypeMetrics.Sum(m => m.FileCount); sb.AppendLine("
"); +<<<<<<< HEAD sb.AppendLine($"

{T["report.section.storage_by_file_type"]} ({totalFiles:N0} {T["report.text.files_unit"]}, {FormatSize(totalSize)})

"); +======= + sb.AppendLine($"

{T["report.section.storage_by_file_type"]} ({totalFiles:N0} files, {FormatSize(totalSize)})

"); +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 var colors = new[] { "#0078d4", "#2b88d8", "#106ebe", "#005a9e", "#004578", "#00bcf2", "#009e49", "#8cbd18", "#ffb900", "#d83b01" }; diff --git a/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs b/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs index 49caae1..57fd8fe 100644 --- a/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs +++ b/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs @@ -214,7 +214,11 @@ public partial class DuplicatesViewModel : FeatureViewModelBase if (dialog.ShowDialog() != true) return; try { +<<<<<<< HEAD await _csvExportService.WriteAsync(_lastGroups, dialog.FileName, CurrentSplit, CancellationToken.None); +======= + await _csvExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None); +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true }); } catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "CSV export failed."); } diff --git a/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs b/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs index 507d270..3c2ccf2 100644 --- a/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs +++ b/SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs @@ -176,7 +176,10 @@ public partial class StorageViewModel : FeatureViewModelBase ExportCsvCommand = new AsyncRelayCommand(ExportCsvAsync, CanExport); ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); +<<<<<<< HEAD ApplyChartThemeColors(); +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 if (_themeManager is not null) _themeManager.ThemeChanged += (_, _) => UpdateChartSeries(); } @@ -398,6 +401,7 @@ public partial class StorageViewModel : FeatureViewModelBase private SKColor ChartFgColor => (_themeManager?.IsDarkActive ?? false) ? new SKColor(0xE7, 0xEA, 0xF1) : new SKColor(0x1F, 0x24, 0x30); private SKColor ChartSeparatorColor => (_themeManager?.IsDarkActive ?? false) ? new SKColor(0x32, 0x38, 0x49) : new SKColor(0xE3, 0xE6, 0xEC); +<<<<<<< HEAD private SKColor ChartSurfaceColor => (_themeManager?.IsDarkActive ?? false) ? new SKColor(0x1E, 0x22, 0x30) : new SKColor(0xFF, 0xFF, 0xFF); private void ApplyChartThemeColors() @@ -407,6 +411,8 @@ public partial class StorageViewModel : FeatureViewModelBase TooltipTextPaint.Color = ChartFgColor; TooltipBackgroundPaint.Color = ChartSurfaceColor; } +======= +>>>>>>> f4cc81bb71b935c6f6f050288c9e283dcca5cfa8 private void UpdateChartSeries() { diff --git a/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml b/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml index 8e24325..71d21f6 100644 --- a/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml +++ b/SharepointToolbox/Views/Dialogs/FolderBrowserDialog.xaml @@ -16,8 +16,12 @@ +<<<<<<< HEAD