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:
@@ -15,6 +15,8 @@ public partial class TransferViewModel : FeatureViewModelBase
|
||||
private readonly IFileTransferService _transferService;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
private readonly BulkResultCsvExportService _exportService;
|
||||
private readonly IOwnershipElevationService? _ownershipService;
|
||||
private readonly SettingsService? _settingsService;
|
||||
private readonly ILogger<FeatureViewModelBase> _logger;
|
||||
private TenantProfile? _currentProfile;
|
||||
private bool _hasLocalSourceSiteOverride;
|
||||
@@ -32,6 +34,17 @@ public partial class TransferViewModel : FeatureViewModelBase
|
||||
// Transfer options
|
||||
[ObservableProperty] private TransferMode _transferMode = TransferMode.Copy;
|
||||
[ObservableProperty] private ConflictPolicy _conflictPolicy = ConflictPolicy.Skip;
|
||||
[ObservableProperty] private bool _includeSourceFolder;
|
||||
[ObservableProperty] private bool _copyFolderContents = true;
|
||||
|
||||
/// <summary>
|
||||
/// Library-relative file paths the user checked in the source picker.
|
||||
/// When non-empty, only these files are transferred — folder recursion is skipped.
|
||||
/// </summary>
|
||||
public List<string> SelectedFilePaths { get; } = new();
|
||||
|
||||
/// <summary>Count of per-file selections, for display in the view.</summary>
|
||||
public int SelectedFileCount => SelectedFilePaths.Count;
|
||||
|
||||
// Results
|
||||
[ObservableProperty] private string _resultSummary = string.Empty;
|
||||
@@ -51,12 +64,16 @@ public partial class TransferViewModel : FeatureViewModelBase
|
||||
IFileTransferService transferService,
|
||||
ISessionManager sessionManager,
|
||||
BulkResultCsvExportService exportService,
|
||||
ILogger<FeatureViewModelBase> logger)
|
||||
ILogger<FeatureViewModelBase> logger,
|
||||
IOwnershipElevationService? ownershipService = null,
|
||||
SettingsService? settingsService = null)
|
||||
: base(logger)
|
||||
{
|
||||
_transferService = transferService;
|
||||
_sessionManager = sessionManager;
|
||||
_exportService = exportService;
|
||||
_ownershipService = ownershipService;
|
||||
_settingsService = settingsService;
|
||||
_logger = logger;
|
||||
|
||||
ExportFailedCommand = new AsyncRelayCommand(ExportFailedAsync, () => HasFailures);
|
||||
@@ -108,6 +125,9 @@ public partial class TransferViewModel : FeatureViewModelBase
|
||||
DestinationFolderPath = DestFolderPath,
|
||||
Mode = TransferMode,
|
||||
ConflictPolicy = ConflictPolicy,
|
||||
SelectedFilePaths = SelectedFilePaths.ToList(),
|
||||
IncludeSourceFolder = IncludeSourceFolder,
|
||||
CopyFolderContents = CopyFolderContents,
|
||||
};
|
||||
|
||||
// Build per-site profiles so SessionManager can resolve contexts
|
||||
@@ -127,7 +147,33 @@ public partial class TransferViewModel : FeatureViewModelBase
|
||||
var srcCtx = await _sessionManager.GetOrCreateContextAsync(srcProfile, ct);
|
||||
var dstCtx = await _sessionManager.GetOrCreateContextAsync(dstProfile, ct);
|
||||
|
||||
_lastResult = await _transferService.TransferAsync(srcCtx, dstCtx, job, progress, ct);
|
||||
var autoOwnership = await IsAutoTakeOwnershipEnabled();
|
||||
|
||||
try
|
||||
{
|
||||
_lastResult = await _transferService.TransferAsync(srcCtx, dstCtx, job, progress, ct);
|
||||
}
|
||||
catch (Microsoft.SharePoint.Client.ServerUnauthorizedAccessException ex)
|
||||
when (_ownershipService != null && autoOwnership)
|
||||
{
|
||||
_logger.LogWarning(ex, "Transfer hit access denied — auto-elevating on source and destination.");
|
||||
var adminUrl = DeriveAdminUrl(_currentProfile.TenantUrl ?? SourceSiteUrl);
|
||||
var adminProfile = new TenantProfile
|
||||
{
|
||||
Name = _currentProfile.Name,
|
||||
TenantUrl = adminUrl,
|
||||
ClientId = _currentProfile.ClientId,
|
||||
};
|
||||
var adminCtx = await _sessionManager.GetOrCreateContextAsync(adminProfile, ct);
|
||||
|
||||
await _ownershipService.ElevateAsync(adminCtx, SourceSiteUrl, string.Empty, ct);
|
||||
await _ownershipService.ElevateAsync(adminCtx, DestSiteUrl, string.Empty, ct);
|
||||
|
||||
// Retry with fresh contexts so the new admin membership is honoured.
|
||||
srcCtx = await _sessionManager.GetOrCreateContextAsync(srcProfile, ct);
|
||||
dstCtx = await _sessionManager.GetOrCreateContextAsync(dstProfile, ct);
|
||||
_lastResult = await _transferService.TransferAsync(srcCtx, dstCtx, job, progress, ct);
|
||||
}
|
||||
|
||||
// Update UI on dispatcher
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
@@ -182,6 +228,34 @@ public partial class TransferViewModel : FeatureViewModelBase
|
||||
DestFolderPath = string.Empty;
|
||||
ResultSummary = string.Empty;
|
||||
HasFailures = false;
|
||||
SelectedFilePaths.Clear();
|
||||
OnPropertyChanged(nameof(SelectedFileCount));
|
||||
_lastResult = null;
|
||||
}
|
||||
|
||||
/// <summary>Replaces the current per-file selection and notifies the view.</summary>
|
||||
public void SetSelectedFiles(IEnumerable<string> libraryRelativePaths)
|
||||
{
|
||||
SelectedFilePaths.Clear();
|
||||
SelectedFilePaths.AddRange(libraryRelativePaths);
|
||||
OnPropertyChanged(nameof(SelectedFileCount));
|
||||
}
|
||||
|
||||
private async Task<bool> IsAutoTakeOwnershipEnabled()
|
||||
{
|
||||
if (_settingsService == null) return false;
|
||||
var settings = await _settingsService.GetSettingsAsync();
|
||||
return settings.AutoTakeOwnership;
|
||||
}
|
||||
|
||||
internal static string DeriveAdminUrl(string tenantUrl)
|
||||
{
|
||||
var uri = new Uri(tenantUrl.TrimEnd('/'));
|
||||
var host = uri.Host;
|
||||
if (host.Contains("-admin.sharepoint.com", StringComparison.OrdinalIgnoreCase))
|
||||
return tenantUrl;
|
||||
var adminHost = host.Replace(".sharepoint.com", "-admin.sharepoint.com",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
return $"{uri.Scheme}://{adminHost}";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user