Files
Sharepoint-Toolbox/SharepointToolbox/ViewModels/Tabs/FolderStructureViewModel.cs
Dev fd442f3b4c
Some checks failed
Release SharePoint Toolbox v2 / release (push) Failing after 14s
chore: archive v1.1 Enhanced Reports milestone
v1.1 shipped with 4 phases (25 plans), 10/10 requirements complete:
- Global site selection (toolbar picker, all tabs consume)
- User access audit (Graph people-picker, direct/group/inherited)
- Simplified permissions (plain-language labels, risk levels, detail toggle)
- Storage visualization (LiveCharts2 pie/donut + bar charts)

Post-phase polish: centralized site selection (removed per-tab pickers),
claims prefix stripping, StorageMetrics backfill, chart tooltip fix,
summary stats in app + HTML exports.

205 tests passing, 10,484 LOC.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 10:21:02 +02:00

175 lines
6.8 KiB
C#

using System.Collections.ObjectModel;
using System.IO;
using System.Reflection;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using Serilog;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
using SharepointToolbox.Services;
using SharepointToolbox.Services.Export;
namespace SharepointToolbox.ViewModels.Tabs;
public partial class FolderStructureViewModel : FeatureViewModelBase
{
private readonly IFolderStructureService _folderService;
private readonly ICsvValidationService _csvService;
private readonly ISessionManager _sessionManager;
private readonly BulkResultCsvExportService _exportService;
private readonly ILogger<FeatureViewModelBase> _logger;
private TenantProfile? _currentProfile;
private List<FolderStructureRow>? _validRows;
private BulkOperationSummary<string>? _lastResult;
[ObservableProperty] private string _libraryTitle = string.Empty;
[ObservableProperty] private string _previewSummary = string.Empty;
[ObservableProperty] private string _resultSummary = string.Empty;
[ObservableProperty] private bool _hasFailures;
[ObservableProperty] private bool _hasPreview;
private ObservableCollection<CsvValidationRow<FolderStructureRow>> _previewRows = new();
public ObservableCollection<CsvValidationRow<FolderStructureRow>> PreviewRows
{
get => _previewRows;
private set { _previewRows = value; OnPropertyChanged(); }
}
public IRelayCommand ImportCsvCommand { get; }
public IRelayCommand LoadExampleCommand { get; }
public IAsyncRelayCommand ExportFailedCommand { get; }
public Func<string, bool>? ShowConfirmDialog { get; set; }
public TenantProfile? CurrentProfile => _currentProfile;
public FolderStructureViewModel(
IFolderStructureService folderService,
ICsvValidationService csvService,
ISessionManager sessionManager,
BulkResultCsvExportService exportService,
ILogger<FeatureViewModelBase> logger)
: base(logger)
{
_folderService = folderService;
_csvService = csvService;
_sessionManager = sessionManager;
_exportService = exportService;
_logger = logger;
ImportCsvCommand = new RelayCommand(ImportCsv);
LoadExampleCommand = new RelayCommand(LoadExample);
ExportFailedCommand = new AsyncRelayCommand(ExportFailedAsync, () => HasFailures);
}
private void ImportCsv()
{
var dlg = new OpenFileDialog
{
Title = TranslationSource.Instance["bulk.csvimport.title"],
Filter = TranslationSource.Instance["bulk.csvimport.filter"],
};
if (dlg.ShowDialog() != true) return;
using var stream = File.OpenRead(dlg.FileName);
LoadAndPreview(stream);
}
private void LoadExample()
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith("folder_structure.csv", StringComparison.OrdinalIgnoreCase));
if (resourceName == null) return;
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream != null) LoadAndPreview(stream);
}
private void LoadAndPreview(Stream stream)
{
var rows = _csvService.ParseAndValidateFolders(stream);
PreviewRows = new ObservableCollection<CsvValidationRow<FolderStructureRow>>(rows);
_validRows = rows.Where(r => r.IsValid && r.Record != null).Select(r => r.Record!).ToList();
var uniquePaths = FolderStructureService.BuildUniquePaths(_validRows);
PreviewSummary = string.Format(TranslationSource.Instance["folderstruct.preview"], uniquePaths.Count);
HasPreview = true;
ResultSummary = string.Empty;
HasFailures = false;
}
protected override async Task RunOperationAsync(CancellationToken ct, IProgress<OperationProgress> progress)
{
if (_currentProfile == null) throw new InvalidOperationException("No tenant connected.");
if (_validRows == null || _validRows.Count == 0)
throw new InvalidOperationException("No valid rows. Import a CSV first.");
if (string.IsNullOrWhiteSpace(LibraryTitle))
throw new InvalidOperationException("Library title is required.");
var urls = GlobalSites.Select(s => s.Url).Where(u => !string.IsNullOrWhiteSpace(u)).ToList();
if (urls.Count == 0)
throw new InvalidOperationException("Select at least one site from the toolbar.");
var uniquePaths = FolderStructureService.BuildUniquePaths(_validRows);
var message = string.Format(TranslationSource.Instance["bulk.confirm.message"],
$"{uniquePaths.Count} folders will be created in {LibraryTitle} on {urls.Count} site(s)");
if (ShowConfirmDialog != null && !ShowConfirmDialog(message))
return;
var allResults = new List<BulkItemResult<string>>();
foreach (var url in urls)
{
var profile = new TenantProfile
{
Name = _currentProfile.Name,
TenantUrl = url,
ClientId = _currentProfile.ClientId,
};
var ctx = await _sessionManager.GetOrCreateContextAsync(profile, ct);
var result = await _folderService.CreateFoldersAsync(ctx, LibraryTitle, _validRows, progress, ct);
allResults.AddRange(result.Results);
}
_lastResult = new BulkOperationSummary<string>(allResults);
await Application.Current.Dispatcher.InvokeAsync(() =>
{
HasFailures = _lastResult.HasFailures;
ExportFailedCommand.NotifyCanExecuteChanged();
ResultSummary = _lastResult.HasFailures
? string.Format(TranslationSource.Instance["bulk.result.success"],
_lastResult.SuccessCount, _lastResult.FailedCount)
: string.Format(TranslationSource.Instance["bulk.result.allsuccess"],
_lastResult.TotalCount);
});
}
private async Task ExportFailedAsync()
{
if (_lastResult == null || !_lastResult.HasFailures) return;
var dlg = new SaveFileDialog { Filter = "CSV Files (*.csv)|*.csv", FileName = "failed_folders.csv" };
if (dlg.ShowDialog() == true)
{
await _exportService.WriteFailedItemsCsvAsync(_lastResult.FailedItems.ToList(), dlg.FileName, CancellationToken.None);
}
}
protected override void OnTenantSwitched(TenantProfile profile)
{
_currentProfile = profile;
LibraryTitle = string.Empty;
PreviewRows = new();
_validRows = null;
PreviewSummary = string.Empty;
ResultSummary = string.Empty;
HasFailures = false;
HasPreview = false;
}
}