Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content list owner filter #15470

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Mvc.Rendering;
Expand All @@ -24,6 +25,9 @@
using OrchardCore.Navigation;
using OrchardCore.Routing;
using OrchardCore.Security.Permissions;
using OrchardCore.Users;
using OrchardCore.Users.Indexes;
using OrchardCore.Users.Models;
using YesSql;
using YesSql.Filters.Query;
using YesSql.Services;
Expand All @@ -37,6 +41,7 @@ public class AdminController : Controller, IUpdateModel
private readonly IContentItemDisplayManager _contentItemDisplayManager;
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IDisplayManager<ContentOptionsViewModel> _contentOptionsDisplayManager;
private readonly UserManager<IUser> _userManager;
private readonly ISession _session;
private readonly INotifier _notifier;

Expand All @@ -49,6 +54,7 @@ public class AdminController : Controller, IUpdateModel
IContentItemDisplayManager contentItemDisplayManager,
IContentDefinitionManager contentDefinitionManager,
IDisplayManager<ContentOptionsViewModel> contentOptionsDisplayManager,
UserManager<IUser> userManager,
ISession session,
INotifier notifier,
IHtmlLocalizer<AdminController> htmlLocalizer,
Expand All @@ -59,6 +65,7 @@ public class AdminController : Controller, IUpdateModel
_contentItemDisplayManager = contentItemDisplayManager;
_contentDefinitionManager = contentDefinitionManager;
_contentOptionsDisplayManager = contentOptionsDisplayManager;
_userManager = userManager;
_session = session;
_notifier = notifier;

Expand Down Expand Up @@ -643,6 +650,42 @@ public async Task<IActionResult> Unpublish(string contentItemId, string returnUr
: RedirectToAction(nameof(List));
}

public async Task<IActionResult> OwnerFilterUserSearch(string searchTerm, string selectedOwnerUserName, int page = 1)
{
var pageSize = 50;
douwinga marked this conversation as resolved.
Show resolved Hide resolved

var query = _session.Query<User, UserIndex>();

if (!string.IsNullOrEmpty(searchTerm))
{
query.Where(x => x.NormalizedUserName.Contains(_userManager.NormalizeName(searchTerm)));
}

var users = await query
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may need to cache this for optimization otherwise we'll significantly increase the server load.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the same thought. It at least isn't page blocking with it loading after page render. I was thinking it would be fine if it would only start loading users when the select dropdown is shown, but bootstrap-select does not make that easy. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@douwinga we agree on your suggestion to only load the list on the first drop down, and maybe less than 50, probably something close to what the drop down can fit by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If "Owner" is indexed, you may also not have to load all users, but just the distinct values of this field.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@douwinga a better idea Sebestien mentioned was to not load any users by default. If the user clicks on the filter menu, then make an AJAX request that would fetch the first 50 users as you are doing today. This way you don't need to cache anything and also you don't need to render anything on every single request.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MikeAlhayek ya, I am just struggling to implement that with bootstrap-select

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me know what you think. I made it load the data on first dropdown toggle. It is gross, but I am not sure how else to do it. bootstrap-select needs help, but I want to use it for this so that it is consistent with all the other filter dropdowns,

.OrderBy(u => u.NormalizedUserName)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ListAsync();

var hasMoreResults = false;

// only check if we have more results if the current page had a full page of results
if (users.Count() == pageSize)
{
var totalUsersCount = await query.CountAsync();
hasMoreResults = page * pageSize < totalUsersCount;
}

var results = users.Select(u => new SelectListItem()
{
Text = u.UserName,
Value = u.UserName,
Selected = _userManager.NormalizeName(u.UserName) == _userManager.NormalizeName(selectedOwnerUserName)
});

return new ObjectResult(new { results, hasMoreResults });
}

private async Task<IActionResult> CreatePOST(
string id,
string returnUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public override IDisplayResult Display(ContentOptionsViewModel model)
View("ContentsAdminFilters_Thumbnail__ContentType", model).Location("Thumbnail", "Content:20"),
View("ContentsAdminFilters_Thumbnail__Stereotype", model).Location("Thumbnail", "Content:30"),
View("ContentsAdminFilters_Thumbnail__Status", model).Location("Thumbnail", "Content:40"),
View("ContentsAdminFilters_Thumbnail__Sort", model).Location("Thumbnail", "Content:50")
View("ContentsAdminFilters_Thumbnail__Owner", model).Location("Thumbnail", "Content:50"),
View("ContentsAdminFilters_Thumbnail__Sort", model).Location("Thumbnail", "Content:60")
);
}

