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:
Dev
2026-04-20 11:23:11 +02:00
parent 8f30a60d2a
commit 12dd1de9f2
93 changed files with 8708 additions and 1159 deletions
@@ -2,20 +2,40 @@ using System.Globalization;
using System.IO;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;
using SharepointToolbox.Core.Models;
using SharepointToolbox.Localization;
namespace SharepointToolbox.Services.Export;
/// <summary>
/// Exports the failed subset of a <see cref="BulkOperationSummary{T}"/> run
/// to CSV. CsvHelper is used so the <typeparamref name="T"/> payload's
/// properties become columns automatically, plus one error-message and one
/// timestamp column appended at the end.
/// </summary>
public class BulkResultCsvExportService
{
private static readonly CsvConfiguration CsvConfig = new(CultureInfo.InvariantCulture)
{
// Prevent CSV formula injection: prefix =, +, -, @, tab, CR with single quote
InjectionOptions = InjectionOptions.Escape,
};
/// <summary>
/// Builds a CSV containing only items whose <see cref="BulkItemResult{T}.IsSuccess"/>
/// is <c>false</c>. Columns: every public property of <typeparamref name="T"/>
/// followed by Error and Timestamp (ISO-8601).
/// </summary>
public string BuildFailedItemsCsv<T>(IReadOnlyList<BulkItemResult<T>> failedItems)
{
var TL = TranslationSource.Instance;
using var writer = new StringWriter();
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);
using var csv = new CsvWriter(writer, CsvConfig);
csv.WriteHeader<T>();
csv.WriteField("Error");
csv.WriteField("Timestamp");
csv.WriteField(TL["report.col.error"]);
csv.WriteField(TL["report.col.timestamp"]);
csv.NextRecord();
foreach (var item in failedItems.Where(r => !r.IsSuccess))
@@ -29,12 +49,13 @@ public class BulkResultCsvExportService
return writer.ToString();
}
/// <summary>Writes the failed-items CSV to <paramref name="filePath"/> with UTF-8 BOM.</summary>
public async Task WriteFailedItemsCsvAsync<T>(
IReadOnlyList<BulkItemResult<T>> failedItems,
string filePath,
CancellationToken ct)
{
var content = BuildFailedItemsCsv(failedItems);
await System.IO.File.WriteAllTextAsync(filePath, content, new UTF8Encoding(true), ct);
await ExportFileWriter.WriteCsvAsync(filePath, content, ct);
}
}