Some checks failed
Release SharePoint Toolbox v2 / release (push) Failing after 14s
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>
175 lines
6.8 KiB
C#
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;
|
|
}
|
|
}
|