diff --git a/authoring/items/Mvp/Feature.Selections/items/renderings/Selections/Admin/Users/MergeUsers.yml b/authoring/items/Mvp/Feature.Selections/items/renderings/Selections/Admin/Users/MergeUsers.yml new file mode 100644 index 00000000..5e564085 --- /dev/null +++ b/authoring/items/Mvp/Feature.Selections/items/renderings/Selections/Admin/Users/MergeUsers.yml @@ -0,0 +1,38 @@ +--- +ID: "b68ca8f0-65af-4a9a-95be-d9cd757b641b" +Parent: "005295d9-718f-4e20-bcf7-76c37ca95b0e" +Template: "04646a89-996f-4ee7-878a-ffdbf1f0ef0d" +Path: /sitecore/layout/Renderings/Feature/Selections/Admin/Users/MergeUsers +SharedFields: +- ID: "037fe404-dd19-4bf7-8e30-4dadf68b27b0" + Hint: componentName + Value: AdminMergeUsers +- ID: "06d5295c-ed2f-4a54-9bf2-26228d113318" + Hint: __Icon + Value: Office/32x32/users_relation2.png +Languages: +- Language: en + Versions: + - Version: 1 + Fields: + - ID: "25bed78c-4957-4165-998a-ca1b52f67497" + Hint: __Created + Value: 20250203T163816Z + - ID: "52807595-0f8f-4b20-8d2a-cb71d28c6103" + Hint: __Owner + Value: | + sitecore\ivan.lieckens@sitecore.com + - ID: "5dd74568-4d4b-44c1-b513-0af5f4cda34f" + Hint: __Created by + Value: | + sitecore\ivan.lieckens@sitecore.com + - ID: "8cdc337e-a112-42fb-bbb4-4143751e123f" + Hint: __Revision + Value: "c26fd65d-504a-4833-9306-06c5918c5047" + - ID: "badd9cf9-53e0-4d0c-bcc0-2d784c282f6a" + Hint: __Updated by + Value: | + sitecore\ivan.lieckens@sitecore.com + - ID: "d9cf14b1-fa16-4ba6-9288-e8a174d4d522" + Hint: __Updated + Value: 20250203T170749Z diff --git a/headapps/MvpSite/MvpSite.Rendering/Extensions/ICollectionExtensions.cs b/headapps/MvpSite/MvpSite.Rendering/Extensions/ICollectionExtensions.cs new file mode 100644 index 00000000..38dc5f07 --- /dev/null +++ b/headapps/MvpSite/MvpSite.Rendering/Extensions/ICollectionExtensions.cs @@ -0,0 +1,13 @@ +namespace MvpSite.Rendering.Extensions; + +// ReSharper disable once InconsistentNaming - This class extends the interface, not the type +public static class ICollectionExtensions +{ + public static void AddRange(this ICollection collection, IEnumerable items) + { + foreach (T item in items) + { + collection.Add(item); + } + } +} \ No newline at end of file diff --git a/headapps/MvpSite/MvpSite.Rendering/Extensions/RenderingEngineOptionsExtensions.cs b/headapps/MvpSite/MvpSite.Rendering/Extensions/RenderingEngineOptionsExtensions.cs index 01bd6522..50be822a 100644 --- a/headapps/MvpSite/MvpSite.Rendering/Extensions/RenderingEngineOptionsExtensions.cs +++ b/headapps/MvpSite/MvpSite.Rendering/Extensions/RenderingEngineOptionsExtensions.cs @@ -75,6 +75,7 @@ public static RenderingEngineOptions AddFeatureSelections(this RenderingEngineOp options.AddViewComponent(ApplicationReviewSettingsViewComponent.ViewComponentName); options.AddViewComponent(ContributionOverviewViewComponent.ViewComponentName); options.AddViewComponent(SelectionOverviewViewComponent.ViewComponentName); + options.AddViewComponent(MergeUsersViewComponent.ViewComponentName); return options; } } \ No newline at end of file diff --git a/headapps/MvpSite/MvpSite.Rendering/Models/Admin/MergeUsersModel.cs b/headapps/MvpSite/MvpSite.Rendering/Models/Admin/MergeUsersModel.cs new file mode 100644 index 00000000..9ba86b7f --- /dev/null +++ b/headapps/MvpSite/MvpSite.Rendering/Models/Admin/MergeUsersModel.cs @@ -0,0 +1,30 @@ +using Mvp.Selections.Domain; + +namespace MvpSite.Rendering.Models.Admin; + +public class MergeUsersModel : BaseModel +{ + public List OldUserOptions { get; init; } = []; + + public List TargetUserOptions { get; init; } = []; + + public Guid? SelectedOldUserId { get; set; } + + public Guid? SelectedTargetUserId { get; set; } + + public User? OldUser { get; set; } + + public User? TargetUser { get; set; } + + public string? OldUserNameSearch { get; set; } + + public string? OldUserEmailSearch { get; set; } + + public string? TargetUserNameSearch { get; set; } + + public string? TargetUserEmailSearch { get; set; } + + public bool IsMerging { get; set; } + + public User? MergedUser { get; set; } +} \ No newline at end of file diff --git a/headapps/MvpSite/MvpSite.Rendering/ViewComponents/Admin/MergeUsersViewComponent.cs b/headapps/MvpSite/MvpSite.Rendering/ViewComponents/Admin/MergeUsersViewComponent.cs new file mode 100644 index 00000000..20d2e7b2 --- /dev/null +++ b/headapps/MvpSite/MvpSite.Rendering/ViewComponents/Admin/MergeUsersViewComponent.cs @@ -0,0 +1,131 @@ +using System.Net; +using Microsoft.AspNetCore.Mvc; +using Mvp.Selections.Client; +using Mvp.Selections.Client.Models; +using Mvp.Selections.Domain; +using MvpSite.Rendering.Extensions; +using MvpSite.Rendering.Models.Admin; +using Sitecore.AspNetCore.SDK.RenderingEngine.Binding; + +namespace MvpSite.Rendering.ViewComponents.Admin; + +[ViewComponent(Name = ViewComponentName)] +public class MergeUsersViewComponent(IViewModelBinder modelBinder, MvpSelectionsApiClient client) + : BaseViewComponent(modelBinder, client) +{ + public const string ViewComponentName = "AdminMergeUsers"; + + public override async Task InvokeAsync() + { + MergeUsersModel model = await ModelBinder.Bind(ViewContext); + Response userResponse = await Client.GetCurrentUserAsync(); + if (userResponse is { StatusCode: HttpStatusCode.OK, Result: not null }) + { + User currentUser = userResponse.Result; + if (currentUser.HasRight(Right.Admin)) + { + if (!model.IsMerging && (!string.IsNullOrWhiteSpace(model.OldUserNameSearch) || !string.IsNullOrWhiteSpace(model.OldUserEmailSearch))) + { + model.OldUserOptions.AddRange( + await SearchUsersAsync(model, model.OldUserNameSearch, model.OldUserEmailSearch)); + } + + if (!model.IsMerging && (!string.IsNullOrWhiteSpace(model.TargetUserNameSearch) || !string.IsNullOrWhiteSpace(model.TargetUserEmailSearch))) + { + model.TargetUserOptions.AddRange( + await SearchUsersAsync(model, model.TargetUserNameSearch, model.TargetUserEmailSearch)); + } + + if (model.SelectedOldUserId.HasValue) + { + model.OldUser = await GetUserAsync(model, model.SelectedOldUserId.Value); + } + + if (model.SelectedTargetUserId.HasValue) + { + model.TargetUser = await GetUserAsync(model, model.SelectedTargetUserId.Value); + } + + if (model.IsMerging && model is { SelectedOldUserId: not null, SelectedTargetUserId: not null }) + { + Response mergedUserResponse = await Client.MergeUsersAsync(model.SelectedOldUserId.Value, model.SelectedTargetUserId.Value); + if (mergedUserResponse is { StatusCode: HttpStatusCode.OK, Result: not null }) + { + model.MergedUser = mergedUserResponse.Result; + ModelState.Clear(); + model.SelectedOldUserId = null; + model.SelectedTargetUserId = null; + model.OldUserNameSearch = null; + model.OldUserEmailSearch = null; + model.TargetUserNameSearch = null; + model.TargetUserEmailSearch = null; + await LoadApplications(model, model.MergedUser.Id, model.MergedUser); + } + else + { + model.ErrorMessages.Add(mergedUserResponse.Message); + } + } + } + else + { + model.ErrorMessages.Add("Only an Administrator can merge users."); + } + } + else + { + model.ErrorMessages.Add(userResponse.Message); + } + + return model.ErrorMessages.Count > 0 ? View("~/Views/Shared/_Error.cshtml", model) : View(model); + } + + private async Task> SearchUsersAsync(MergeUsersModel model, string? name, string? email) + { + List result = []; + if (!string.IsNullOrWhiteSpace(name) || !string.IsNullOrWhiteSpace(email)) + { + Response> response = await Client.GetUsersAsync(name, email); + if (response is { StatusCode: HttpStatusCode.OK, Result: not null }) + { + result.AddRange(response.Result); + } + else + { + model.ErrorMessages.Add(response.Message); + } + } + + return result; + } + + private async Task GetUserAsync(MergeUsersModel model, Guid userId) + { + User? result = null; + Response response = await Client.GetUserAsync(userId); + if (response is { StatusCode: HttpStatusCode.OK, Result: not null }) + { + result = response.Result; + await LoadApplications(model, userId, result); + } + else + { + model.ErrorMessages.Add(response.Message); + } + + return result; + } + + private async Task LoadApplications(MergeUsersModel model, Guid userId, User user) + { + Response> response = await Client.GetApplicationsForUserAsync(userId); + if (response is { StatusCode: HttpStatusCode.OK, Result: not null }) + { + user.Applications.AddRange(response.Result); + } + else + { + model.ErrorMessages.Add(response.Message); + } + } +} \ No newline at end of file diff --git a/headapps/MvpSite/MvpSite.Rendering/Views/Shared/Components/AdminMergeUsers/Default.cshtml b/headapps/MvpSite/MvpSite.Rendering/Views/Shared/Components/AdminMergeUsers/Default.cshtml new file mode 100644 index 00000000..073fbc1f --- /dev/null +++ b/headapps/MvpSite/MvpSite.Rendering/Views/Shared/Components/AdminMergeUsers/Default.cshtml @@ -0,0 +1,159 @@ +@using Mvp.Selections.Domain +@using Mvp.Selections.Domain.Roles +@model MvpSite.Rendering.Models.Admin.MergeUsersModel + +
+
+
+
+

