+
+
+
+ @Model.Activity.GetTitleOrDefault(() => T["Get users in roles"])
+
+
+@T["Get users in roles {0} to {1}", string.Join(',', Model.Activity.Roles), Model.Activity.OutputKeyName]
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/GetUsersByRoleTask.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/GetUsersByRoleTask.Fields.Edit.cshtml
new file mode 100644
index 00000000000..e127ea6f91a
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/GetUsersByRoleTask.Fields.Edit.cshtml
@@ -0,0 +1,17 @@
+@using OrchardCore.Roles.Workflows.ViewModels
+
+@model GetUsersByRoleTaskViewModel
+
+
+ @T["Output Key Name"]
+
+
+ @T["The designated key name for storing the users' list in the workflow output. This key enables later access to the list within the workflow.With Liquid support."]
+
+
+
+ @T["Roles"]
+ @await Component.InvokeAsync("SelectRoles", new { selectedRoles = Model.Roles, htmlName = Html.NameFor(m => m.Roles), except = new[] { "Anonymous", "Authenticated" } })
+
+ @T["Choose the roles used to identify users during list generation."]
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/GetUsersByRoleTask.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/GetUsersByRoleTask.Fields.Thumbnail.cshtml
new file mode 100644
index 00000000000..af8fe0c29f4
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/GetUsersByRoleTask.Fields.Thumbnail.cshtml
@@ -0,0 +1,4 @@
+
+ @T["Get users in roles"]
+
+@T["Get users in roles."]
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Design.cshtml b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Design.cshtml
new file mode 100644
index 00000000000..8476667d3f6
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Design.cshtml
@@ -0,0 +1,13 @@
+@using OrchardCore.Workflows.ViewModels
+@using OrchardCore.Workflows.Helpers
+@using OrchardCore.Roles.Workflows.Activities
+@using OrchardCore.Roles.Workflows.ViewModels
+
+@model ActivityViewModel
+
+
+
+ @Model.Activity.GetTitleOrDefault(() => T["Unassign user from roles"])
+
+
+@T["Unassign {0} from roles {1}", Model.Activity.UserName, string.Join(",", Model.Activity.Roles)]
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Edit.cshtml
new file mode 100644
index 00000000000..a48ad1dc01f
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Edit.cshtml
@@ -0,0 +1,17 @@
+@using OrchardCore.Roles.Workflows.ViewModels
+
+@model UnassignUserRoleTaskViewModel
+
+
+ @T["UserName"]
+
+
+ @T["The User to update. With Liquid support."]
+
+
+
+ @T["Roles"]
+ @await Component.InvokeAsync("SelectRoles", new { selectedRoles = Model.Roles, htmlName = Html.NameFor(m => m.Roles), except = new[] { "Anonymous", "Authenticated" } })
+
+ @T["The Roles to remove."]
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Thumbnail.cshtml b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Thumbnail.cshtml
new file mode 100644
index 00000000000..825971a5265
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Items/UnassignUserRoleTask.Fields.Thumbnail.cshtml
@@ -0,0 +1,4 @@
+
+ @T["Unassign user from roles"]
+
+@T["Unassign a user from roles."]
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Shared/Components/SelectRoles/Default.cshtml b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Shared/Components/SelectRoles/Default.cshtml
index 06a4d9cb2a8..23388e17d15 100644
--- a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Shared/Components/SelectRoles/Default.cshtml
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Shared/Components/SelectRoles/Default.cshtml
@@ -1,13 +1,15 @@
@model SelectRolesViewModel
-
+
@for (var i = 0; i < Model.RoleSelections.Count; i++)
{
var role = Model.RoleSelections[i];
-
-
-
- @role.Item
+ var htmlId = Html.IdFor(m => m.RoleSelections[i]);
+
+
+
+
+@role.Item
}
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Activities/GetUsersByRoleTask.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Activities/GetUsersByRoleTask.cs
new file mode 100644
index 00000000000..d420701388d
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Activities/GetUsersByRoleTask.cs
@@ -0,0 +1,76 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Fluid;
+using Fluid.Values;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Localization;
+using OrchardCore.Users;
+using OrchardCore.Users.Models;
+using OrchardCore.Workflows.Abstractions.Models;
+using OrchardCore.Workflows.Activities;
+using OrchardCore.Workflows.Models;
+using OrchardCore.Workflows.Services;
+
+namespace OrchardCore.Roles.Workflows.Activities;
+
+public class GetUsersByRoleTask : TaskActivity
+{
+ private readonly UserManager _userManager;
+ private readonly IWorkflowExpressionEvaluator _expressionEvaluator;
+ protected readonly IStringLocalizer S;
+
+ public GetUsersByRoleTask(UserManager userManager, IWorkflowExpressionEvaluator expressionEvaluator, IStringLocalizer localizer)
+ {
+ _userManager = userManager;
+ _expressionEvaluator = expressionEvaluator;
+ S = localizer;
+ }
+
+ public override LocalizedString DisplayText => S["Get Users by Role Task"];
+
+ public override LocalizedString Category => S["User"];
+
+ public WorkflowExpression OutputKeyName
+ {
+ get => GetProperty(() => new WorkflowExpression());
+ set => SetProperty(value);
+ }
+
+ public IEnumerable Roles
+ {
+ get => GetProperty(() => new List());
+ set => SetProperty(value);
+ }
+
+ public override IEnumerable GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
+ => Outcomes(S["Done"], S["Failed"]);
+
+ public override async Task ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
+ {
+ var outputKeyName = await _expressionEvaluator.EvaluateAsync(OutputKeyName, workflowContext, null);
+
+ if (!string.IsNullOrEmpty(outputKeyName))
+ {
+ var usersInRole = new Dictionary();
+
+ foreach (var role in Roles)
+ {
+ foreach (var u in await _userManager.GetUsersInRoleAsync(role))
+ {
+ if (u is not User user)
+ {
+ continue;
+ }
+
+ usersInRole.TryAdd(user.Id.ToString(), user.UserId);
+ }
+ }
+
+ workflowContext.Output[outputKeyName] = FluidValue.Create(usersInRole, new TemplateOptions());
+
+ return Outcomes("Done");
+ }
+
+ return Outcomes("Failed");
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Activities/UnassignUserRoleTask.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Activities/UnassignUserRoleTask.cs
new file mode 100644
index 00000000000..985a36ecabe
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Activities/UnassignUserRoleTask.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.Localization;
+using OrchardCore.Users;
+using OrchardCore.Users.Models;
+using OrchardCore.Users.Services;
+using OrchardCore.Workflows.Abstractions.Models;
+using OrchardCore.Workflows.Activities;
+using OrchardCore.Workflows.Models;
+using OrchardCore.Workflows.Services;
+
+namespace OrchardCore.Roles.Workflows.Activities;
+
+public class UnassignUserRoleTask : TaskActivity
+{
+ private readonly UserManager _userManager;
+ private readonly IUserService _userService;
+ private readonly IWorkflowExpressionEvaluator _expressionEvaluator;
+ protected readonly IStringLocalizer S;
+
+ public UnassignUserRoleTask(UserManager userManager, IUserService userService, IWorkflowExpressionEvaluator expressionvaluator, IStringLocalizer localizer)
+ {
+ _userManager = userManager;
+ _userService = userService;
+ _expressionEvaluator = expressionvaluator;
+ S = localizer;
+ }
+
+ public override LocalizedString DisplayText => S["Unassign User Role Task"];
+
+ public override LocalizedString Category => S["User"];
+
+ public WorkflowExpression UserName
+ {
+ get => GetProperty(() => new WorkflowExpression());
+ set => SetProperty(value);
+ }
+
+ public IEnumerable Roles
+ {
+ get => GetProperty(() => new List());
+ set => SetProperty(value);
+ }
+
+ public override IEnumerable GetPossibleOutcomes(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
+ => Outcomes(S["Done"], S["Failed"]);
+
+ public override async Task ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
+ {
+ var userName = await _expressionEvaluator.EvaluateAsync(UserName, workflowContext, null);
+
+ var u = await _userService.GetUserAsync(userName);
+
+ if (u is User user)
+ {
+ foreach(var role in Roles)
+ {
+ if (user.RoleNames.Contains(role))
+ {
+ await _userManager.RemoveFromRoleAsync(user, role);
+ }
+ }
+
+ return Outcomes("Done");
+ }
+
+ return Outcomes("Failed");
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Drivers/GetUsersByRoleTaskDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Drivers/GetUsersByRoleTaskDisplayDriver.cs
new file mode 100644
index 00000000000..362a5b96297
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Drivers/GetUsersByRoleTaskDisplayDriver.cs
@@ -0,0 +1,21 @@
+using OrchardCore.Roles.Workflows.Activities;
+using OrchardCore.Roles.Workflows.ViewModels;
+using OrchardCore.Workflows.Display;
+using OrchardCore.Workflows.Models;
+
+namespace OrchardCore.Roles.Workflows.Drivers;
+
+public class GetUsersByRoleTaskDisplayDriver : ActivityDisplayDriver
+{
+ protected override void EditActivity(GetUsersByRoleTask activity, GetUsersByRoleTaskViewModel model)
+ {
+ model.OutputKeyName = activity.OutputKeyName.Expression;
+ model.Roles = activity.Roles;
+ }
+
+ protected override void UpdateActivity(GetUsersByRoleTaskViewModel model, GetUsersByRoleTask activity)
+ {
+ activity.OutputKeyName = new WorkflowExpression(model.OutputKeyName);
+ activity.Roles = model.Roles;
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Drivers/UnassignUserRoleTaskDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Drivers/UnassignUserRoleTaskDisplayDriver.cs
new file mode 100644
index 00000000000..aef9ee5d99a
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Drivers/UnassignUserRoleTaskDisplayDriver.cs
@@ -0,0 +1,21 @@
+using OrchardCore.Roles.Workflows.Activities;
+using OrchardCore.Roles.Workflows.ViewModels;
+using OrchardCore.Workflows.Display;
+using OrchardCore.Workflows.Models;
+
+namespace OrchardCore.Roles.Workflows.Drivers;
+
+public class UnassignUserRoleTaskDisplayDriver : ActivityDisplayDriver
+{
+ protected override void EditActivity(UnassignUserRoleTask activity, UnassignUserRoleTaskViewModel model)
+ {
+ model.UserName = activity.UserName.Expression;
+ model.Roles = activity.Roles;
+ }
+
+ protected override void UpdateActivity(UnassignUserRoleTaskViewModel model, UnassignUserRoleTask activity)
+ {
+ activity.UserName = new WorkflowExpression(model.UserName);
+ activity.Roles = model.Roles;
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Startup.cs
new file mode 100644
index 00000000000..e0b27296920
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/Startup.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.DependencyInjection;
+using OrchardCore.Modules;
+using OrchardCore.Roles.Workflows.Activities;
+using OrchardCore.Roles.Workflows.Drivers;
+using OrchardCore.Workflows.Helpers;
+
+namespace OrchardCore.Roles.Workflows;
+
+[RequireFeatures("OrchardCore.Workflows")]
+public class Startup : StartupBase
+{
+ public override void ConfigureServices(IServiceCollection services)
+ {
+ services.AddActivity();
+ services.AddActivity();
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/ViewModels/GetUsersByRoleTaskViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/ViewModels/GetUsersByRoleTaskViewModel.cs
new file mode 100644
index 00000000000..18a31bcae96
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/ViewModels/GetUsersByRoleTaskViewModel.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace OrchardCore.Roles.Workflows.ViewModels;
+
+public class GetUsersByRoleTaskViewModel
+{
+ [Required]
+ public string OutputKeyName { get; set; }
+
+ [Required]
+ public IEnumerable Roles { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/ViewModels/UnassignUserRoleTaskViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/ViewModels/UnassignUserRoleTaskViewModel.cs
new file mode 100644
index 00000000000..6049d1422cf
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.Roles/Views/Workflows/ViewModels/UnassignUserRoleTaskViewModel.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace OrchardCore.Roles.Workflows.ViewModels;
+
+public class UnassignUserRoleTaskViewModel
+{
+ [Required]
+ public string UserName { get; set; }
+
+ [Required]
+ public IEnumerable Roles { get; set; }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Liquid/UsersByIdFilter.cs b/src/OrchardCore.Modules/OrchardCore.Users/Liquid/UsersByIdFilter.cs
index 6ddc1645af8..f8447d7f6e2 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Liquid/UsersByIdFilter.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Liquid/UsersByIdFilter.cs
@@ -1,3 +1,4 @@
+using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Fluid;
@@ -21,6 +22,34 @@ public UsersByIdFilter(ISession session)
public async ValueTask ProcessAsync(FluidValue input, FilterArguments arguments, LiquidTemplateContext ctx)
{
+ if (input.Type == FluidValues.Dictionary)
+ {
+ if (input.ToObjectValue() is IFluidIndexable values)
+ {
+ var items = new Dictionary();
+
+ foreach (var key in values.Keys)
+ {
+ if (long.TryParse(key, out var id) && values.TryGetValue(key, out var value))
+ {
+ items.Add(id, value.ToStringValue());
+ }
+ }
+
+ var cachedUsers = await _session.GetAsync(items.Keys.ToArray());
+
+ var cachedUserIds = cachedUsers.Select(x => x.UserId).ToHashSet();
+
+ var missingUserIds = items.Values.Where(userId => !cachedUserIds.Contains(userId)).ToList();
+
+ var missingUsers = await _session.Query(x => x.UserId.IsIn(missingUserIds)).ListAsync();
+
+ return FluidValue.Create(missingUsers.Concat(cachedUsers).ToList(), ctx.Options);
+ }
+
+ return NilValue.Empty;
+ }
+
if (input.Type == FluidValues.Array)
{
// List of user ids