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
+55 -23
View File
@@ -93,8 +93,7 @@ public class TemplateService : ITemplateService
{
ctx.Load(list.RootFolder, f => f.ServerRelativeUrl);
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
libInfo.Folders = await EnumerateFoldersRecursiveAsync(
ctx, list.RootFolder, string.Empty, progress, ct);
libInfo.Folders = await EnumerateLibraryFoldersAsync(ctx, list, ct);
}
template.Libraries.Add(libInfo);
@@ -293,39 +292,72 @@ public class TemplateService : ITemplateService
return siteUrl;
}
private async Task<List<TemplateFolderInfo>> EnumerateFoldersRecursiveAsync(
/// <summary>
/// Enumerates every folder in a library via one paginated CAML scan, then
/// reconstructs the hierarchy from the server-relative paths. Replaces the
/// former per-level Folder.Folders lazy loading, which hits the list-view
/// threshold on libraries above 5,000 items.
/// </summary>
private static async Task<List<TemplateFolderInfo>> EnumerateLibraryFoldersAsync(
ClientContext ctx,
Folder parentFolder,
string parentRelativePath,
IProgress<OperationProgress> progress,
List list,
CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var result = new List<TemplateFolderInfo>();
ctx.Load(parentFolder, f => f.Folders.Include(sf => sf.Name, sf => sf.ServerRelativeUrl));
await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct);
var rootUrl = list.RootFolder.ServerRelativeUrl.TrimEnd('/');
foreach (var subFolder in parentFolder.Folders)
// Collect all folders flat: (relativePath, parentRelativePath).
var folders = new List<(string Relative, string Parent)>();
await foreach (var item in SharePointPaginationHelper.GetItemsInFolderAsync(
ctx, list, rootUrl, recursive: true,
viewFields: new[] { "FSObjType", "FileLeafRef", "FileRef", "FileDirRef" },
ct: ct))
{
// Skip system folders
if (subFolder.Name.StartsWith("_") || subFolder.Name == "Forms")
if (item["FSObjType"]?.ToString() != "1") continue; // folders only
var name = item["FileLeafRef"]?.ToString() ?? string.Empty;
var fileRef = (item["FileRef"]?.ToString() ?? string.Empty).TrimEnd('/');
var dirRef = (item["FileDirRef"]?.ToString() ?? string.Empty).TrimEnd('/');
if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(fileRef)) continue;
if (name.StartsWith("_", StringComparison.Ordinal) ||
name.Equals("Forms", StringComparison.OrdinalIgnoreCase))
continue;
var relativePath = string.IsNullOrEmpty(parentRelativePath)
? subFolder.Name
: $"{parentRelativePath}/{subFolder.Name}";
// Paths relative to the library root.
var rel = fileRef.StartsWith(rootUrl, StringComparison.OrdinalIgnoreCase)
? fileRef.Substring(rootUrl.Length).TrimStart('/')
: name;
var parentRel = dirRef.StartsWith(rootUrl, StringComparison.OrdinalIgnoreCase)
? dirRef.Substring(rootUrl.Length).TrimStart('/')
: string.Empty;
var folderInfo = new TemplateFolderInfo
{
Name = subFolder.Name,
RelativePath = relativePath,
Children = await EnumerateFoldersRecursiveAsync(ctx, subFolder, relativePath, progress, ct),
};
result.Add(folderInfo);
folders.Add((rel, parentRel));
}
return result;
// Build tree keyed by relative path.
var nodes = folders.ToDictionary(
f => f.Relative,
f => new TemplateFolderInfo
{
Name = System.IO.Path.GetFileName(f.Relative),
RelativePath = f.Relative,
Children = new List<TemplateFolderInfo>(),
},
StringComparer.OrdinalIgnoreCase);
var roots = new List<TemplateFolderInfo>();
foreach (var (rel, parent) in folders)
{
if (!nodes.TryGetValue(rel, out var node)) continue;
if (!string.IsNullOrEmpty(parent) && nodes.TryGetValue(parent, out var p))
p.Children.Add(node);
else
roots.Add(node);
}
return roots;
}
private static async Task CreateFoldersFromTemplateAsync(