Old User

+
+
+ +
+
+ +
+
+ +
+
+ @if (Model.OldUserOptions.Count > 0) + { +
+
+ +
+
+ +
+
+ } + @if (Model.OldUser != null) + { +
+
+

@Model.OldUser.Name

+

@Model.OldUser.Email

+

+

    +
  • + Roles: + @foreach (Role role in Model.OldUser.Roles) + { + @role.Name + } +
  • +
  • + Applications: + @foreach (Application application in Model.OldUser.Applications) + { + @application.Selection.Year + } +
  • +
+

+
+
+ } +
+
+

Target User

+
+
+ +
+
+ +
+
+ +
+
+ @if (Model.TargetUserOptions.Count > 0) + { +
+
+ +
+
+ +
+
+ } + @if (Model.TargetUser != null) + { +
+
+

@Model.TargetUser.Name

+

@Model.TargetUser.Email

+

+

    +
  • + Roles: + @foreach (Role role in Model.TargetUser.Roles) + { + @role.Name + } +
  • +
  • + Applications: + @foreach (Application application in Model.TargetUser.Applications) + { + @application.Selection.Year + } +
  • +
+

+
+
+ } +
+
+ @if (Model is { OldUser: not null, TargetUser: not null, MergedUser: null }) + { + + + } + else if (Model.MergedUser != null) + { + +
+
+

@Model.MergedUser.Name

+

@Model.MergedUser.Email

+

+

    +
  • + Roles: + @foreach (Role role in Model.MergedUser.Roles) + { + @role.Name + } +
  • +
  • + Applications: + @foreach (Application application in Model.MergedUser.Applications) + { + @application.Selection.Year + } +
  • +
+

+
+
+ } +
+