diff --git a/SharepointToolbox.Tests/Services/BulkOperationRunnerTests.cs b/SharepointToolbox.Tests/Services/BulkOperationRunnerTests.cs new file mode 100644 index 0000000..e510ecd --- /dev/null +++ b/SharepointToolbox.Tests/Services/BulkOperationRunnerTests.cs @@ -0,0 +1,105 @@ +using SharepointToolbox.Core.Models; +using SharepointToolbox.Services; + +namespace SharepointToolbox.Tests.Services; + +public class BulkOperationRunnerTests +{ + [Fact] + public async Task RunAsync_AllSucceed_ReturnsAllSuccess() + { + var items = new List { "a", "b", "c" }; + var progress = new Progress(); + + var summary = await BulkOperationRunner.RunAsync( + items, + (item, idx, ct) => Task.CompletedTask, + progress, + CancellationToken.None); + + Assert.Equal(3, summary.TotalCount); + Assert.Equal(3, summary.SuccessCount); + Assert.Equal(0, summary.FailedCount); + Assert.False(summary.HasFailures); + } + + [Fact] + public async Task RunAsync_SomeItemsFail_ContinuesAndReportsPerItem() + { + var items = new List { "ok1", "fail", "ok2" }; + var progress = new Progress(); + + var summary = await BulkOperationRunner.RunAsync( + items, + (item, idx, ct) => + { + if (item == "fail") throw new InvalidOperationException("Test error"); + return Task.CompletedTask; + }, + progress, + CancellationToken.None); + + Assert.Equal(3, summary.TotalCount); + Assert.Equal(2, summary.SuccessCount); + Assert.Equal(1, summary.FailedCount); + Assert.True(summary.HasFailures); + Assert.Contains(summary.FailedItems, r => r.ErrorMessage == "Test error"); + } + + [Fact] + public async Task RunAsync_Cancelled_ThrowsOperationCanceled() + { + var items = new List { "a", "b", "c" }; + var cts = new CancellationTokenSource(); + cts.Cancel(); + var progress = new Progress(); + + await Assert.ThrowsAsync(() => + BulkOperationRunner.RunAsync( + items, + (item, idx, ct) => Task.CompletedTask, + progress, + cts.Token)); + } + + [Fact] + public async Task RunAsync_CancelledMidOperation_StopsProcessing() + { + var items = new List { "a", "b", "c", "d" }; + var cts = new CancellationTokenSource(); + var processedCount = 0; + var progress = new Progress(); + + await Assert.ThrowsAsync(() => + BulkOperationRunner.RunAsync( + items, + async (item, idx, ct) => + { + Interlocked.Increment(ref processedCount); + if (idx == 1) cts.Cancel(); // cancel after second item + await Task.CompletedTask; + }, + progress, + cts.Token)); + + Assert.True(processedCount <= 3); // should not process all 4 + } + + [Fact] + public async Task RunAsync_ReportsProgress() + { + var items = new List { "a", "b" }; + var progressReports = new List(); + var progress = new Progress(p => progressReports.Add(p)); + + await BulkOperationRunner.RunAsync( + items, + (item, idx, ct) => Task.CompletedTask, + progress, + CancellationToken.None); + + // Progress is async, give it a moment to flush + await Task.Delay(100); + Assert.True(progressReports.Count >= 2); + } +} diff --git a/SharepointToolbox.Tests/Services/BulkResultCsvExportServiceTests.cs b/SharepointToolbox.Tests/Services/BulkResultCsvExportServiceTests.cs new file mode 100644 index 0000000..233953e --- /dev/null +++ b/SharepointToolbox.Tests/Services/BulkResultCsvExportServiceTests.cs @@ -0,0 +1,45 @@ +using SharepointToolbox.Core.Models; +using SharepointToolbox.Services.Export; + +namespace SharepointToolbox.Tests.Services; + +public class BulkResultCsvExportServiceTests +{ + [Fact] + public void BuildFailedItemsCsv_WithFailedItems_IncludesErrorColumn() + { + var service = new BulkResultCsvExportService(); + var items = new List> + { + BulkItemResult.Failed( + new BulkMemberRow { Email = "bad@test.com", GroupName = "G1", GroupUrl = "http://test", Role = "Member" }, + "User not found"), + }; + + var csv = service.BuildFailedItemsCsv(items); + + Assert.Contains("Error", csv); + Assert.Contains("Timestamp", csv); + Assert.Contains("bad@test.com", csv); + Assert.Contains("User not found", csv); + } + + [Fact] + public void BuildFailedItemsCsv_SuccessItems_Excluded() + { + var service = new BulkResultCsvExportService(); + var items = new List> + { + BulkItemResult.Success( + new BulkMemberRow { Email = "ok@test.com", GroupName = "G1", GroupUrl = "http://test", Role = "Member" }), + BulkItemResult.Failed( + new BulkMemberRow { Email = "bad@test.com", GroupName = "G1", GroupUrl = "http://test", Role = "Member" }, + "Error"), + }; + + var csv = service.BuildFailedItemsCsv(items); + + Assert.DoesNotContain("ok@test.com", csv); + Assert.Contains("bad@test.com", csv); + } +} diff --git a/SharepointToolbox.Tests/Services/CsvValidationServiceTests.cs b/SharepointToolbox.Tests/Services/CsvValidationServiceTests.cs new file mode 100644 index 0000000..058c095 --- /dev/null +++ b/SharepointToolbox.Tests/Services/CsvValidationServiceTests.cs @@ -0,0 +1,36 @@ +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Tests.Services; + +public class CsvValidationServiceTests +{ + [Fact(Skip = "Implemented in Plan 04-02")] + public void ParseAndValidateMembers_ValidCsv_ReturnsValidRows() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void ParseAndValidateMembers_InvalidEmail_ReturnsErrors() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void ParseAndValidateSites_TeamWithoutOwner_ReturnsError() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void ParseAndValidateFolders_ValidCsv_ReturnsValidRows() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void ParseAndValidate_BomDetection_WorksWithAndWithoutBom() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void ParseAndValidate_SemicolonDelimiter_DetectedAutomatically() + { + } +} diff --git a/SharepointToolbox.Tests/Services/TemplateRepositoryTests.cs b/SharepointToolbox.Tests/Services/TemplateRepositoryTests.cs new file mode 100644 index 0000000..3150383 --- /dev/null +++ b/SharepointToolbox.Tests/Services/TemplateRepositoryTests.cs @@ -0,0 +1,26 @@ +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Tests.Services; + +public class TemplateRepositoryTests +{ + [Fact(Skip = "Implemented in Plan 04-02")] + public void SaveAndLoad_RoundTrips_Correctly() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void GetAll_ReturnsAllSavedTemplates() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void Delete_RemovesTemplate() + { + } + + [Fact(Skip = "Implemented in Plan 04-02")] + public void Rename_UpdatesTemplateName() + { + } +} diff --git a/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj b/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj index 069a016..500ab71 100644 --- a/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj +++ b/SharepointToolbox.Tests/SharepointToolbox.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/SharepointToolbox/Core/Models/BulkMemberRow.cs b/SharepointToolbox/Core/Models/BulkMemberRow.cs new file mode 100644 index 0000000..6956015 --- /dev/null +++ b/SharepointToolbox/Core/Models/BulkMemberRow.cs @@ -0,0 +1,18 @@ +using CsvHelper.Configuration.Attributes; + +namespace SharepointToolbox.Core.Models; + +public class BulkMemberRow +{ + [Name("GroupName")] + public string GroupName { get; set; } = string.Empty; + + [Name("GroupUrl")] + public string GroupUrl { get; set; } = string.Empty; + + [Name("Email")] + public string Email { get; set; } = string.Empty; + + [Name("Role")] + public string Role { get; set; } = string.Empty; // "Member" or "Owner" +} diff --git a/SharepointToolbox/Core/Models/BulkOperationResult.cs b/SharepointToolbox/Core/Models/BulkOperationResult.cs new file mode 100644 index 0000000..e8b430f --- /dev/null +++ b/SharepointToolbox/Core/Models/BulkOperationResult.cs @@ -0,0 +1,35 @@ +namespace SharepointToolbox.Core.Models; + +public class BulkItemResult +{ + public T Item { get; } + public bool IsSuccess { get; } + public string? ErrorMessage { get; } + public DateTime Timestamp { get; } + + private BulkItemResult(T item, bool success, string? error) + { + Item = item; + IsSuccess = success; + ErrorMessage = error; + Timestamp = DateTime.UtcNow; + } + + public static BulkItemResult Success(T item) => new(item, true, null); + public static BulkItemResult Failed(T item, string error) => new(item, false, error); +} + +public class BulkOperationSummary +{ + public IReadOnlyList> Results { get; } + public int TotalCount => Results.Count; + public int SuccessCount => Results.Count(r => r.IsSuccess); + public int FailedCount => Results.Count(r => !r.IsSuccess); + public bool HasFailures => FailedCount > 0; + public IReadOnlyList> FailedItems => Results.Where(r => !r.IsSuccess).ToList(); + + public BulkOperationSummary(IReadOnlyList> results) + { + Results = results; + } +} diff --git a/SharepointToolbox/Core/Models/BulkSiteRow.cs b/SharepointToolbox/Core/Models/BulkSiteRow.cs new file mode 100644 index 0000000..4110f0f --- /dev/null +++ b/SharepointToolbox/Core/Models/BulkSiteRow.cs @@ -0,0 +1,24 @@ +using CsvHelper.Configuration.Attributes; + +namespace SharepointToolbox.Core.Models; + +public class BulkSiteRow +{ + [Name("Name")] + public string Name { get; set; } = string.Empty; + + [Name("Alias")] + public string Alias { get; set; } = string.Empty; + + [Name("Type")] + public string Type { get; set; } = string.Empty; // "Team" or "Communication" + + [Name("Template")] + public string Template { get; set; } = string.Empty; + + [Name("Owners")] + public string Owners { get; set; } = string.Empty; // comma-separated emails + + [Name("Members")] + public string Members { get; set; } = string.Empty; // comma-separated emails +} diff --git a/SharepointToolbox/Core/Models/ConflictPolicy.cs b/SharepointToolbox/Core/Models/ConflictPolicy.cs new file mode 100644 index 0000000..f2ccf4d --- /dev/null +++ b/SharepointToolbox/Core/Models/ConflictPolicy.cs @@ -0,0 +1,8 @@ +namespace SharepointToolbox.Core.Models; + +public enum ConflictPolicy +{ + Skip, + Overwrite, + Rename +} diff --git a/SharepointToolbox/Core/Models/CsvValidationRow.cs b/SharepointToolbox/Core/Models/CsvValidationRow.cs new file mode 100644 index 0000000..42f03b9 --- /dev/null +++ b/SharepointToolbox/Core/Models/CsvValidationRow.cs @@ -0,0 +1,25 @@ +namespace SharepointToolbox.Core.Models; + +public class CsvValidationRow +{ + public T? Record { get; } + public bool IsValid => Errors.Count == 0; + public List Errors { get; } + public string? RawRecord { get; } + + public CsvValidationRow(T record, List errors) + { + Record = record; + Errors = errors; + } + + private CsvValidationRow(string rawRecord, string parseError) + { + Record = default; + RawRecord = rawRecord; + Errors = new List { parseError }; + } + + public static CsvValidationRow ParseError(string? rawRecord, string error) + => new(rawRecord ?? string.Empty, error); +} diff --git a/SharepointToolbox/Core/Models/FolderStructureRow.cs b/SharepointToolbox/Core/Models/FolderStructureRow.cs new file mode 100644 index 0000000..147dc11 --- /dev/null +++ b/SharepointToolbox/Core/Models/FolderStructureRow.cs @@ -0,0 +1,28 @@ +using CsvHelper.Configuration.Attributes; + +namespace SharepointToolbox.Core.Models; + +public class FolderStructureRow +{ + [Name("Level1")] + public string Level1 { get; set; } = string.Empty; + + [Name("Level2")] + public string Level2 { get; set; } = string.Empty; + + [Name("Level3")] + public string Level3 { get; set; } = string.Empty; + + [Name("Level4")] + public string Level4 { get; set; } = string.Empty; + + /// + /// Builds the folder path from non-empty level values (e.g. "Admin/HR/Contracts"). + /// + public string BuildPath() + { + var parts = new[] { Level1, Level2, Level3, Level4 } + .Where(s => !string.IsNullOrWhiteSpace(s)); + return string.Join("/", parts); + } +} diff --git a/SharepointToolbox/Core/Models/SiteTemplate.cs b/SharepointToolbox/Core/Models/SiteTemplate.cs new file mode 100644 index 0000000..9e3e16f --- /dev/null +++ b/SharepointToolbox/Core/Models/SiteTemplate.cs @@ -0,0 +1,27 @@ +namespace SharepointToolbox.Core.Models; + +public class SiteTemplate +{ + public string Id { get; set; } = Guid.NewGuid().ToString(); + public string Name { get; set; } = string.Empty; + public string SourceUrl { get; set; } = string.Empty; + public DateTime CapturedAt { get; set; } + public string SiteType { get; set; } = string.Empty; // "Team" or "Communication" + public SiteTemplateOptions Options { get; set; } = new(); + public TemplateSettings? Settings { get; set; } + public TemplateLogo? Logo { get; set; } + public List Libraries { get; set; } = new(); + public List PermissionGroups { get; set; } = new(); +} + +public class TemplateSettings +{ + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public int Language { get; set; } +} + +public class TemplateLogo +{ + public string LogoUrl { get; set; } = string.Empty; +} diff --git a/SharepointToolbox/Core/Models/SiteTemplateOptions.cs b/SharepointToolbox/Core/Models/SiteTemplateOptions.cs new file mode 100644 index 0000000..dbc1eca --- /dev/null +++ b/SharepointToolbox/Core/Models/SiteTemplateOptions.cs @@ -0,0 +1,10 @@ +namespace SharepointToolbox.Core.Models; + +public class SiteTemplateOptions +{ + public bool CaptureLibraries { get; set; } = true; + public bool CaptureFolders { get; set; } = true; + public bool CapturePermissionGroups { get; set; } = true; + public bool CaptureLogo { get; set; } = true; + public bool CaptureSettings { get; set; } = true; +} diff --git a/SharepointToolbox/Core/Models/TemplateFolderInfo.cs b/SharepointToolbox/Core/Models/TemplateFolderInfo.cs new file mode 100644 index 0000000..235be87 --- /dev/null +++ b/SharepointToolbox/Core/Models/TemplateFolderInfo.cs @@ -0,0 +1,8 @@ +namespace SharepointToolbox.Core.Models; + +public class TemplateFolderInfo +{ + public string Name { get; set; } = string.Empty; + public string RelativePath { get; set; } = string.Empty; + public List Children { get; set; } = new(); +} diff --git a/SharepointToolbox/Core/Models/TemplateLibraryInfo.cs b/SharepointToolbox/Core/Models/TemplateLibraryInfo.cs new file mode 100644 index 0000000..503e245 --- /dev/null +++ b/SharepointToolbox/Core/Models/TemplateLibraryInfo.cs @@ -0,0 +1,9 @@ +namespace SharepointToolbox.Core.Models; + +public class TemplateLibraryInfo +{ + public string Name { get; set; } = string.Empty; + public string BaseType { get; set; } = string.Empty; // "DocumentLibrary", "GenericList" + public int BaseTemplate { get; set; } + public List Folders { get; set; } = new(); +} diff --git a/SharepointToolbox/Core/Models/TemplatePermissionGroup.cs b/SharepointToolbox/Core/Models/TemplatePermissionGroup.cs new file mode 100644 index 0000000..94fca80 --- /dev/null +++ b/SharepointToolbox/Core/Models/TemplatePermissionGroup.cs @@ -0,0 +1,8 @@ +namespace SharepointToolbox.Core.Models; + +public class TemplatePermissionGroup +{ + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public List RoleDefinitions { get; set; } = new(); // e.g. "Full Control", "Contribute" +} diff --git a/SharepointToolbox/Core/Models/TransferJob.cs b/SharepointToolbox/Core/Models/TransferJob.cs new file mode 100644 index 0000000..be9d5b8 --- /dev/null +++ b/SharepointToolbox/Core/Models/TransferJob.cs @@ -0,0 +1,13 @@ +namespace SharepointToolbox.Core.Models; + +public class TransferJob +{ + public string SourceSiteUrl { get; set; } = string.Empty; + public string SourceLibrary { get; set; } = string.Empty; + public string SourceFolderPath { get; set; } = string.Empty; // relative within library + public string DestinationSiteUrl { get; set; } = string.Empty; + public string DestinationLibrary { get; set; } = string.Empty; + public string DestinationFolderPath { get; set; } = string.Empty; + public TransferMode Mode { get; set; } = TransferMode.Copy; + public ConflictPolicy ConflictPolicy { get; set; } = ConflictPolicy.Skip; +} diff --git a/SharepointToolbox/Core/Models/TransferMode.cs b/SharepointToolbox/Core/Models/TransferMode.cs new file mode 100644 index 0000000..56bfaa4 --- /dev/null +++ b/SharepointToolbox/Core/Models/TransferMode.cs @@ -0,0 +1,7 @@ +namespace SharepointToolbox.Core.Models; + +public enum TransferMode +{ + Copy, + Move +} diff --git a/SharepointToolbox/Services/BulkOperationRunner.cs b/SharepointToolbox/Services/BulkOperationRunner.cs new file mode 100644 index 0000000..3155a0e --- /dev/null +++ b/SharepointToolbox/Services/BulkOperationRunner.cs @@ -0,0 +1,36 @@ +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public static class BulkOperationRunner +{ + /// + /// Runs a bulk operation with continue-on-error semantics, per-item result tracking, + /// and cancellation support. OperationCanceledException propagates immediately. + /// + public static async Task> RunAsync( + IReadOnlyList items, + Func processItem, + IProgress progress, + CancellationToken ct) + { + var results = new List>(); + for (int i = 0; i < items.Count; i++) + { + ct.ThrowIfCancellationRequested(); + progress.Report(new OperationProgress(i + 1, items.Count, $"Processing {i + 1}/{items.Count}...")); + try + { + await processItem(items[i], i, ct); + results.Add(BulkItemResult.Success(items[i])); + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + results.Add(BulkItemResult.Failed(items[i], ex.Message)); + } + } + progress.Report(new OperationProgress(items.Count, items.Count, "Complete.")); + return new BulkOperationSummary(results); + } +} diff --git a/SharepointToolbox/Services/Export/BulkResultCsvExportService.cs b/SharepointToolbox/Services/Export/BulkResultCsvExportService.cs new file mode 100644 index 0000000..58498f7 --- /dev/null +++ b/SharepointToolbox/Services/Export/BulkResultCsvExportService.cs @@ -0,0 +1,40 @@ +using System.Globalization; +using System.IO; +using System.Text; +using CsvHelper; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services.Export; + +public class BulkResultCsvExportService +{ + public string BuildFailedItemsCsv(IReadOnlyList> failedItems) + { + using var writer = new StringWriter(); + using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); + + csv.WriteHeader(); + csv.WriteField("Error"); + csv.WriteField("Timestamp"); + csv.NextRecord(); + + foreach (var item in failedItems.Where(r => !r.IsSuccess)) + { + csv.WriteRecord(item.Item); + csv.WriteField(item.ErrorMessage); + csv.WriteField(item.Timestamp.ToString("o")); + csv.NextRecord(); + } + + return writer.ToString(); + } + + public async Task WriteFailedItemsCsvAsync( + IReadOnlyList> failedItems, + string filePath, + CancellationToken ct) + { + var content = BuildFailedItemsCsv(failedItems); + await System.IO.File.WriteAllTextAsync(filePath, content, new UTF8Encoding(true), ct); + } +} diff --git a/SharepointToolbox/Services/IBulkMemberService.cs b/SharepointToolbox/Services/IBulkMemberService.cs new file mode 100644 index 0000000..77c6b23 --- /dev/null +++ b/SharepointToolbox/Services/IBulkMemberService.cs @@ -0,0 +1,14 @@ +using Microsoft.SharePoint.Client; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public interface IBulkMemberService +{ + Task> AddMembersAsync( + ClientContext ctx, + string clientId, + IReadOnlyList rows, + IProgress progress, + CancellationToken ct); +} diff --git a/SharepointToolbox/Services/IBulkSiteService.cs b/SharepointToolbox/Services/IBulkSiteService.cs new file mode 100644 index 0000000..803fcd8 --- /dev/null +++ b/SharepointToolbox/Services/IBulkSiteService.cs @@ -0,0 +1,13 @@ +using Microsoft.SharePoint.Client; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public interface IBulkSiteService +{ + Task> CreateSitesAsync( + ClientContext adminCtx, + IReadOnlyList rows, + IProgress progress, + CancellationToken ct); +} diff --git a/SharepointToolbox/Services/ICsvValidationService.cs b/SharepointToolbox/Services/ICsvValidationService.cs new file mode 100644 index 0000000..d895f9f --- /dev/null +++ b/SharepointToolbox/Services/ICsvValidationService.cs @@ -0,0 +1,12 @@ +using System.IO; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public interface ICsvValidationService +{ + List> ParseAndValidate(Stream csvStream) where T : class; + List> ParseAndValidateMembers(Stream csvStream); + List> ParseAndValidateSites(Stream csvStream); + List> ParseAndValidateFolders(Stream csvStream); +} diff --git a/SharepointToolbox/Services/IFileTransferService.cs b/SharepointToolbox/Services/IFileTransferService.cs new file mode 100644 index 0000000..50306dd --- /dev/null +++ b/SharepointToolbox/Services/IFileTransferService.cs @@ -0,0 +1,18 @@ +using Microsoft.SharePoint.Client; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public interface IFileTransferService +{ + /// + /// Transfers files/folders from source to destination. + /// Returns per-item results (file paths as string items). + /// + Task> TransferAsync( + ClientContext sourceCtx, + ClientContext destCtx, + TransferJob job, + IProgress progress, + CancellationToken ct); +} diff --git a/SharepointToolbox/Services/IFolderStructureService.cs b/SharepointToolbox/Services/IFolderStructureService.cs new file mode 100644 index 0000000..b931e6f --- /dev/null +++ b/SharepointToolbox/Services/IFolderStructureService.cs @@ -0,0 +1,14 @@ +using Microsoft.SharePoint.Client; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public interface IFolderStructureService +{ + Task> CreateFoldersAsync( + ClientContext ctx, + string libraryTitle, + IReadOnlyList rows, + IProgress progress, + CancellationToken ct); +} diff --git a/SharepointToolbox/Services/ITemplateService.cs b/SharepointToolbox/Services/ITemplateService.cs new file mode 100644 index 0000000..43492b3 --- /dev/null +++ b/SharepointToolbox/Services/ITemplateService.cs @@ -0,0 +1,22 @@ +using Microsoft.SharePoint.Client; +using SharepointToolbox.Core.Models; +using ModelSiteTemplate = SharepointToolbox.Core.Models.SiteTemplate; + +namespace SharepointToolbox.Services; + +public interface ITemplateService +{ + Task CaptureTemplateAsync( + ClientContext ctx, + SiteTemplateOptions options, + IProgress progress, + CancellationToken ct); + + Task ApplyTemplateAsync( + ClientContext adminCtx, + ModelSiteTemplate template, + string newSiteTitle, + string newSiteAlias, + IProgress progress, + CancellationToken ct); +} diff --git a/SharepointToolbox/SharepointToolbox.csproj b/SharepointToolbox/SharepointToolbox.csproj index 24a4c99..c635ea4 100644 --- a/SharepointToolbox/SharepointToolbox.csproj +++ b/SharepointToolbox/SharepointToolbox.csproj @@ -27,7 +27,9 @@ + +