Expand Down Expand Up @@ -65,6 +66,7 @@ private static void BuildContentOptionsViewModel(ContentOptionsViewModel m, Cont
m.OrderBy = model.OrderBy;
m.SelectedContentType = model.SelectedContentType;
m.FilterResult = model.FilterResult;
m.SelectedOwnerUserName = model.SelectedOwnerUserName;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Settings.Core\OrchardCore.Settings.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Sitemaps.Abstractions\OrchardCore.Sitemaps.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Users.Abstractions\OrchardCore.Users.Abstractions.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Users.Core\OrchardCore.Users.Core.csproj" />
<ProjectReference Include="..\..\OrchardCore\OrchardCore.Workflows.Abstractions\OrchardCore.Workflows.Abstractions.csproj" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Metadata;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.ContentManagement.Records;
using OrchardCore.Contents.ViewModels;
using OrchardCore.Users;
using OrchardCore.Users.Models;
using YesSql;
using YesSql.Filters.Query;
using YesSql.Services;
Expand Down Expand Up @@ -249,6 +252,39 @@ public void Build(QueryEngineBuilder<ContentItem> builder)
return query;
})
)
.WithNamedTerm("owner", builder => builder
.OneCondition(async (userName, query, ctx) =>
{
if (!string.IsNullOrEmpty(userName))
{
var context = (ContentQueryContext)ctx;
var userManager = context.ServiceProvider.GetRequiredService<UserManager<IUser>>();
var user = await userManager.FindByNameAsync(userName) as User;
var userId = user?.UserId;

query.With<ContentItemIndex>(x => x.Owner == userId);
}

return query;
})
.MapTo<ContentOptionsViewModel>((userName, model) =>
{
if (!string.IsNullOrEmpty(userName))
{
model.SelectedOwnerUserName = userName;
}
})
.MapFrom<ContentOptionsViewModel>((model) =>
{
if (!string.IsNullOrEmpty(model.SelectedOwnerUserName))
{
return (true, model.SelectedOwnerUserName);
}

return (false, string.Empty);
})
.AlwaysRun()
)
.WithDefaultTerm(ContentsAdminListFilterOptions.DefaultTermName, builder => builder
.ManyCondition(
(val, query) => query.With<ContentItemIndex>(x => x.DisplayText.Contains(val)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@
var selectedItems = $("#selected-items");
var itemsCheckboxes = $(":checkbox[name='itemIds']");

@* This applies to all filter selectpickers on page. Add .nosubmit to not submit *@
$('.selectpicker:not(.nosubmit)').on('changed.bs.select', function (e, clickedIndex, isSelected, previousValue) {
$("[name='submit.Filter']").click();
});
@* This applies to all filter selectpickers on page. Add .nosubmit to not submit *@
$('.selectpicker:not(.nosubmit),.ajax-selectpicker:not(.nosubmit)').on('changed.bs.select', function (e, clickedIndex, isSelected, previousValue) {
$("[name='submit.Filter']").click();
});

$(".dropdown-menu .dropdown-item").filter(function () {
return $(this).data("action");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,47 @@
@model ContentOptionsViewModel
@{
var formActionWithoutTypeName = Url.RouteUrl(new { area = "OrchardCore.Contents", controller = "Admin", action = "List", contentTypeId = "" });
var ownerFilterUserSearchUrl = Url.RouteUrl(new { area = "OrchardCore.Contents", controller = "Admin", action = "OwnerFilterUserSearch" });
}
<div class="btn-group filter">
@if (Model.ContentTypeOptions.Count > 0)
{
<select asp-for="SelectedContentType" asp-items="Model.ContentTypeOptions" class="selectpicker contenttypes-selectpicker show-tick me-2" data-header="@T["Filter by content type"]" data-selected-text-format="static" data-width="fit" data-dropdown-align-right="auto" title="@T["Content Type"]" data-style="btn-sm" data-live-search="true"></select>
}
<select asp-for="ContentsStatus" asp-items="@Model.ContentStatuses" class="selectpicker contentstatuses-selectpicker show-tick me-2" data-header="@T["Filter by status"]" data-selected-text-format="static" data-width="fit" data-dropdown-align-right="auto" title="@T["Show"]" data-style="btn-sm"></select>
<select asp-for="SelectedOwnerUserName" class="ajax-selectpicker contentowners-selectpicker show-tick me-2" data-header="@T["Filter by owner"]" data-selected-text-format="static" data-width="fit" data-dropdown-align-right="auto" title="@T["Owner"]" data-style="btn-sm" data-live-search="true"></select>
<select asp-for="OrderBy" asp-items="@Model.ContentSorts" class="selectpicker contentsorts-selectpicker show-tick" data-header="@T["Sort by"]" data-width="fit" data-selected-text-format="static" data-dropdown-align-right="true" title="@T["Sort"]" data-style="btn-sm"></select>
</div>

<script at="Foot">
$(function () {
douwinga marked this conversation as resolved.
Show resolved Hide resolved
var selectedOwnerUserName = '@Model.SelectedOwnerUserName';

// bootstrap-select has an issue with hammering the server, so we use flags to only make a request
// if something changes that would warrant another request
var previousPage;
var previousSearchTerm;

$('#@Html.IdFor(m => m.SelectedOwnerUserName)').selectpicker({
source: {
pageSize: 50,
data: function (callback, page) {
if (previousPage !== page) {
previousPage = page;

$.ajax('@ownerFilterUserSearchUrl', { data: { page, selectedOwnerUserName } })
.then((response) => callback(response.results, response.hasMoreResults));
}
},
search: function (callback, page, searchTerm) {
if (previousPage !== page || previousSearchTerm !== searchTerm) {
previousPage = page;
previousSearchTerm = searchTerm;

$.ajax('@ownerFilterUserSearchUrl', { data: { page, searchTerm, selectedOwnerUserName } })
.then((response) => callback(response.results, response.hasMoreResults));
}
}
}
});
})
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@model ShapeViewModel<ContentOptionsViewModel>
@{
var term = Model.Value.FilterResult.FirstOrDefault(x => x.TermName == "owner");
}

<h4 class="card-title">@T["Owner"]</h4>
<pre class="mb-3">@(term?.ToString() ?? "owner:...")</pre>
<p>@T["Filters on the owner of a content item."]</p>
<div class="d-block text-end">
<span>
<i class="fa-solid fa-sm fa-minus text-primary" aria-hidden="true"></i>
</span>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public ContentOptionsViewModel()

public string SelectedContentType { get; set; }

public string SelectedOwnerUserName { get; set; }

public bool CanCreateSelectedContentType { get; set; }

public ContentsOrder OrderBy { get; set; }
Expand Down