Skip to content

Commit

Permalink
build(sln): v29.3.0.3 - admin extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
seangwright committed Aug 5, 2024
1 parent 9f3af67 commit ffc32de
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 56 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<!-- See https://aka.ms/dotnet/msbuild/customize for more details on customizing your build -->
<PropertyGroup>
<VersionPrefix>29.3.0.2</VersionPrefix>
<VersionPrefix>29.3.0.3</VersionPrefix>
</PropertyGroup>

<PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using CMS.DataEngine;
using CMS.Membership;
using Kentico.Community.Portal.Admin.Features.MemberBadges;
using Kentico.Community.Portal.Admin.UIPages;
using Kentico.Community.Portal.Admin.Features.Members;
using Kentico.Community.Portal.Core.Modules;
using Kentico.Xperience.Admin.Base;

Expand Down Expand Up @@ -31,6 +31,8 @@ public override async Task ConfigurePage()
PageConfiguration.QueryModifiers
.AddModifier((q, settings) =>
{
MemberListFilter.ModifyQueryForBadgeFilter(q);

var ruleAssigned =
new ObjectQuery(MemberBadgeMemberInfo.OBJECT_TYPE)
.Source(s => s.Join<MemberBadgeInfo>(nameof(MemberBadgeMemberInfo.MemberBadgeMemberMemberBadgeId), nameof(MemberBadgeInfo.MemberBadgeID)))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
using CMS.DataEngine;
using CMS.Membership;
using CSharpFunctionalExtensions;
using Kentico.Community.Portal.Admin.UIPages;
using Kentico.Community.Portal.Admin.Features.Members;
using Kentico.Xperience.Admin.Base;
using Kentico.Xperience.Admin.Base.Filters;
using Kentico.Xperience.Admin.Base.FormAnnotations;
using Kentico.Xperience.Admin.Base.UIPages;

[assembly: PageExtender(typeof(MemberListExtender))]

namespace Kentico.Community.Portal.Admin.UIPages;
namespace Kentico.Community.Portal.Admin.Features.Members;

public class MemberListExtender : PageExtender<MemberList>
{
public override async Task ConfigurePage()
{
await base.ConfigurePage();

Page.PageConfiguration.QueryModifiers.AddModifier((q, settings) =>
MemberListFilter.ModifyQueryForBadgeFilter(q));

var configs = Page.PageConfiguration.ColumnConfigurations
.AddColumn("MemberFirstName", caption: "First name")
.AddColumn("MemberLastName", caption: "Last name");
Expand Down Expand Up @@ -46,43 +46,3 @@ public override async Task ConfigurePage()
}
}
}

public class MemberListFilter
{
[DropDownComponent(
Label = "Status",
Options = "Enabled\r\nDisabled")]
[FilterCondition(
BuilderType = typeof(MemberStatusWhereConditionBuilder),
ColumnName = nameof(MemberInfo.MemberEnabled)
)]
public string Status { get; set; } = "";
}

public class MemberStatusWhereConditionBuilder : IWhereConditionBuilder
{
public Task<IWhereCondition> Build(string columnName, object value)
{
if (string.IsNullOrEmpty(columnName))
{
throw new ArgumentException(
$"{nameof(columnName)} cannot be a null or an empty string.");
}

var whereCondition = new WhereCondition();

if (value is null || value is not string status)
{
return Task.FromResult<IWhereCondition>(whereCondition);
}

whereCondition = status switch
{
"Disabled" => new WhereCondition().WhereEquals(nameof(MemberInfo.MemberEnabled), 0),
"Enabled" => new WhereCondition().WhereEquals(nameof(MemberInfo.MemberEnabled), 1),
"" or _ => new WhereCondition("1 = 1"),
};

return Task.FromResult<IWhereCondition>(whereCondition);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
using CMS.DataEngine;
using CMS.Membership;
using CSharpFunctionalExtensions;
using Kentico.Community.Portal.Core.Modules;
using Kentico.Xperience.Admin.Base.Filters;
using Kentico.Xperience.Admin.Base.FormAnnotations;
using Kentico.Xperience.Admin.Base.Forms;

namespace Kentico.Community.Portal.Admin.Features.Members;

/// <summary>
/// Applies more complex filter conditions to member listing
/// </summary>
/// <remarks>
/// Should be used with <see cref="ModifyQueryForBadgeFilter"/> applied to the listing query
/// </remarks>
public class MemberListFilter
{
[DropDownComponent(
Label = "Status",
Options = "Enabled\r\nDisabled")]
[FilterCondition(
BuilderType = typeof(MemberStatusWhereConditionBuilder),
ColumnName = nameof(MemberInfo.MemberEnabled)
)]
public string Status { get; set; } = "";

[GeneralSelectorComponent(
dataProviderType: typeof(MemberBadgeGeneralSelectorDataProvider),
Label = "Badges",
Placeholder = "Any",
Order = 0
)]
[FilterCondition(
BuilderType = typeof(MemberBadgeWhereConditionBuilder),
ColumnName = nameof(MemberBadgeInfo.MemberBadgeCodeName)
)]
public IEnumerable<string> MemberBadges { get; set; } = [];

public static ObjectQuery ModifyQueryForBadgeFilter(ObjectQuery query) =>
query.Source(source =>
source
.LeftJoin(
// Ideally we'd apply this with a HAVING or CTE,
// but the filter query IWhereConditionBuilders only have access
// to the WHERE part of the SQL statement.
// A sub-query allows us to filter the results in the WHERE
sourceExpression: """
(SELECT
M.MemberID as Sub_MemberID,
STRING_AGG(B.MemberBadgeCodeName, ',') AS BadgeList
FROM
CMS_Member M
LEFT JOIN
KenticoCommunity_MemberBadgeMember MBM ON M.MemberID = MBM.MemberBadgeMemberMemberId
LEFT JOIN
KenticoCommunity_MemberBadge B ON MBM.MemberBadgeMemberMemberBadgeId = B.MemberBadgeID
GROUP BY
M.MemberID) AS Badges
""",
condition: "Badges.Sub_MemberID = CMS_Member.MemberID"))
.AddColumn(new QueryColumn("ISNULL(Badges.BadgeList, '')") { ColumnAlias = "BadgeList" });

}

public class MemberStatusWhereConditionBuilder : IWhereConditionBuilder
{
public Task<IWhereCondition> Build(string columnName, object value)
{
if (string.IsNullOrEmpty(columnName))
{
throw new ArgumentException(
$"{nameof(columnName)} cannot be a null or an empty string.");
}

var whereCondition = new WhereCondition();

if (value is null || value is not string status)
{
return Task.FromResult<IWhereCondition>(whereCondition);
}

whereCondition = status switch
{
"Disabled" => new WhereCondition().WhereEquals(nameof(MemberInfo.MemberEnabled), 0),
"Enabled" => new WhereCondition().WhereEquals(nameof(MemberInfo.MemberEnabled), 1),
"" or _ => new WhereCondition("1 = 1"),
};

return Task.FromResult<IWhereCondition>(whereCondition);
}
}

public class MemberBadgeGeneralSelectorDataProvider(IMemberBadgeInfoProvider badgeProvider)
: IGeneralSelectorDataProvider
{
private static ObjectSelectorListItem<string> InvalidItem => new() { IsValid = false, Text = "Inavlid", Value = "" };
private readonly IMemberBadgeInfoProvider badgeProvider = badgeProvider;

public async Task<PagedSelectListItems<string>> GetItemsAsync(string searchTerm, int pageIndex, CancellationToken cancellationToken)
{
var badges = (await badgeProvider.GetAllMemberBadgesCached()).AsEnumerable();

if (!string.IsNullOrEmpty(searchTerm))
{
badges = badges.Where(i => i.MemberBadgeDisplayName.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));
}

return new PagedSelectListItems<string>()
{
NextPageAvailable = false,
Items = badges.Select(b => new ObjectSelectorListItem<string> { IsValid = true, Text = b.MemberBadgeDisplayName, Value = b.MemberBadgeCodeName }),
};
}

public async Task<IEnumerable<ObjectSelectorListItem<string>>> GetSelectedItemsAsync(IEnumerable<string> selectedValues, CancellationToken cancellationToken)
{
var badges = await badgeProvider.GetAllMemberBadgesCached();

return (selectedValues ?? []).Select(GetSelectedItemByValue(badges));
}

private static Func<string, ObjectSelectorListItem<string>> GetSelectedItemByValue(IEnumerable<MemberBadgeInfo> badges) =>
(string badgeName) => badges
.TryFirst(b => string.Equals(b.MemberBadgeCodeName, badgeName, StringComparison.OrdinalIgnoreCase))
.Map(b => new ObjectSelectorListItem<string> { IsValid = true, Text = b.MemberBadgeDisplayName, Value = b.MemberBadgeCodeName })
.GetValueOrDefault(InvalidItem);
}

/// <summary>
/// Used with <see cref="MemberListFilter.ModifyQueryForBadgeFilter"/> to
/// filter member results by the badges assigned to members
/// </summary>
public class MemberBadgeWhereConditionBuilder : IWhereConditionBuilder
{
public Task<IWhereCondition> Build(string columnName, object value)
{
if (string.IsNullOrEmpty(columnName))
{
throw new ArgumentException(
$"{nameof(columnName)} cannot be a null or an empty string.");
}

var whereCondition = new WhereCondition();

if (value is null || value is not IEnumerable<string> memberBadges)
{
return Task.FromResult<IWhereCondition>(whereCondition);
}

string badgesToFilter = string.Join(",", memberBadges);

string query = $"""
SELECT COUNT(*)
FROM STRING_SPLIT(@BadgesToFilter, ',') badge
WHERE ISNULL(badges.BadgeList, '') LIKE '%' + badge.value + '%')
= (SELECT COUNT(*)
FROM STRING_SPLIT(@BadgesToFilter, ',')
""";

_ = whereCondition.Where(query, new QueryDataParameters
{
{ "BadgesToFilter", badgesToFilter }
});

return Task.FromResult<IWhereCondition>(whereCondition);
}
}
16 changes: 9 additions & 7 deletions src/Kentico.Community.Portal.Admin/UIPages/PreviewTabExtender.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using CMS.ContentEngine;
using CMS.DataEngine;
using CMS.Websites;
using CMS.Websites.Internal;
using Kentico.Community.Portal.Admin.UIPages;
Expand All @@ -19,11 +18,11 @@ namespace Kentico.Community.Portal.Admin.UIPages;
/// <param name="infoProvider"></param>
/// <param name="contextAccessor"></param>
/// <param name="queryExecutor"></param>
public class PreviewTabExtender(IWebPageUrlRetriever urlRetriever, IInfoProvider<WebsiteChannelInfo> infoProvider, IHttpContextAccessor contextAccessor, IContentQueryExecutor queryExecutor)
public class PreviewTabExtender(IWebPageUrlRetriever urlRetriever, IWebsiteChannelDomainProvider domainProvider, IHttpContextAccessor contextAccessor, IContentQueryExecutor queryExecutor)
: PageExtender<PreviewTab>
{
private readonly IWebPageUrlRetriever urlRetriever = urlRetriever;
private readonly IInfoProvider<WebsiteChannelInfo> infoProvider = infoProvider;
private readonly IWebsiteChannelDomainProvider domainProvider = domainProvider;
private readonly IHttpContextAccessor contextAccessor = contextAccessor;
private readonly IContentQueryExecutor queryExecutor = queryExecutor;

Expand Down Expand Up @@ -58,19 +57,22 @@ clientProps.WebPageState.MenuActions is not IReadOnlyCollection<IContentItemActi
? relativeUrl[1..]
: relativeUrl;

var channel = await infoProvider.GetAsync(Page.ApplicationIdentifier.WebsiteChannelID);
string domain = await domainProvider.GetDomain(Page.ApplicationIdentifier.WebsiteChannelID);
string fullUrl = UriHelper.BuildAbsolute(
scheme: contextAccessor.HttpContext!.Request.Scheme,
host: new HostString(channel.WebsiteChannelDomain),
host: new HostString(domain),
pathBase: "",
path: relativeUrl);

mutableActions.Add(new ContentItemAction
{
Name = "OpenPublishedPage",
Label = "Open Published Page",
Label = "↗️ Open Published Page",
Tooltip = "Opens the current page in a new tab",
Permission = WebPageAclPermissions.DISPLAY,
Url = fullUrl
Url = fullUrl,
Icon = Icons.ArrowRightTopSquare,
ButtonColor = ButtonColor.Primary,
});

return props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ public override async Task ConfigurePage()
.TryFirst(c => string.Equals(c.Name, nameof(ReusableFieldSchema.Name), StringComparison.OrdinalIgnoreCase))
.Execute(c => c.MaxWidth = 35);

_ = configs.AddColumn("usedByContentTypes", "Used by", minWidth: 50, maxWidth: 100, formatter: (data, dc) =>
_ = configs.AddColumn("usedByContentTypes", "Used by (Content Types)", minWidth: 50, maxWidth: 100, formatter: (data, dc) =>
{
var schemaGUID = ValidationHelper.GetGuid(dc[nameof(ReusableFieldSchema.Guid)], default);
var contentTypes = schemaManager.GetContentTypesWithSchema(schemaGUID) ?? [];

return string.Join(", ", contentTypes);
return contentTypes.Count().ToString();
});
}
}

0 comments on commit ffc32de

Please sign in to comment.