From 0984a36bc78403eecbf6c3746b55d776922d4968 Mon Sep 17 00:00:00 2001 From: Dev Date: Thu, 2 Apr 2026 15:44:26 +0200 Subject: [PATCH] feat(03-08): create DuplicatesViewModel, DuplicatesView XAML and code-behind - DuplicatesViewModel: ModeFiles/Folders, criteria checkboxes, group flattening to DuplicateRow - Uses TenantProfile site URL override pattern (ctx.Url is read-only) - ExportHtmlCommand exports DuplicateGroup list via DuplicatesHtmlExportService - DuplicatesView.xaml: type selector, criteria panel + flattened DataGrid - DuplicatesView.xaml.cs: DI constructor with DataContext wiring --- .../ViewModels/Tabs/DuplicatesViewModel.cs | 169 ++++++++++++++++++ .../Views/Tabs/DuplicatesView.xaml | 77 ++++++++ .../Views/Tabs/DuplicatesView.xaml.cs | 12 ++ 3 files changed, 258 insertions(+) create mode 100644 SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs create mode 100644 SharepointToolbox/Views/Tabs/DuplicatesView.xaml create mode 100644 SharepointToolbox/Views/Tabs/DuplicatesView.xaml.cs diff --git a/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs b/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs new file mode 100644 index 0000000..4d40b15 --- /dev/null +++ b/SharepointToolbox/ViewModels/Tabs/DuplicatesViewModel.cs @@ -0,0 +1,169 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Microsoft.Extensions.Logging; +using Microsoft.Win32; +using SharepointToolbox.Core.Models; +using SharepointToolbox.Services; +using SharepointToolbox.Services.Export; + +namespace SharepointToolbox.ViewModels.Tabs; + +/// Flat display row wrapping a DuplicateItem with its group name. +public class DuplicateRow +{ + public string GroupName { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string Library { get; set; } = string.Empty; + public long? SizeBytes { get; set; } + public DateTime? Created { get; set; } + public DateTime? Modified { get; set; } + public int? FolderCount { get; set; } + public int? FileCount { get; set; } + public int GroupSize { get; set; } +} + +public partial class DuplicatesViewModel : FeatureViewModelBase +{ + private readonly IDuplicatesService _duplicatesService; + private readonly ISessionManager _sessionManager; + private readonly DuplicatesHtmlExportService _htmlExportService; + private readonly ILogger _logger; + private TenantProfile? _currentProfile; + private IReadOnlyList _lastGroups = Array.Empty(); + + [ObservableProperty] private string _siteUrl = string.Empty; + [ObservableProperty] private bool _modeFiles = true; + [ObservableProperty] private bool _modeFolders; + [ObservableProperty] private bool _matchSize = true; + [ObservableProperty] private bool _matchCreated; + [ObservableProperty] private bool _matchModified; + [ObservableProperty] private bool _matchSubfolders; + [ObservableProperty] private bool _matchFileCount; + [ObservableProperty] private bool _includeSubsites; + [ObservableProperty] private string _library = string.Empty; + + private ObservableCollection _results = new(); + public ObservableCollection Results + { + get => _results; + private set + { + _results = value; + OnPropertyChanged(); + ExportHtmlCommand.NotifyCanExecuteChanged(); + } + } + + public IAsyncRelayCommand ExportHtmlCommand { get; } + public TenantProfile? CurrentProfile => _currentProfile; + + public DuplicatesViewModel( + IDuplicatesService duplicatesService, + ISessionManager sessionManager, + DuplicatesHtmlExportService htmlExportService, + ILogger logger) + : base(logger) + { + _duplicatesService = duplicatesService; + _sessionManager = sessionManager; + _htmlExportService = htmlExportService; + _logger = logger; + + ExportHtmlCommand = new AsyncRelayCommand(ExportHtmlAsync, CanExport); + } + + protected override async Task RunOperationAsync(CancellationToken ct, IProgress progress) + { + if (_currentProfile == null) + { + StatusMessage = "No tenant selected. Please connect to a tenant first."; + return; + } + if (string.IsNullOrWhiteSpace(SiteUrl)) + { + StatusMessage = "Please enter a site URL."; + return; + } + + var siteProfile = new TenantProfile + { + TenantUrl = SiteUrl.TrimEnd('/'), + ClientId = _currentProfile.ClientId, + Name = _currentProfile.Name + }; + var ctx = await _sessionManager.GetOrCreateContextAsync(siteProfile, ct); + + var opts = new DuplicateScanOptions( + Mode: ModeFiles ? "Files" : "Folders", + MatchSize: MatchSize, + MatchCreated: MatchCreated, + MatchModified: MatchModified, + MatchSubfolderCount: MatchSubfolders, + MatchFileCount: MatchFileCount, + IncludeSubsites: IncludeSubsites, + Library: string.IsNullOrWhiteSpace(Library) ? null : Library + ); + + var groups = await _duplicatesService.ScanDuplicatesAsync(ctx, opts, progress, ct); + _lastGroups = groups; + + // Flatten groups to display rows + var rows = groups + .SelectMany(g => g.Items.Select(item => new DuplicateRow + { + GroupName = g.Name, + Name = item.Name, + Path = item.Path, + Library = item.Library, + SizeBytes = item.SizeBytes, + Created = item.Created, + Modified = item.Modified, + FolderCount = item.FolderCount, + FileCount = item.FileCount, + GroupSize = g.Items.Count + })) + .ToList(); + + if (Application.Current?.Dispatcher is { } dispatcher) + await dispatcher.InvokeAsync(() => Results = new ObservableCollection(rows)); + else + Results = new ObservableCollection(rows); + } + + protected override void OnTenantSwitched(Core.Models.TenantProfile profile) + { + _currentProfile = profile; + Results = new ObservableCollection(); + _lastGroups = Array.Empty(); + SiteUrl = string.Empty; + OnPropertyChanged(nameof(CurrentProfile)); + ExportHtmlCommand.NotifyCanExecuteChanged(); + } + + internal void SetCurrentProfile(TenantProfile profile) => _currentProfile = profile; + + private bool CanExport() => _lastGroups.Count > 0; + + private async Task ExportHtmlAsync() + { + if (_lastGroups.Count == 0) return; + var dialog = new SaveFileDialog + { + Title = "Export duplicates report to HTML", + Filter = "HTML files (*.html)|*.html|All files (*.*)|*.*", + DefaultExt = "html", + FileName = "duplicates_report" + }; + if (dialog.ShowDialog() != true) return; + try + { + await _htmlExportService.WriteAsync(_lastGroups, dialog.FileName, CancellationToken.None); + Process.Start(new ProcessStartInfo(dialog.FileName) { UseShellExecute = true }); + } + catch (Exception ex) { StatusMessage = $"Export failed: {ex.Message}"; _logger.LogError(ex, "HTML export failed."); } + } +} diff --git a/SharepointToolbox/Views/Tabs/DuplicatesView.xaml b/SharepointToolbox/Views/Tabs/DuplicatesView.xaml new file mode 100644 index 0000000..3f78c0c --- /dev/null +++ b/SharepointToolbox/Views/Tabs/DuplicatesView.xaml @@ -0,0 +1,77 @@ + + + + + +