From 7bce3375c6e526aab2fb72d3449028e130791be3 Mon Sep 17 00:00:00 2001
From: "Sean G. Wright"
Date: Mon, 11 Nov 2024 17:05:47 -0500
Subject: [PATCH] feat(sln): v29.6.1.13 - blog and Q&A search UX, email channel
member emails
---
Directory.Build.props | 2 +-
.../Members/MemberQAndAAnswersListPage.cs | 3 +-
.../Members/MemberQAndAQuestionsListPage.cs | 3 +-
.../Features/QAndA/QAndAListingPage.cs | 3 +-
.../Infrastructure/CollectionExtensions.cs | 14 +
.../QueryCachingPipelineBehavior.cs | 13 +-
.../App_Data/CDRepository/repository.config | 17 +-
.../kenticocommunity.autoresponderemail.xml | 2 +-
.../kenticocommunity.newsletteremail.xml | 4 +-
.../kenticocommunity.qandaquestionpage.xml | 7 +-
.../cms.scheduledtask/licensereporter.xml | 4 +-
...registrationemailconfirmation-6wjw8hge.xml | 17 ++
...mberresetpasswordconfirmation-ahw9v8cj.xml | 17 ++
...6f-45e2..e214d40d7041_en-us@4a24d0ed7c.xml | 36 +++
...f1-49da..ff4280145266_en-us@7b1717e018.xml | 36 +++
...a9-4401..06bc3da8f00d_en-us@94baa41103.xml | 28 ++
...5e-450d..a1e019b9efce_en-us@aaf2102e2d.xml | 28 ++
.../06f2860c-0158-449d-8386-465d0cf6c6ab.xml | 44 ++++
.../61b13ff8-be07-4f15-8674-c26b0ff3dff2.xml | 44 ++++
...registrationemailconfirmation-6wjw8hge.xml | 20 ++
...mberresetpasswordconfirmation-ahw9v8cj.xml | 20 ++
...44-4c85..e779daf3de55_en-us@03af2e1a83.xml | 2 +-
...61-4f22..9c12b5f3d4cd_en-us@d95d1e4367.xml | 2 +-
...70-4071..01a731c8abeb_en-us@9133e0fa1c.xml | 2 +-
...ca-45ca..6364a77af46e_en-us@b151c4c24e.xml | 44 +++-
...2b-4bb0..182a7e9a5be9_en-us@d144aacbcc.xml | 36 ++-
...be-46c1..6a5cefbfd333_en-us@5ebcec0a9e.xml | 36 ++-
.../75732d76-77c9-4454-a9c1-11f2cda4a4e4.xml | 3 +
.../b35fb74d-e802-4169-afd3-a8f580fdccfd.xml | 5 +-
.../b85c6b9e-4134-460e-bf74-ec6b93cdf4af.xml | 3 +
.../Client/js/features/search.js | 103 +++-----
.../Client/styles/00_core/_variables.scss | 3 +
.../BlogPostList/BlogPostListWidget.cs | 27 ++
.../BlogPostListWidgetViewModel.cs | 42 ---
.../Configuration/GlobalEventsModule.cs | 8 +
.../ServiceCollectionAppExtensions.cs | 4 +-
.../ServiceCollectionMembershipExtensions.cs | 4 +-
.../Features/Blog/BlogPostViewModel.cs | 14 -
.../Blog/Components/BlogSearch.cshtml | 107 --------
.../Components/BlogSearchViewComponent.cs | 59 -----
.../Operations/BlogPostTaxonomiesQuery.cs | 40 ++-
.../Features/Blog/Search/BlogSearch.cshtml | 151 +++++++++++
.../Blog/Search/BlogSearchIndexModel.cs | 137 ++++------
.../Features/Blog/Search/BlogSearchService.cs | 154 ++++++-----
.../Blog/Search/BlogSearchViewComponent.cs | 120 +++++++++
.../Members/Badges/MemberBadgeService.cs | 2 +-
.../Features/Members/MemberEmailService.cs | 148 +++++++++++
.../PasswordRecoveryController.cs | 35 +--
.../Components/Search/QAndASearch.cshtml | 143 -----------
.../Search/QAndASearchViewComponent.cs | 103 --------
.../QAndAQuestionPageTaxonomiesQuery.cs | 37 ---
.../QAndA/Operations/QAndATaxonomiesQuery.cs | 56 ++++
.../QAndA/QAndAQuestionPageController.cs | 5 +-
.../Features/QAndA/Search/QAndASearch.cshtml | 204 +++++++++++++++
.../QAndA/Search/QAndASearchIndexModel.cs | 93 ++++---
.../QAndA/Search/QAndASearchService.cs | 168 +++++++-----
.../QAndA/Search/QAndASearchViewComponent.cs | 136 ++++++++++
.../Registration/RegistrationController.cs | 29 +--
.../Features/SEO/RSSFeedController.cs | 2 +-
.../Support/Components/SupportForm.cshtml | 3 +-
.../Infrastructure/Search/FacetOption.cs | 3 +-
.../package-lock.json | 242 +++++++++++-------
src/Kentico.Community.Portal.Web/package.json | 6 +-
63 files changed, 1862 insertions(+), 1021 deletions(-)
create mode 100644 src/Kentico.Community.Portal.Core/Infrastructure/CollectionExtensions.cs
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberregistrationemailconfirmation-6wjw8hge.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberresetpasswordconfirmation-ahw9v8cj.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberregistration..firmation-6wjw8hge@05bb3aa664/276a786f-446f-45e2..e214d40d7041_en-us@4a24d0ed7c.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/9ba20fad-01f1-49da..ff4280145266_en-us@7b1717e018.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberregistration..firmation-6wjw8hge@05bb3aa664/2c19ca5e-48a9-4401..06bc3da8f00d_en-us@94baa41103.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/99529439-fd5e-450d..a1e019b9efce_en-us@aaf2102e2d.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberregistration..e214d40d7041_en-us@1a9f5e14bb/06f2860c-0158-449d-8386-465d0cf6c6ab.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberresetpasswor..ff4280145266_en-us@4230cd9423/61b13ff8-be07-4f15-8674-c26b0ff3dff2.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberregistrationemailconfirmation-6wjw8hge.xml
create mode 100644 src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberresetpasswordconfirmation-ahw9v8cj.xml
delete mode 100644 src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidgetViewModel.cs
delete mode 100644 src/Kentico.Community.Portal.Web/Features/Blog/BlogPostViewModel.cs
delete mode 100644 src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearch.cshtml
delete mode 100644 src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearchViewComponent.cs
create mode 100644 src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearch.cshtml
create mode 100644 src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchViewComponent.cs
create mode 100644 src/Kentico.Community.Portal.Web/Features/Members/MemberEmailService.cs
delete mode 100644 src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearch.cshtml
delete mode 100644 src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearchViewComponent.cs
delete mode 100644 src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndAQuestionPageTaxonomiesQuery.cs
create mode 100644 src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndATaxonomiesQuery.cs
create mode 100644 src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearch.cshtml
create mode 100644 src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchViewComponent.cs
diff --git a/Directory.Build.props b/Directory.Build.props
index 1b8d2d39..6662317d 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,7 +1,7 @@
- 29.6.1.10
+ 29.6.1.13
diff --git a/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAAnswersListPage.cs b/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAAnswersListPage.cs
index 86a20b48..10bdf2c5 100644
--- a/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAAnswersListPage.cs
+++ b/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAAnswersListPage.cs
@@ -9,6 +9,7 @@
using Kentico.Community.Portal.Core.Modules;
using Kentico.Xperience.Admin.Base;
using Kentico.Xperience.Admin.Base.UIPages;
+using Kentico.Xperience.Admin.Websites;
using Kentico.Xperience.Admin.Websites.UIPages;
[assembly: UIPage(
@@ -156,7 +157,7 @@ private TableRowLinkProps QuestionLinkModelRetriever(object value, IDataContaine
string pageUrl = pageLinkGenerator.GetPath(new()
{
- { typeof(WebPageLayout), $"{PortalWebSiteChannel.DEFAULT_LANGUAGE}_{webPageItemID}" },
+ { typeof(WebPageLayout), new WebPageUrlIdentifier(PortalWebSiteChannel.DEFAULT_LANGUAGE, webPageItemID) },
{ typeof(WebPagesApplication), $"webpages-{websiteChannelID}" },
});
diff --git a/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAQuestionsListPage.cs b/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAQuestionsListPage.cs
index d2dd0b78..ac47655f 100644
--- a/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAQuestionsListPage.cs
+++ b/src/Kentico.Community.Portal.Admin/Features/Members/MemberQAndAQuestionsListPage.cs
@@ -9,6 +9,7 @@
using Kentico.Community.Portal.Core.Content;
using Kentico.Xperience.Admin.Base;
using Kentico.Xperience.Admin.Base.UIPages;
+using Kentico.Xperience.Admin.Websites;
using Kentico.Xperience.Admin.Websites.UIPages;
[assembly: UIPage(
@@ -100,7 +101,7 @@ private TableRowLinkProps QuestionPageLinkModelRetriever(object value, IDataCont
string pageUrl = pageLinkGenerator.GetPath(new()
{
- { typeof(WebPageLayout), $"{PortalWebSiteChannel.DEFAULT_LANGUAGE}_{webPageItemID}" },
+ { typeof(WebPageLayout), new WebPageUrlIdentifier(PortalWebSiteChannel.DEFAULT_LANGUAGE, webPageItemID) },
{ typeof(WebPagesApplication), $"webpages-{websiteChannelID}" },
});
diff --git a/src/Kentico.Community.Portal.Admin/Features/QAndA/QAndAListingPage.cs b/src/Kentico.Community.Portal.Admin/Features/QAndA/QAndAListingPage.cs
index 9885e827..82b716b7 100644
--- a/src/Kentico.Community.Portal.Admin/Features/QAndA/QAndAListingPage.cs
+++ b/src/Kentico.Community.Portal.Admin/Features/QAndA/QAndAListingPage.cs
@@ -6,6 +6,7 @@
using Kentico.Community.Portal.Core;
using Kentico.Community.Portal.Core.Modules;
using Kentico.Xperience.Admin.Base;
+using Kentico.Xperience.Admin.Websites;
using Kentico.Xperience.Admin.Websites.UIPages;
[assembly: UIPage(
@@ -100,7 +101,7 @@ private TableRowLinkProps QuestionPageLinkModelRetriever(object value, IDataCont
string pageUrl = pageLinkGenerator.GetPath(new()
{
- { typeof(WebPageLayout), $"{PortalWebSiteChannel.DEFAULT_LANGUAGE}_{webPageItemID}" },
+ { typeof(WebPageLayout), new WebPageUrlIdentifier(PortalWebSiteChannel.DEFAULT_LANGUAGE, webPageItemID) },
{ typeof(WebPagesApplication), $"webpages-{websiteChannelID}" },
});
diff --git a/src/Kentico.Community.Portal.Core/Infrastructure/CollectionExtensions.cs b/src/Kentico.Community.Portal.Core/Infrastructure/CollectionExtensions.cs
new file mode 100644
index 00000000..612250fc
--- /dev/null
+++ b/src/Kentico.Community.Portal.Core/Infrastructure/CollectionExtensions.cs
@@ -0,0 +1,14 @@
+using System.Diagnostics.Contracts;
+
+namespace System.Linq;
+
+public static class CollectionExtensions
+{
+ [Pure]
+ public static IEnumerable WhereNotNull(this IEnumerable o) where T : class =>
+ o.Where(x => x != null)!;
+
+ [Pure]
+ public static IEnumerable WHereNotNull(this IEnumerable enumerable) where T : struct =>
+ enumerable.Where(e => e.HasValue).Select(e => e!.Value);
+}
diff --git a/src/Kentico.Community.Portal.Core/Operations/QueryCachingPipelineBehavior.cs b/src/Kentico.Community.Portal.Core/Operations/QueryCachingPipelineBehavior.cs
index cce13431..68483d1a 100644
--- a/src/Kentico.Community.Portal.Core/Operations/QueryCachingPipelineBehavior.cs
+++ b/src/Kentico.Community.Portal.Core/Operations/QueryCachingPipelineBehavior.cs
@@ -1,6 +1,7 @@
using CMS.Helpers;
using CMS.Websites.Routing;
using MediatR;
+using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
namespace Kentico.Community.Portal.Core.Operations;
@@ -15,7 +16,8 @@ public class QueryCachingPipelineBehavior(
IEnumerable> creators,
IEnumerable> cacheCustomizers,
IWebsiteChannelContext channelContext,
- IOptions settings) : IPipelineBehavior
+ IOptions settings,
+ IHttpContextAccessor contextAccessor) : IPipelineBehavior
where TQuery : IQuery
{
private readonly IProgressiveCache cache = cache;
@@ -26,16 +28,23 @@ public class QueryCachingPipelineBehavior(
private readonly IEnumerable> creators = creators;
private readonly IEnumerable> cacheCustomizers = cacheCustomizers;
private readonly IWebsiteChannelContext channelContext = channelContext;
+ private readonly IHttpContextAccessor contextAccessor = contextAccessor;
private readonly DefaultQueryCacheSettings settings = settings.Value;
public async Task Handle(TQuery query, RequestHandlerDelegate next, CancellationToken cancellationToken)
{
var creator = creators.FirstOrDefault();
+ /*
+ * Guard against IWebsiteChannelContext.IsPreview throwing an exception
+ * when no HttpContext is available (e.g. background process)
+ */
+ bool isPreview = contextAccessor.HttpContext is not null && channelContext.IsPreview;
+
/*
* Skip caching is we are in preview mode, caching is disabled, or we cannot generate cache keys for the current query
*/
- if (channelContext.IsPreview ||
+ if (isPreview ||
!this.settings.IsEnabled ||
creator is null)
{
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CDRepository/repository.config b/src/Kentico.Community.Portal.Web/App_Data/CDRepository/repository.config
index 0b816c1b..5416fa9b 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CDRepository/repository.config
+++ b/src/Kentico.Community.Portal.Web/App_Data/CDRepository/repository.config
@@ -13,12 +13,27 @@
-->
+ cms.class
cms.contenttype
+ cms.contentitem
+ cms.contentitemcommondata
+ cms.contentitemlanguagemetadata
+ emaillibrary.emailconfiguration
- placeholder;
+ kenticocommunity.qandaquestionpage;
+ kenticocommunity.autoresponderemail;
+
+
+
+ MemberRegistrationEmailConfirmation-6wjw8hge;
+ MemberResetPasswordConfirmation-ahw9v8cj;
+
+
+
+ CMS.ContentItemCommonData
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.autoresponderemail.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.autoresponderemail.xml
index a4825dbf..b3bc1bdc 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.autoresponderemail.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.autoresponderemail.xml
@@ -45,7 +45,7 @@
Kentico.Administration.TextInput
-
+
False
Content
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.newsletteremail.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.newsletteremail.xml
index 77dfd7a2..53db7bb4 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.newsletteremail.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.newsletteremail.xml
@@ -66,7 +66,7 @@
Kentico.Administration.DateInput
-
+
False
Content
@@ -108,7 +108,7 @@
contentTypes
-
+
False
Outro Content
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.qandaquestionpage.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.qandaquestionpage.xml
index a1e10306..4954cc1a 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.qandaquestionpage.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.contenttype/kenticocommunity.qandaquestionpage.xml
@@ -58,14 +58,15 @@
0
+
+
+
False
Author
False
- Kentico.Administration.SingleObjectIdSelector
- CMS.Member
- 0
+ Kentico.Administration.NumberInput
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.scheduledtask/licensereporter.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.scheduledtask/licensereporter.xml
index c01e7305..5ededa2f 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.scheduledtask/licensereporter.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/@global/cms.scheduledtask/licensereporter.xml
@@ -2,13 +2,13 @@
CMS.LicenseProvider
CMS.LicenseProvider.LicenseReporter
-
+ 1
False
Periodically contacts license server
True
d82be54b-d55b-4aae-bd3d-3ef859a312fb
-
+
LicenseReporter
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberregistrationemailconfirmation-6wjw8hge.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberregistrationemailconfirmation-6wjw8hge.xml
new file mode 100644
index 00000000..d82a05db
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberregistrationemailconfirmation-6wjw8hge.xml
@@ -0,0 +1,17 @@
+
+
+
+ KenticoCommunityEmails
+ 1b41b848-ddd2-4a8d-9bc7-850a93e147c4
+ cms.channel
+
+
+ KenticoCommunity.AutoresponderEmail
+ 70764325-6007-438e-93fe-dc319714c7a6
+ cms.contenttype
+
+ a00b9bc8-fd48-4ba9-a57b-dea12798e23b
+ False
+ False
+ MemberRegistrationEmailConfirmation-6wjw8hge
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberresetpasswordconfirmation-ahw9v8cj.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberresetpasswordconfirmation-ahw9v8cj.xml
new file mode 100644
index 00000000..72b5c433
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitem/memberresetpasswordconfirmation-ahw9v8cj.xml
@@ -0,0 +1,17 @@
+
+
+
+ KenticoCommunityEmails
+ 1b41b848-ddd2-4a8d-9bc7-850a93e147c4
+ cms.channel
+
+
+ KenticoCommunity.AutoresponderEmail
+ 70764325-6007-438e-93fe-dc319714c7a6
+ cms.contenttype
+
+ 4eb4da09-d82b-4224-92be-f8bfa76d5a94
+ False
+ False
+ MemberResetPasswordConfirmation-ahw9v8cj
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberregistration..firmation-6wjw8hge@05bb3aa664/276a786f-446f-45e2..e214d40d7041_en-us@4a24d0ed7c.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberregistration..firmation-6wjw8hge@05bb3aa664/276a786f-446f-45e2..e214d40d7041_en-us@4a24d0ed7c.xml
new file mode 100644
index 00000000..37221e8c
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberregistration..firmation-6wjw8hge@05bb3aa664/276a786f-446f-45e2..e214d40d7041_en-us@4a24d0ed7c.xml
@@ -0,0 +1,36 @@
+
+
+
+ MemberRegistrationEmailConfirmation-6wjw8hge
+ a00b9bc8-fd48-4ba9-a57b-dea12798e23b
+ cms.contentitem
+
+
+ en-US
+ 6c743a9e-8a63-425b-bef6-756c12c1bbf5
+ cms.contentlanguage
+
+ 2024-11-06 20:26:45Z
+ 276a786f-446f-45e2-a7da-e214d40d7041
+ True
+ 2024-11-11 13:56:32Z
+ 2
+
+
+
+
+
+ f8e8d7f5-688a-46d3-9d98-ae157e3e32fd
+ 45fcfad2-ea4d-47be-9fb4-3e4347051cdd
+
+ 276a786f-446f-45e2-a7da-e214d40d7041
+ cms.contentitemcommondata
+
+
+ KenticoCommunityEmailDefaults-1do1ulq8
+ a0222552-0368-4784-9661-fd5660bfe492
+ cms.contentitem
+
+
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/9ba20fad-01f1-49da..ff4280145266_en-us@7b1717e018.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/9ba20fad-01f1-49da..ff4280145266_en-us@7b1717e018.xml
new file mode 100644
index 00000000..982a4e18
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemcommondata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/9ba20fad-01f1-49da..ff4280145266_en-us@7b1717e018.xml
@@ -0,0 +1,36 @@
+
+
+
+ MemberResetPasswordConfirmation-ahw9v8cj
+ 4eb4da09-d82b-4224-92be-f8bfa76d5a94
+ cms.contentitem
+
+
+ en-US
+ 6c743a9e-8a63-425b-bef6-756c12c1bbf5
+ cms.contentlanguage
+
+ 2024-11-11 13:59:37Z
+ 9ba20fad-01f1-49da-bfa9-ff4280145266
+ True
+ 2024-11-11 13:59:37Z
+ 2
+
+
+
+
+
+ f8e8d7f5-688a-46d3-9d98-ae157e3e32fd
+ 9aba55ca-d130-4dcc-b647-3e15aff9bd30
+
+ 9ba20fad-01f1-49da-bfa9-ff4280145266
+ cms.contentitemcommondata
+
+
+ KenticoCommunityEmailDefaults-1do1ulq8
+ a0222552-0368-4784-9661-fd5660bfe492
+ cms.contentitem
+
+
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberregistration..firmation-6wjw8hge@05bb3aa664/2c19ca5e-48a9-4401..06bc3da8f00d_en-us@94baa41103.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberregistration..firmation-6wjw8hge@05bb3aa664/2c19ca5e-48a9-4401..06bc3da8f00d_en-us@94baa41103.xml
new file mode 100644
index 00000000..ea169d61
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberregistration..firmation-6wjw8hge@05bb3aa664/2c19ca5e-48a9-4401..06bc3da8f00d_en-us@94baa41103.xml
@@ -0,0 +1,28 @@
+
+
+
+ MemberRegistrationEmailConfirmation-6wjw8hge
+ a00b9bc8-fd48-4ba9-a57b-dea12798e23b
+ cms.contentitem
+
+
+ en-US
+ 6c743a9e-8a63-425b-bef6-756c12c1bbf5
+ cms.contentlanguage
+
+
+ SeanW_kentico.com
+ d6a0d252-e67a-4090-8610-1a9f9b6a2cb3
+ cms.user
+
+ 2024-11-06 20:23:25Z
+ Member registration email confirmation
+ 2c19ca5e-48a9-4401-8c59-06bc3da8f00d
+ False
+ 2
+
+ SeanW_kentico.com
+ d6a0d252-e67a-4090-8610-1a9f9b6a2cb3
+ cms.user
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/99529439-fd5e-450d..a1e019b9efce_en-us@aaf2102e2d.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/99529439-fd5e-450d..a1e019b9efce_en-us@aaf2102e2d.xml
new file mode 100644
index 00000000..64d35b6e
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/cms.contentitemlanguagemetadata/memberresetpasswor..firmation-ahw9v8cj@69d7c8613a/99529439-fd5e-450d..a1e019b9efce_en-us@aaf2102e2d.xml
@@ -0,0 +1,28 @@
+
+
+
+ MemberResetPasswordConfirmation-ahw9v8cj
+ 4eb4da09-d82b-4224-92be-f8bfa76d5a94
+ cms.contentitem
+
+
+ en-US
+ 6c743a9e-8a63-425b-bef6-756c12c1bbf5
+ cms.contentlanguage
+
+
+ SeanW_kentico.com
+ d6a0d252-e67a-4090-8610-1a9f9b6a2cb3
+ cms.user
+
+ 2024-11-07 23:08:02Z
+ Member reset password confirmation
+ 99529439-fd5e-450d-9545-a1e019b9efce
+ False
+ 2
+
+ SeanW_kentico.com
+ d6a0d252-e67a-4090-8610-1a9f9b6a2cb3
+ cms.user
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberregistration..e214d40d7041_en-us@1a9f5e14bb/06f2860c-0158-449d-8386-465d0cf6c6ab.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberregistration..e214d40d7041_en-us@1a9f5e14bb/06f2860c-0158-449d-8386-465d0cf6c6ab.xml
new file mode 100644
index 00000000..c8686184
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberregistration..e214d40d7041_en-us@1a9f5e14bb/06f2860c-0158-449d-8386-465d0cf6c6ab.xml
@@ -0,0 +1,44 @@
+
+
+
+ To confirm your email address, click here.
You can also copy and paste this URL into your browser.
TOKEN_ConfirmationURL
]]>
+
+ Confirm your email
+
+
+
+
+ 276a786f-446f-45e2-a7da-e214d40d7041
+ cms.contentitemcommondata
+
+ MemberRegistrationEmailConfirmation-6wjw8hge
+ a00b9bc8-fd48-4ba9-a57b-dea12798e23b
+ cms.contentitem
+
+
+ 06f2860c-0158-449d-8386-465d0cf6c6ab
+
+ Confirm your email to complete registration
+
+ no-reply
+ 24520bc4-cdda-401d-812a-e5a73699edac
+ emaillibrary.emailchannelsender
+
+ 2b0fe19c-6251-455f-8233-407a1303b541
+ emaillibrary.emailchannel
+
+ KenticoCommunityEmails
+ 1b41b848-ddd2-4a8d-9bc7-850a93e147c4
+ cms.channel
+
+
+
+
+
+
+
+ CommunityAutoresponder
+ 7e959dee-df06-42b5-a033-0daabea045c7
+ emaillibrary.emailtemplate
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberresetpasswor..ff4280145266_en-us@4230cd9423/61b13ff8-be07-4f15-8674-c26b0ff3dff2.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberresetpasswor..ff4280145266_en-us@4230cd9423/61b13ff8-be07-4f15-8674-c26b0ff3dff2.xml
new file mode 100644
index 00000000..25e5f593
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/contentitemdata.ke..autoresponderemail@79a961133c/memberresetpasswor..ff4280145266_en-us@4230cd9423/61b13ff8-be07-4f15-8674-c26b0ff3dff2.xml
@@ -0,0 +1,44 @@
+
+
+
+ To reset your account's password, click here.You can also copy and paste this URL into your browser.
TOKEN_PasswordResetURL
]]>
+
+ Reset your password
+
+
+
+
+ 9ba20fad-01f1-49da-bfa9-ff4280145266
+ cms.contentitemcommondata
+
+ MemberResetPasswordConfirmation-ahw9v8cj
+ 4eb4da09-d82b-4224-92be-f8bfa76d5a94
+ cms.contentitem
+
+
+ 61b13ff8-be07-4f15-8674-c26b0ff3dff2
+
+ Password reset request
+
+ no-reply
+ 24520bc4-cdda-401d-812a-e5a73699edac
+ emaillibrary.emailchannelsender
+
+ 2b0fe19c-6251-455f-8233-407a1303b541
+ emaillibrary.emailchannel
+
+ KenticoCommunityEmails
+ 1b41b848-ddd2-4a8d-9bc7-850a93e147c4
+ cms.channel
+
+
+
+
+
+
+
+ CommunityAutoresponder
+ 7e959dee-df06-42b5-a033-0daabea045c7
+ emaillibrary.emailtemplate
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberregistrationemailconfirmation-6wjw8hge.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberregistrationemailconfirmation-6wjw8hge.xml
new file mode 100644
index 00000000..446c62ee
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberregistrationemailconfirmation-6wjw8hge.xml
@@ -0,0 +1,20 @@
+
+
+
+ MemberRegistrationEmailConfirmation-6wjw8hge
+ a00b9bc8-fd48-4ba9-a57b-dea12798e23b
+ cms.contentitem
+
+
+ 2b0fe19c-6251-455f-8233-407a1303b541
+ emaillibrary.emailchannel
+
+ KenticoCommunityEmails
+ 1b41b848-ddd2-4a8d-9bc7-850a93e147c4
+ cms.channel
+
+
+ 930816ee-3e1e-4481-a5fa-3d7e15a4b079
+ MemberRegistrationEmailConfirmation-6wjw8hge
+ FormAutoresponder
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberresetpasswordconfirmation-ahw9v8cj.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberresetpasswordconfirmation-ahw9v8cj.xml
new file mode 100644
index 00000000..22289db9
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/KenticoCommunityEmails/emaillibrary.emailconfiguration/memberresetpasswordconfirmation-ahw9v8cj.xml
@@ -0,0 +1,20 @@
+
+
+
+ MemberResetPasswordConfirmation-ahw9v8cj
+ 4eb4da09-d82b-4224-92be-f8bfa76d5a94
+ cms.contentitem
+
+
+ 2b0fe19c-6251-455f-8233-407a1303b541
+ emaillibrary.emailchannel
+
+ KenticoCommunityEmails
+ 1b41b848-ddd2-4a8d-9bc7-850a93e147c4
+ cms.channel
+
+
+ 3f1888dd-8edf-43e8-8176-d091a3fd809f
+ MemberResetPasswordConfirmation-ahw9v8cj
+ FormAutoresponder
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/be37e92b-7244-4c85..e779daf3de55_en-us@03af2e1a83.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/be37e92b-7244-4c85..e779daf3de55_en-us@03af2e1a83.xml
index 7bef0b0b..1eba94fd 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/be37e92b-7244-4c85..e779daf3de55_en-us@03af2e1a83.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/be37e92b-7244-4c85..e779daf3de55_en-us@03af2e1a83.xml
@@ -13,7 +13,7 @@
2024-10-28 21:14:05Z
be37e92b-7244-4c85-bae8-e779daf3de55
True
- 2024-10-28 21:14:05Z
+ 2024-11-09 14:20:07Z
2
False
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/8858f8ba-fd61-4f22..9c12b5f3d4cd_en-us@d95d1e4367.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/8858f8ba-fd61-4f22..9c12b5f3d4cd_en-us@d95d1e4367.xml
index 39080ccf..7357192b 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/8858f8ba-fd61-4f22..9c12b5f3d4cd_en-us@d95d1e4367.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/8858f8ba-fd61-4f22..9c12b5f3d4cd_en-us@d95d1e4367.xml
@@ -13,7 +13,7 @@
2024-11-01 23:52:04Z
8858f8ba-fd61-4f22-8876-9c12b5f3d4cd
True
- 2024-11-01 23:53:08Z
+ 2024-11-09 14:17:22Z
2
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/2c95891d-6670-4071..01a731c8abeb_en-us@9133e0fa1c.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/2c95891d-6670-4071..01a731c8abeb_en-us@9133e0fa1c.xml
index e994cbaf..0302eabd 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/2c95891d-6670-4071..01a731c8abeb_en-us@9133e0fa1c.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemcommondata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/2c95891d-6670-4071..01a731c8abeb_en-us@9133e0fa1c.xml
@@ -13,7 +13,7 @@
2024-08-30 21:14:54Z
2c95891d-6670-4071-87e0-01a731c8abeb
True
- 2024-08-30 21:14:54Z
+ 2024-11-09 14:20:34Z
2
False
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/5353bb15-dfca-45ca..6364a77af46e_en-us@b151c4c24e.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/5353bb15-dfca-45ca..6364a77af46e_en-us@b151c4c24e.xml
index ae7f9ea2..8c0ba8ca 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/5353bb15-dfca-45ca..6364a77af46e_en-us@b151c4c24e.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/blog-discussion-xp..ctsrv8va-localtest@d1571c64da/5353bb15-dfca-45ca..6364a77af46e_en-us@b151c4c24e.xml
@@ -23,11 +23,19 @@
False
2
- membership_author
- fdad91c4-9c0c-420f-80cd-8bf8203bac70
+ SeanW_kentico.com
+ d6a0d252-e67a-4090-8610-1a9f9b6a2cb3
cms.user
+
+
+ 5353bb15-dfca-45ca-a07a-6364a77af46e
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ aa57c06d-5738-4fdb-ab10-08b42fc6c9c2
+
5353bb15-dfca-45ca-a07a-6364a77af46e
@@ -36,5 +44,37 @@
22ef0471-1e20-432b-9af4-fd7004dcfbda
0a81201d-8daa-4a54-bcc1-320914635b8f
+
+
+ 5353bb15-dfca-45ca-a07a-6364a77af46e
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 7d35333b-4b76-4fd6-8b8a-5198f013e0f0
+
+
+
+ 5353bb15-dfca-45ca-a07a-6364a77af46e
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 55f4d13a-2b1f-4c3d-9f7b-552b3eda78f1
+
+
+
+ 5353bb15-dfca-45ca-a07a-6364a77af46e
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ b4c38f8c-f5b6-4969-90d5-9c0109b1ba8b
+
+
+
+ 5353bb15-dfca-45ca-a07a-6364a77af46e
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 803ca767-e932-4db9-a6bb-cb1393bfd255
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/e832aa91-0c2b-4bb0..182a7e9a5be9_en-us@d144aacbcc.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/e832aa91-0c2b-4bb0..182a7e9a5be9_en-us@d144aacbcc.xml
index 451743c8..b3a7437f 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/e832aa91-0c2b-4bb0..182a7e9a5be9_en-us@d144aacbcc.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/is-there-a-new-xpe..tysbut8z-localtest@3378283d53/e832aa91-0c2b-4bb0..182a7e9a5be9_en-us@d144aacbcc.xml
@@ -23,8 +23,8 @@
False
2
- membership_author
- fdad91c4-9c0c-420f-80cd-8bf8203bac70
+ SeanW_kentico.com
+ d6a0d252-e67a-4090-8610-1a9f9b6a2cb3
cms.user
@@ -36,5 +36,37 @@
22ef0471-1e20-432b-9af4-fd7004dcfbda
c50e7dd3-2b8e-47b5-96ee-3f04ccfde8b6
+
+
+ e832aa91-0c2b-4bb0-adce-182a7e9a5be9
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 0fc775d0-b29f-4d64-b5f3-4845693c9af3
+
+
+
+ e832aa91-0c2b-4bb0-adce-182a7e9a5be9
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 803ca767-e932-4db9-a6bb-cb1393bfd255
+
+
+
+ e832aa91-0c2b-4bb0-adce-182a7e9a5be9
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 3e155a08-fb9e-4526-97fb-ee856919c7df
+
+
+
+ e832aa91-0c2b-4bb0-adce-182a7e9a5be9
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 5ac0694e-8f92-4a8d-8ef0-fed17ff1d3d5
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/97569580-40be-46c1..6a5cefbfd333_en-us@5ebcec0a9e.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/97569580-40be-46c1..6a5cefbfd333_en-us@5ebcec0a9e.xml
index e3c8c48a..1d4f81e2 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/97569580-40be-46c1..6a5cefbfd333_en-us@5ebcec0a9e.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/cms.contentitemlanguagemetadata/test-question-25-8..64v0algc-localtest@18cd9b4b8d/97569580-40be-46c1..6a5cefbfd333_en-us@5ebcec0a9e.xml
@@ -21,8 +21,8 @@
False
2
- membership_author
- fdad91c4-9c0c-420f-80cd-8bf8203bac70
+ SeanW_kentico.com
+ d6a0d252-e67a-4090-8610-1a9f9b6a2cb3
cms.user
@@ -34,5 +34,37 @@
22ef0471-1e20-432b-9af4-fd7004dcfbda
c50e7dd3-2b8e-47b5-96ee-3f04ccfde8b6
+
+
+ 97569580-40be-46c1-91cd-6a5cefbfd333
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 4cc8182a-1481-4667-9aae-631366e58170
+
+
+
+ 97569580-40be-46c1-91cd-6a5cefbfd333
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 094324f2-a6cb-4ad3-bd29-81eee615f82d
+
+
+
+ 97569580-40be-46c1-91cd-6a5cefbfd333
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ aea4a65d-5518-4ed3-9dbe-b8952163f052
+
+
+
+ 97569580-40be-46c1-91cd-6a5cefbfd333
+ cms.contentitemlanguagemetadata
+
+ f3cfe649-5652-46d2-8fe8-a848f39c0fbd
+ 5e40f0dd-8822-48ce-b5b4-cc67559f4463
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/blog-discussion-xp..e779daf3de55_en-us@4a02b60417/75732d76-77c9-4454-a9c1-11f2cda4a4e4.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/blog-discussion-xp..e779daf3de55_en-us@4a02b60417/75732d76-77c9-4454-a9c1-11f2cda4a4e4.xml
index aa3007c2..20d62adf 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/blog-discussion-xp..e779daf3de55_en-us@4a02b60417/75732d76-77c9-4454-a9c1-11f2cda4a4e4.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/blog-discussion-xp..e779daf3de55_en-us@4a02b60417/75732d76-77c9-4454-a9c1-11f2cda4a4e4.xml
@@ -27,6 +27,9 @@ Continue discussions 🤗 on this blog post below.
+
+
+
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/is-there-a-new-xpe..9c12b5f3d4cd_en-us@5d017b6ce9/b35fb74d-e802-4169-afd3-a8f580fdccfd.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/is-there-a-new-xpe..9c12b5f3d4cd_en-us@5d017b6ce9/b35fb74d-e802-4169-afd3-a8f580fdccfd.xml
index 8a532393..06dfb526 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/is-there-a-new-xpe..9c12b5f3d4cd_en-us@5d017b6ce9/b35fb74d-e802-4169-afd3-a8f580fdccfd.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/is-there-a-new-xpe..9c12b5f3d4cd_en-us@5d017b6ce9/b35fb74d-e802-4169-afd3-a8f580fdccfd.xml
@@ -11,7 +11,7 @@
b35fb74d-e802-4169-afd3-a8f580fdccfd
00000000-0000-0000-0000-000000000000
- 48
+ 21
+
+
+
Is there a new Xperience by Kentico tutorial?
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/test-question-25-8..01a731c8abeb_en-us@96ed3a18e3/b85c6b9e-4134-460e-bf74-ec6b93cdf4af.xml b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/test-question-25-8..01a731c8abeb_en-us@96ed3a18e3/b85c6b9e-4134-460e-bf74-ec6b93cdf4af.xml
index 740e3563..796e6bc2 100644
--- a/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/test-question-25-8..01a731c8abeb_en-us@96ed3a18e3/b85c6b9e-4134-460e-bf74-ec6b93cdf4af.xml
+++ b/src/Kentico.Community.Portal.Web/App_Data/CIRepository/devnet/contentitemdata.kenticocommunity.qandaquestionpage/test-question-25-8..01a731c8abeb_en-us@96ed3a18e3/b85c6b9e-4134-460e-bf74-ec6b93cdf4af.xml
@@ -30,5 +30,8 @@
+
+
+
Test question 25
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/Client/js/features/search.js b/src/Kentico.Community.Portal.Web/Client/js/features/search.js
index f2d68cdf..07adb47c 100644
--- a/src/Kentico.Community.Portal.Web/Client/js/features/search.js
+++ b/src/Kentico.Community.Portal.Web/Client/js/features/search.js
@@ -31,79 +31,53 @@ function initializeSortByOnForm(formEl) {
*
* @param {HTMLFormElement} formEl
*/
-function initializeCheckboxOnForm(formEl) {
- let checkboxes = document.querySelectorAll("[search-checkbox]");
-
- for (let i = 0; i < checkboxes.length; i++) {
- checkboxes[i].addEventListener("change", function (e) {
- if (formEl !== null) {
- if (e.target instanceof HTMLInputElement) {
- // For aspnet core model binding
- e.target.value = e.target.checked;
- }
- const loadPanel = document.getElementById("overlay");
- showLoadPanel(loadPanel);
- formEl.submit();
- }
- });
- }
-}
-
-/**
- *
- * @param {HTMLFormElement} formEl
- * @param {string} facetType
- */
-function initializeFacetsOnForm(formEl, facetType) {
+function initializeCheckboxFacetsOnForm(formEl) {
const submitButton = document.querySelector("#submitSearch");
if (!(submitButton instanceof HTMLButtonElement)) {
throw new Error("Missing search submit button");
}
-
- const facetInput = document.querySelector(
- `[selected-facet-value="${facetType}"]`
- );
- if (!(facetInput instanceof HTMLElement)) {
- throw new Error("Missing facet value form input");
- }
-
- function addFacetsToFacetInput() {
- let tags = document.querySelectorAll(
- `[facet-selected][facet-type="${facetType}"]`
- );
-
- const selectedValues = [...tags].map((tag) =>
- tag.getAttribute("facet-value")
- );
-
- facetInput.value = selectedValues.join(";");
- // TODO - reset page number
- }
-
- let facets = document.querySelectorAll(
- `[facet-value][facet-type="${facetType}"]`
- );
+ submitButton.addEventListener("click", () => {
+ showLoadPanel(loadPanel);
+ });
const loadPanel = document.getElementById("overlay");
- for (let i = 0; i < facets.length; i++) {
- facets[i].addEventListener("click", (e) => {
- e.preventDefault();
- showLoadPanel(loadPanel);
- if (facets[i].hasAttribute("facet-selected")) {
- facets[i].removeAttribute("facet-selected");
- } else if (!facets[i].hasAttribute("facet-selected")) {
- facets[i].setAttribute("facet-selected", "");
+ /**
+ * @type {HTMLInputElement[]}
+ */
+ const facetEls = [...formEl.querySelectorAll(`[facet-field]`)];
+ for (const facetEl of facetEls) {
+ facetEl.addEventListener("click", (e) => {
+ if (!(e.target instanceof HTMLInputElement)) {
+ return;
+ }
+
+ const value = e.target.value;
+
+ if (e.target.hasAttribute("facet-mobile")) {
+ /**
+ * @type {HTMLInputElement}
+ */
+ const el = document.querySelector(
+ `[value="${value}"]:not([facet-mobile])`,
+ );
+ if (el instanceof HTMLInputElement) {
+ /** disable unused field to prevent double field submission */
+ el.disabled = true;
+ }
+ } else {
+ /**
+ * @type {HTMLInputElement}
+ */
+ const el = document.querySelector(`[value=${value}][facet-mobile]`);
+ if (el instanceof HTMLInputElement) {
+ /** disable unused field to prevent double field submission */
+ el.disabled = true;
+ }
}
- addFacetsToFacetInput();
formEl.submit();
});
}
-
- submitButton.addEventListener("click", () => {
- showLoadPanel(loadPanel);
- addFacetsToFacetInput();
- });
}
function initializeQAndASearch() {
@@ -113,8 +87,7 @@ function initializeQAndASearch() {
}
initializeSortByOnForm(form);
- initializeCheckboxOnForm(form);
- initializeFacetsOnForm(form, "discussionType");
+ initializeCheckboxFacetsOnForm(form);
}
function initializeBlogSearch() {
@@ -123,6 +96,6 @@ function initializeBlogSearch() {
return;
}
- initializeFacetsOnForm(form, "blogType");
initializeSortByOnForm(form);
+ initializeCheckboxFacetsOnForm(form);
}
diff --git a/src/Kentico.Community.Portal.Web/Client/styles/00_core/_variables.scss b/src/Kentico.Community.Portal.Web/Client/styles/00_core/_variables.scss
index 8d922305..1ec7641c 100644
--- a/src/Kentico.Community.Portal.Web/Client/styles/00_core/_variables.scss
+++ b/src/Kentico.Community.Portal.Web/Client/styles/00_core/_variables.scss
@@ -293,3 +293,6 @@ $tooltip-max-width: 240px !default;
$tooltip-padding-y: 0.5rem !default;
$tooltip-padding-x: 0.75rem !default;
+
+// $enable-grid-classes: false
+$enable-cssgrid: true;
diff --git a/src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidget.cs b/src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidget.cs
index 1ec3b787..a824579a 100644
--- a/src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidget.cs
+++ b/src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidget.cs
@@ -259,3 +259,30 @@ public enum ItemLayout
Minimal
}
+public class BlogPostListWidgetViewModel : BaseWidgetViewModel
+{
+ protected override string WidgetName { get; } = BlogPostListWidget.NAME;
+
+ public string? Heading { get; } = "";
+ public IReadOnlyList BlogPosts { get; set; } = [];
+ public ItemLayout Layout { get; set; } = ItemLayout.Minimal;
+ public string BlogType { get; set; } = "";
+
+ public BlogPostListWidgetViewModel(BlogPostListWidgetProperties props, IEnumerable posts)
+ {
+ Heading = string.IsNullOrWhiteSpace(props.Heading) ? null : props.Heading;
+ BlogPosts = posts.ToList();
+ Layout = props.ItemLayoutSourceParsed;
+ }
+}
+
+public class BlogPostViewModel(BlogPostAuthorViewModel author)
+{
+ public string Title { get; init; } = "";
+ public DateTime Date { get; init; }
+ public Maybe TeaserImage { get; init; }
+ public BlogPostAuthorViewModel Author { get; init; } = author;
+ public string ShortDescription { get; init; } = "";
+ public string LinkPath { get; init; } = "";
+ public string? Taxonomy { get; set; } = "";
+}
diff --git a/src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidgetViewModel.cs b/src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidgetViewModel.cs
deleted file mode 100644
index 4865907d..00000000
--- a/src/Kentico.Community.Portal.Web/Components/Widgets/BlogPostList/BlogPostListWidgetViewModel.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Kentico.Community.Portal.Web.Components.ViewComponents.Pagination;
-using Kentico.Community.Portal.Web.Features.Blog;
-using Kentico.Community.Portal.Web.Infrastructure.Search;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Kentico.Community.Portal.Web.Components.Widgets.BlogPostList;
-
-public class BlogPostListWidgetViewModel : BaseWidgetViewModel, IPagedViewModel
-{
- protected override string WidgetName { get; } = BlogPostListWidget.NAME;
-
- public string? Heading { get; } = "";
- public IReadOnlyList BlogPosts { get; set; } = [];
- public ItemLayout Layout { get; set; } = ItemLayout.Minimal;
- public string? Query { get; set; } = "";
- public string SortBy { get; set; } = "";
- [HiddenInput]
- public string BlogType { get; set; } = "";
- [HiddenInput]
- public int Page { get; set; } = 0;
- public List BlogTypes { get; set; } = [];
- public int TotalPages { get; set; } = 0;
-
- public BlogPostListWidgetViewModel(BlogPostListWidgetProperties props, IEnumerable posts)
- {
- Heading = string.IsNullOrWhiteSpace(props.Heading) ? null : props.Heading;
- BlogPosts = posts.ToList();
- Layout = props.ItemLayoutSourceParsed;
- }
-
- public BlogPostListWidgetViewModel() { }
-
- public Dictionary GetRouteData(int page) =>
- new()
- {
- { "query", Query },
- { "page", page.ToString() },
- { "sortBy", SortBy },
- { "blogType", BlogType }
- };
-}
-
diff --git a/src/Kentico.Community.Portal.Web/Configuration/GlobalEventsModule.cs b/src/Kentico.Community.Portal.Web/Configuration/GlobalEventsModule.cs
index f4ff4874..2251a0b6 100644
--- a/src/Kentico.Community.Portal.Web/Configuration/GlobalEventsModule.cs
+++ b/src/Kentico.Community.Portal.Web/Configuration/GlobalEventsModule.cs
@@ -3,9 +3,11 @@
using CMS.Core;
using CMS.DataEngine;
using CMS.EmailLibrary;
+using CMS.EmailLibrary.Internal;
using Kentico.Community.Portal.Core.Modules;
using Kentico.Community.Portal.Web.Configuration;
using Kentico.Community.Portal.Web.Features.Blog.Events;
+using Kentico.Community.Portal.Web.Features.Members;
using Kentico.Community.Portal.Web.Features.QAndA.Events;
using Kentico.Community.Portal.Web.Rendering.Events;
@@ -48,6 +50,12 @@ protected override void OnInit(ModuleInitParameters parameters)
TagInfo.TYPEINFO.Events.Update.Before += Tag_ModifyBefore;
TagInfo.TYPEINFO.Events.Delete.Before += Tag_DeleteBefore;
+ EmailContentFilterRegister.Instance
+ .Register(
+ () => new CustomValueFilter(),
+ EmailContentFilterType.Sending,
+ 100);
+
base.OnInit(parameters);
}
diff --git a/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionAppExtensions.cs b/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionAppExtensions.cs
index 09d1e5ee..9863c7bf 100644
--- a/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionAppExtensions.cs
+++ b/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionAppExtensions.cs
@@ -53,8 +53,8 @@ private static IServiceCollection AddOperations(this IServiceCollection services
.RegisterServicesFromAssembly(typeof(HomePageQuery).Assembly)
.AddOpenBehavior(typeof(QueryCachingPipelineBehavior<,>))
.AddOpenBehavior(typeof(CommandHandlerLogDecorator<,>)))
- .AddClosedGenericTypes(typeof(HomePageQuery).Assembly, typeof(IQueryHandler<,>), ServiceLifetime.Scoped)
- .AddClosedGenericTypes(typeof(HomePageQuery).Assembly, typeof(ICommandHandler<,>), ServiceLifetime.Scoped)
+ .AddClosedGenericTypes(typeof(HomePageQuery).Assembly, typeof(IQueryHandler<,>), ServiceLifetime.Transient)
+ .AddClosedGenericTypes(typeof(HomePageQuery).Assembly, typeof(ICommandHandler<,>), ServiceLifetime.Transient)
.AddTransient()
.AddTransient()
.AddTransient()
diff --git a/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionMembershipExtensions.cs b/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionMembershipExtensions.cs
index cb3d1b6f..d7e0c63c 100644
--- a/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionMembershipExtensions.cs
+++ b/src/Kentico.Community.Portal.Web/Configuration/ServiceCollectionMembershipExtensions.cs
@@ -1,3 +1,4 @@
+using Kentico.Community.Portal.Web.Features.Members;
using Kentico.Community.Portal.Web.Membership;
using Kentico.Membership;
using Kentico.OnlineMarketing.Web.Mvc;
@@ -43,5 +44,6 @@ public static IServiceCollection AddAppXperienceMembership(this IServiceCollecti
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
})
- .AddAuthorization();
+ .AddAuthorization()
+ .AddSingleton();
}
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/BlogPostViewModel.cs b/src/Kentico.Community.Portal.Web/Features/Blog/BlogPostViewModel.cs
deleted file mode 100644
index d2be5311..00000000
--- a/src/Kentico.Community.Portal.Web/Features/Blog/BlogPostViewModel.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Kentico.Community.Portal.Web.Rendering;
-
-namespace Kentico.Community.Portal.Web.Features.Blog;
-
-public class BlogPostViewModel(BlogPostAuthorViewModel author)
-{
- public string Title { get; init; } = "";
- public DateTime Date { get; init; }
- public Maybe TeaserImage { get; init; }
- public BlogPostAuthorViewModel Author { get; init; } = author;
- public string ShortDescription { get; init; } = "";
- public string LinkPath { get; init; } = "";
- public string? Taxonomy { get; set; } = "";
-}
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearch.cshtml b/src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearch.cshtml
deleted file mode 100644
index 6801b7fa..00000000
--- a/src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearch.cshtml
+++ /dev/null
@@ -1,107 +0,0 @@
-@using Kentico.Community.Portal.Web.Features.Blog
-@using Kentico.Community.Portal.Web.Components.Widgets.BlogPostList
-
-@model BlogPostListWidgetViewModel
-
-
-
-
- @if (Model.Heading is not null)
- {
-
@Model.Heading
- }
-
-
-
- @if (Model.BlogPosts.Count == 0 && Model.TotalPages == 0)
- {
-
-
No results could be found.
-
- }
-
- @foreach (var post in Model.BlogPosts)
- {
-
-
-
-
-
-
- @post.ShortDescription
-
-
- @if (post.Taxonomy is string taxonomy)
- {
-
- }
-
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearchViewComponent.cs b/src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearchViewComponent.cs
deleted file mode 100644
index 33c07e1c..00000000
--- a/src/Kentico.Community.Portal.Web/Features/Blog/Components/BlogSearchViewComponent.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using Kentico.Community.Portal.Web.Components.Widgets.BlogPostList;
-using Kentico.Community.Portal.Web.Features.Blog.Search;
-using Kentico.Community.Portal.Web.Infrastructure.Search;
-using MediatR;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Kentico.Community.Portal.Web.Features.Blog.Components;
-
-public class BlogSearchViewComponent(IMediator mediator, BlogSearchService searchService) : ViewComponent
-{
- private readonly IMediator mediator = mediator;
- private readonly BlogSearchService searchService = searchService;
-
- public async Task InvokeAsync()
- {
- var request = new BlogSearchRequest(HttpContext.Request);
-
- var result = await mediator.Send(new BlogPostTaxonomiesQuery());
- var taxonomies = result.Items;
-
- var searchResult = searchService.SearchBlog(request);
- var chosenFacets = request.BlogType.ToLower().Split(";", StringSplitOptions.RemoveEmptyEntries)?.ToList() ?? [];
-
- var model = new BlogPostListWidgetViewModel()
- {
- BlogPosts = (searchResult?.Hits ?? []).Select(result => new BlogPostViewModel(new()
- {
- ID = result.AuthorMemberID,
- Name = result.AuthorName,
- Photo = Maybe.From(result.AuthorAvatarImage!).Map(i => i.ToImageViewModel()),
- })
- {
- Title = result.Title,
- Date = result.PublishedDate,
- LinkPath = result.Url,
- ShortDescription = result.ShortDescription,
- TeaserImage = Maybe.From(result.TeaserImage!).Map(i => i.ToImageViewModel()),
- Taxonomy = result.BlogType
- }).ToList(),
- Page = request.PageNumber,
- Query = request.SearchText,
- SortBy = request.SortBy,
- BlogType = request.BlogType,
- BlogTypes = [.. taxonomies
- .Select(x => new FacetOption()
- {
- Label = x.DisplayName,
- Value = searchResult?.Facets?.FirstOrDefault(y => y.Label.Equals(x.Value, StringComparison.InvariantCultureIgnoreCase))?.Value ?? 0,
- IsSelected = chosenFacets.Contains(x.DisplayName, StringComparer.OrdinalIgnoreCase)
- })
- .Where(x => x.Value != 0)
- .OrderBy(f => f.Label)],
- TotalPages = searchResult?.TotalPages ?? 0
- };
-
- return View("~/Features/Blog/Components/BlogSearch.cshtml", model);
- }
-}
-
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/Operations/BlogPostTaxonomiesQuery.cs b/src/Kentico.Community.Portal.Web/Features/Blog/Operations/BlogPostTaxonomiesQuery.cs
index 1184e3fe..0e96ee1d 100644
--- a/src/Kentico.Community.Portal.Web/Features/Blog/Operations/BlogPostTaxonomiesQuery.cs
+++ b/src/Kentico.Community.Portal.Web/Features/Blog/Operations/BlogPostTaxonomiesQuery.cs
@@ -1,3 +1,4 @@
+using System.Text.RegularExpressions;
using CMS.ContentEngine;
using Kentico.Community.Portal.Core;
using Kentico.Community.Portal.Core.Modules;
@@ -7,24 +8,49 @@ namespace Kentico.Community.Portal.Web.Features.Blog;
public record BlogPostTaxonomiesQuery() : IQuery;
-public record BlogPostTaxonomy(Guid Guid, string Value, string DisplayName);
-public record BlogPostTaxonomiesQueryResponse(IReadOnlyList Items);
+public class BlogPostTaxonomy
+{
+ public Guid Guid { get; set; }
+ public string Name { get; set; }
+ public string NormalizedName { get; set; }
+ public string DisplayName { get; set; }
+
+ public BlogPostTaxonomy(Tag tag)
+ {
+ Guid = tag.Identifier;
+ Name = tag.Name;
+ NormalizedName = RegexTools.AlphanumericRegex().Replace(tag.Name, "").ToLowerInvariant();
+ DisplayName = tag.Title;
+ }
+}
+
+public record BlogPostTaxonomiesQueryResponse(IReadOnlyList Types, IReadOnlyList DXTopics);
public class BlogPostTaxonomiesQueryHandler(DataItemQueryTools tools, ITaxonomyRetriever taxonomyRetriever) : DataItemQueryHandler(tools)
{
private readonly ITaxonomyRetriever taxonomyRetriever = taxonomyRetriever;
public override async Task Handle(BlogPostTaxonomiesQuery request, CancellationToken cancellationToken = default)
{
- var taxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.BlogTypeTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE, cancellationToken);
+ var typeTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.BlogTypeTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE, cancellationToken);
+ var typeTags = typeTaxonomy.Tags
+ .Select(tag => new BlogPostTaxonomy(tag))
+ .ToList();
- var taxonomies = taxonomy.Tags
- .Select(tag => new BlogPostTaxonomy(tag.Identifier, tag.Name, tag.Title))
+ var topicsTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.DXTopicTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE, cancellationToken);
+ var topicTags = topicsTaxonomy.Tags
+ .Select(tag => new BlogPostTaxonomy(tag))
.ToList();
- return new BlogPostTaxonomiesQueryResponse(taxonomies);
+ return new BlogPostTaxonomiesQueryResponse(typeTags, topicTags);
}
protected override ICacheDependencyKeysBuilder AddDependencyKeys(BlogPostTaxonomiesQuery query, BlogPostTaxonomiesQueryResponse result, ICacheDependencyKeysBuilder builder) =>
builder.Object(TaxonomyInfo.OBJECT_TYPE, SystemTaxonomies.BlogTypeTaxonomy.CodeName)
- .Collection(result.Items, i => builder.Object(TagInfo.OBJECT_TYPE, i.Value));
+ .Collection(result.Types, i => builder.Object(TagInfo.OBJECT_TYPE, i.Name));
+}
+
+public partial class RegexTools
+{
+ [GeneratedRegex(@"[^a-zA-Z0-9]")]
+ public static partial Regex AlphanumericRegex();
}
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearch.cshtml b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearch.cshtml
new file mode 100644
index 00000000..6e30f530
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearch.cshtml
@@ -0,0 +1,151 @@
+@using Kentico.Community.Portal.Web.Features.Blog.Search
+
+@model BlogSearchViewModel
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchIndexModel.cs b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchIndexModel.cs
index 6714d43a..50f04525 100644
--- a/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchIndexModel.cs
+++ b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchIndexModel.cs
@@ -1,18 +1,15 @@
-using System.Collections.Specialized;
+using System.Text.Json;
using CMS.ContentEngine;
using CMS.Core;
-using CMS.Helpers;
using CMS.MediaLibrary;
using Kentico.Community.Portal.Core;
-using Kentico.Community.Portal.Core.Modules;
using Kentico.Community.Portal.Web.Infrastructure.Search;
using Kentico.Community.Portal.Web.Rendering;
-using Kentico.Content.Web.Mvc;
using Kentico.Xperience.Lucene.Core.Indexing;
using Lucene.Net.Documents;
using Lucene.Net.Documents.Extensions;
using Lucene.Net.Facet;
-using Newtonsoft.Json;
+using MediatR;
namespace Kentico.Community.Portal.Web.Features.Blog.Search;
@@ -24,9 +21,9 @@ public class BlogSearchIndexModel
public string Title { get; set; } = "";
public string Content { get; set; } = "";
public DateTime PublishedDate { get; set; }
- public const string DXTopicsFacetField = $"{nameof(DXTopics)}_Facet";
- public string DXTopics { get; set; } = "";
- public const string BlogTypeFacetField = $"{nameof(BlogType)}_Facet";
+ public List DXTopicsFacets { get; set; } = [];
+ public List DXTopics { get; set; } = [];
+ public string BlogTypeFacet { get; set; } = "";
public string BlogType { get; set; } = "";
public string ShortDescription { get; set; } = "";
public ImageAssetViewModelSerializable? TeaserImage { get; set; } = null;
@@ -41,32 +38,39 @@ public Document ToDocument()
new TextField(nameof(Title), Title, Field.Store.YES),
new TextField(nameof(Content), Content, Field.Store.NO),
new Int64Field(nameof(PublishedDate), DateTools.TicksToUnixTimeMilliseconds(PublishedDate.Ticks), Field.Store.YES),
- new TextField(nameof(DXTopics), (string.IsNullOrWhiteSpace(DXTopics) ? "untaxonomized" : DXTopics).ToLowerInvariant(), Field.Store.YES),
- new TextField(nameof(BlogType), (string.IsNullOrWhiteSpace(BlogType) ? "untaxonomized" : BlogType).ToLowerInvariant(), Field.Store.YES),
+ new TextField(nameof(DXTopics), string.Join(";", DXTopics), Field.Store.YES),
+ new TextField(nameof(BlogType), BlogType, Field.Store.YES),
new TextField(nameof(ShortDescription), ShortDescription, Field.Store.YES),
- new TextField(nameof(TeaserImage), JsonConvert.SerializeObject(TeaserImage), Field.Store.YES),
+ new TextField(nameof(TeaserImage), JsonSerializer.Serialize(TeaserImage), Field.Store.YES),
new Int32Field(nameof(AuthorMemberID), AuthorMemberID, Field.Store.YES),
new TextField(nameof(AuthorName), AuthorName, Field.Store.YES),
- new TextField(nameof(AuthorAvatarImage), JsonConvert.SerializeObject(AuthorAvatarImage), Field.Store.YES),
+ new TextField(nameof(AuthorAvatarImage), JsonSerializer.Serialize(AuthorAvatarImage), Field.Store.YES),
};
- _ = indexDocument.AddFacetField(nameof(DXTopicsFacetField), indexDocument.Get(nameof(DXTopics)));
- _ = indexDocument.AddFacetField(nameof(BlogTypeFacetField), indexDocument.Get(nameof(BlogType)));
+ foreach (string topicFacet in DXTopicsFacets)
+ {
+ _ = indexDocument.AddFacetField(nameof(DXTopicsFacets), topicFacet);
+ }
+
+ if (!string.IsNullOrWhiteSpace(BlogTypeFacet))
+ {
+ _ = indexDocument.AddFacetField(nameof(BlogTypeFacet), BlogTypeFacet);
+ }
return indexDocument;
}
public static BlogSearchIndexModel FromDocument(Document doc)
{
- var teaserImage = JsonConvert.DeserializeObject(doc.Get(nameof(TeaserImage)) ?? "{ }");
- var authorImage = JsonConvert.DeserializeObject(doc.Get(nameof(AuthorAvatarImage)) ?? "{ }");
+ var teaserImage = JsonSerializer.Deserialize(doc.Get(nameof(TeaserImage)) ?? "{ }");
+ var authorImage = JsonSerializer.Deserialize(doc.Get(nameof(AuthorAvatarImage)) ?? "{ }");
var model = new BlogSearchIndexModel
{
Url = doc.Get(nameof(Url)),
Title = doc.Get(nameof(Title)),
ShortDescription = doc.Get(nameof(ShortDescription)),
- DXTopics = doc.Get(nameof(DXTopics)),
+ DXTopics = [.. doc.Get(nameof(DXTopics)).Split(";")],
BlogType = doc.Get(nameof(BlogType)),
TeaserImage = teaserImage,
AuthorMemberID = int.TryParse(doc.Get(nameof(AuthorMemberID)), out int authorMemberID)
@@ -84,25 +88,21 @@ public static BlogSearchIndexModel FromDocument(Document doc)
}
}
-public class BlogSearchIndexingStrategy(
+public partial class BlogSearchIndexingStrategy(
IContentQueryExecutor executor,
- AssetItemService assetService,
- ITaxonomyRetriever taxonomyRetriever,
WebScraperHtmlSanitizer htmlSanitizer,
WebCrawlerService webCrawler,
IChannelDataProvider channelDataProvider,
- IProgressiveCache cache,
+ IMediator mediator,
IEventLogService log) : DefaultLuceneIndexingStrategy
{
public const string IDENTIFIER = "BLOG_SEARCH";
private readonly IContentQueryExecutor executor = executor;
- private readonly AssetItemService assetService = assetService;
- private readonly ITaxonomyRetriever taxonomyRetriever = taxonomyRetriever;
private readonly WebScraperHtmlSanitizer htmlSanitizer = htmlSanitizer;
private readonly WebCrawlerService webCrawler = webCrawler;
private readonly IChannelDataProvider channelDataProvider = channelDataProvider;
- private readonly IProgressiveCache cache = cache;
+ private readonly IMediator mediator = mediator;
private readonly IEventLogService log = log;
public override async Task> FindItemsToReindex(IndexEventWebPageItemModel changedItem) => await Task.FromResult>([changedItem]);
@@ -197,23 +197,31 @@ public override async Task> FindItemsToReindex
blogPost.ListableItemFeaturedImageContent
.TryFirst()
.Map(i => new ImageAssetViewModelSerializable(i))
- .IfNoValue(blogPost.ListableItemFeaturedImage
- .TryFirst()
- .Map(i => new ImageAssetViewModelSerializable(i)))
.Execute(i => indexModel.TeaserImage = i);
- var blogPostTaxonomy = await GetBlogPostTaxonomy();
- var dxTopicsTaxonomy = await GetDXTopicsTaxonomy();
+ var taxonomies = await mediator.Send(new BlogPostTaxonomiesQuery());
blogPost.BlogPostContentBlogType
.TryFirst()
.Map(t => t.Identifier)
- .Bind(id => blogPostTaxonomy.Tags.TryFirst(t => t.Identifier == id))
- .Execute(tag => indexModel.BlogType = tag.Title);
- indexModel.DXTopics = string.Join(",", blogPost
+ .Bind(id => taxonomies.Types
+ .TryFirst(t => t.Guid == id))
+ .Execute(tag =>
+ {
+ indexModel.BlogType = tag.DisplayName;
+ indexModel.BlogTypeFacet = tag.NormalizedName;
+ });
+ var dxTopics = blogPost
.BlogPostContentDXTopics
- .Select(t => dxTopicsTaxonomy.Tags.FirstOrDefault(t => t.Identifier == t.Identifier)?.Title ?? "")
- .Where(t => !string.IsNullOrWhiteSpace(t)));
+ .Select(tagRef => taxonomies.DXTopics
+ .FirstOrDefault(t => tagRef.Identifier == t.Guid))
+ .WhereNotNull();
+
+ foreach (var tag in dxTopics)
+ {
+ indexModel.DXTopics.Add(tag.DisplayName);
+ indexModel.DXTopicsFacets.Add(tag.NormalizedName);
+ }
string content = await webCrawler.CrawlWebPage(page);
indexModel.Content = htmlSanitizer.SanitizeHtmlDocument(content);
@@ -234,28 +242,13 @@ public override FacetsConfig FacetsConfigFactory()
{
var facetConfig = new FacetsConfig();
- facetConfig.SetMultiValued(nameof(BlogSearchIndexModel.DXTopicsFacetField), false);
- facetConfig.SetMultiValued(nameof(BlogSearchIndexModel.BlogTypeFacetField), false);
+ facetConfig.SetMultiValued(nameof(BlogSearchIndexModel.DXTopicsFacets), true);
+ facetConfig.SetMultiValued(nameof(BlogSearchIndexModel.BlogTypeFacet), false);
return facetConfig;
}
-
- private Task GetBlogPostTaxonomy() =>
- cache.LoadAsync(cs =>
- {
- cs.CacheDependency = CacheHelper.GetCacheDependency($"{TaxonomyInfo.OBJECT_TYPE}|byname|{SystemTaxonomies.BlogTypeTaxonomy.CodeName}");
- return taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.BlogTypeTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE);
- }, new CacheSettings(5, [nameof(BlogSearchIndexModel), nameof(GetBlogPostTaxonomy)]));
- private Task GetDXTopicsTaxonomy() =>
- cache.LoadAsync(cs =>
- {
- cs.CacheDependency = CacheHelper.GetCacheDependency($"{TaxonomyInfo.OBJECT_TYPE}|byname|{SystemTaxonomies.DXTopicTaxonomy.CodeName}");
- return taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.DXTopicTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE);
- }, new CacheSettings(5, [nameof(BlogSearchIndexModel), nameof(GetDXTopicsTaxonomy)]));
}
-
-
public class ImageAssetViewModelSerializable
{
public ImageAssetViewModelSerializable(ImageContent image)
@@ -267,15 +260,6 @@ public ImageAssetViewModelSerializable(ImageContent image)
Dimensions = new() { Width = image.MediaItemAssetWidth, Height = image.MediaItemAssetHeight };
}
- public ImageAssetViewModelSerializable(MediaAssetContent image)
- {
- ID = image.SystemFields.ContentItemGUID;
- Title = image.MediaAssetContentTitle;
- URL = image.MediaAssetContentAssetLight.Url;
- AltText = image.MediaAssetContentShortDescription;
- Dimensions = new() { Width = image.MediaAssetContentImageLightWidth, Height = image.MediaAssetContentImageLightHeight };
- }
-
public ImageAssetViewModelSerializable() { }
public Guid ID { get; set; }
@@ -286,36 +270,3 @@ public ImageAssetViewModelSerializable() { }
public ImageViewModel ToImageViewModel() => new(Title, AltText, Dimensions.Width, Dimensions.Height, URL) { ID = ID };
}
-
-public class SerializableMediaFileUrl
-{
- public string RelativePath { get; set; } = "";
- public string DirectPath { get; set; } = "";
- public bool IsImage { get; set; }
-
- public SerializableMediaFileUrl(IMediaFileUrl url)
- {
- RelativePath = url.RelativePath;
- DirectPath = url.DirectPath;
- IsImage = url.IsImage;
- }
-
- public SerializableMediaFileUrl() { }
-
- public DefaultMediaFileUrl ToMediaFileUrl() =>
- new()
- {
- DirectPath = DirectPath,
- IsImage = IsImage,
- RelativePath = RelativePath,
- QueryStringParameters = []
- };
-}
-
-public class DefaultMediaFileUrl : IMediaFileUrl
-{
- public string RelativePath { get; set; } = "";
- public string DirectPath { get; set; } = "";
- public NameValueCollection QueryStringParameters { get; set; } = [];
- public bool IsImage { get; set; }
-}
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchService.cs b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchService.cs
index 1346ef9a..5a45096e 100644
--- a/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchService.cs
+++ b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchService.cs
@@ -2,6 +2,7 @@
using Kentico.Xperience.Lucene.Core.Indexing;
using Kentico.Xperience.Lucene.Core.Search;
using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
using Lucene.Net.Facet;
using Lucene.Net.Search;
using Lucene.Net.Util;
@@ -16,9 +17,12 @@ public BlogSearchRequest(HttpRequest request)
{
var query = request.Query;
- BlogType = query.TryGetValue("blogType", out var facetValues)
- ? facetValues.ToString()
- : "";
+ BlogTypes = query.TryGetValue("blogTypes", out var blogTypeValues)
+ ? blogTypeValues.WhereNotNull().ToList()
+ : [];
+ DXTopics = query.TryGetValue("dxTopics", out var topicValues)
+ ? topicValues.WhereNotNull().ToList()
+ : [];
SearchText = query.TryGetValue("query", out var queryValues)
? queryValues.ToString()
: "";
@@ -38,7 +42,8 @@ public BlogSearchRequest(string sortBy, int pageSize)
PageSize = pageSize;
}
- public string BlogType { get; } = "";
+ public IEnumerable BlogTypes { get; } = [];
+ public IEnumerable DXTopics { get; } = [];
public string SearchText { get; } = "";
public string SortBy { get; } = "";
public int PageNumber { get; } = 1;
@@ -48,9 +53,56 @@ public BlogSearchRequest(string sortBy, int pageSize)
public bool AreFiltersDefault => string.IsNullOrWhiteSpace(SearchText) && AuthorMemberID < 1;
}
-public class BlogSearchResultViewModel : LuceneSearchResultModel
+public class BlogSearchResultsViewModel
{
- public string SortBy { get; set; } = "";
+ public string Query { get; init; } = "";
+ public IEnumerable Hits { get; } = [];
+ public int TotalHits { get; set; }
+ public int TotalPages { get; set; }
+ public int PageSize { get; init; }
+ public int Page { get; init; }
+ public LabelAndValue[] BlogTypes { get; } = [];
+ public LabelAndValue[] BlogDXTopics { get; } = [];
+ public string SortBy { get; init; } = "";
+
+ public static BlogSearchResultsViewModel Empty(BlogSearchRequest request) => new()
+ {
+ Page = request.PageNumber,
+ PageSize = request.PageSize,
+ Query = request.SearchText,
+ SortBy = request.SortBy,
+ };
+
+ public BlogSearchResultsViewModel(TopDocs topDocs, MultiFacets facets, BlogSearchRequest request, Func retrieveDoc)
+ {
+ /*
+ * This is performing "fake" paging. We request all the results and then
+ * offset/limit from there.
+ * This is normal for Lucene.NET because the ScoreDocs are very lightweight
+ * until they are actually retrieved from the index using searcher.Doc()
+ * https://stackoverflow.com/a/8287427/939634
+ */
+ int pageSize = Math.Max(1, request.PageSize);
+ int pageNumber = Math.Max(1, request.PageNumber);
+ int offset = pageSize * (pageNumber - 1);
+ int limit = pageSize;
+
+ Query = request.SearchText ?? "";
+ Page = pageNumber;
+ PageSize = pageSize;
+ TotalPages = topDocs.TotalHits <= 0 ? 0 : ((topDocs.TotalHits - 1) / pageSize) + 1;
+ TotalHits = topDocs.TotalHits;
+ Hits = topDocs.ScoreDocs
+ .Skip(offset)
+ .Take(limit)
+ .Select(d => BlogSearchIndexModel.FromDocument(retrieveDoc(d)))
+ .ToList();
+ BlogTypes = facets.GetTopChildren(100, nameof(BlogSearchIndexModel.BlogTypeFacet))?.LabelValues.ToArray() ?? [];
+ BlogDXTopics = facets.GetTopChildren(100, nameof(BlogSearchIndexModel.DXTopicsFacets))?.LabelValues.ToArray() ?? [];
+ SortBy = request.SortBy;
+ }
+
+ private BlogSearchResultsViewModel() { }
}
public class BlogSearchService(
@@ -67,72 +119,33 @@ public class BlogSearchService(
private readonly BlogSearchIndexingStrategy blogSearchStrategy = blogSearchStrategy;
private readonly ILuceneIndexManager indexManager = indexManager;
- public LuceneSearchResultModel SearchBlog(BlogSearchRequest request)
+ public BlogSearchResultsViewModel SearchBlog(BlogSearchRequest request)
{
var index = indexManager.GetRequiredIndex(BlogSearchIndexModel.IndexName);
var query = GetBlogTermQuery(request);
- var combinedQuery = new BooleanQuery
- {
- { query, Occur.MUST }
- };
-
- if (request.BlogType is string facet)
- {
- var drillDownQuery = new DrillDownQuery(blogSearchStrategy.FacetsConfigFactory());
-
- string[] subFacets = facet.Split(';', StringSplitOptions.RemoveEmptyEntries);
-
- foreach (string subFacet in subFacets)
+ var combinedQuery = AddFacetsToQuery(
+ request,
+ new BooleanQuery
{
- drillDownQuery.Add(nameof(BlogSearchIndexModel.BlogTypeFacetField), subFacet);
- }
-
- combinedQuery.Add(drillDownQuery, Occur.MUST);
- }
+ { query, Occur.MUST }
+ });
try
{
return luceneSearchService.UseSearcherWithFacets(
index,
- query, 20,
+ combinedQuery, 20,
(searcher, facets) =>
{
var sortOptions = GetSortOption(request.SortBy);
- var chosenSubFacets = new List();
- /*
- * This is performing "fake" paging. We request all the results and then
- * offset/limit from there.
- * This is normal for Lucene.NET because the ScoreDocs are very lightweight
- * until they are actually retrieved from the index using searcher.Doc()
- * https://stackoverflow.com/a/8287427/939634
- */
- int pageSize = Math.Max(1, request.PageSize);
- int pageNumber = Math.Max(1, request.PageNumber);
- int offset = pageSize * (pageNumber - 1);
- int limit = pageSize;
TopDocs topDocs = sortOptions is null
? topDocs = searcher.Search(combinedQuery, MAX_RESULTS)
: topDocs = searcher.Search(combinedQuery, MAX_RESULTS, new Sort(sortOptions));
- return new BlogSearchResultViewModel
- {
- Query = request.SearchText ?? "",
- Page = pageNumber,
- PageSize = pageSize,
- TotalPages = topDocs.TotalHits <= 0 ? 0 : ((topDocs.TotalHits - 1) / pageSize) + 1,
- TotalHits = topDocs.TotalHits,
- Hits = topDocs.ScoreDocs
- .Skip(offset)
- .Take(limit)
- .Select(d => BlogSearchIndexModel.FromDocument(searcher.Doc(d.Doc)))
- .ToList(),
- Facet = request.BlogType,
- Facets = facets?.GetTopChildren(10, nameof(BlogSearchIndexModel.BlogTypeFacetField), [.. chosenSubFacets])?.LabelValues.ToArray(),
- SortBy = request.SortBy
- };
+ return new BlogSearchResultsViewModel(topDocs, facets, request, d => searcher.Doc(d.Doc));
}
);
}
@@ -140,19 +153,30 @@ public LuceneSearchResultModel SearchBlog(BlogSearchReques
{
log.LogException(nameof(BlogSearchService), "BLOG_SEARCH_FAILURE", ex);
- return new BlogSearchResultViewModel
- {
- Facet = null,
- Facets = [],
- Hits = [],
- Page = request.PageNumber,
- PageSize = request.PageSize,
- Query = request.SearchText,
- SortBy = request.SortBy,
- TotalHits = 0,
- TotalPages = 0
- };
+ return BlogSearchResultsViewModel.Empty(request);
+ }
+ }
+
+ private Query AddFacetsToQuery(BlogSearchRequest request, Query baseQuery)
+ {
+ if (!request.BlogTypes.Any() && !request.DXTopics.Any())
+ {
+ return baseQuery;
+ }
+
+ var drillDownQuery = new DrillDownQuery(blogSearchStrategy.FacetsConfigFactory(), baseQuery);
+
+ foreach (string blogType in request.BlogTypes)
+ {
+ drillDownQuery.Add(nameof(BlogSearchIndexModel.BlogTypeFacet), blogType.ToLowerInvariant());
}
+
+ foreach (string topic in request.DXTopics)
+ {
+ drillDownQuery.Add(nameof(BlogSearchIndexModel.DXTopicsFacets), topic.ToLowerInvariant());
+ }
+
+ return drillDownQuery;
}
private static Query GetBlogTermQuery(BlogSearchRequest request)
diff --git a/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchViewComponent.cs b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchViewComponent.cs
new file mode 100644
index 00000000..816d8a15
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/Features/Blog/Search/BlogSearchViewComponent.cs
@@ -0,0 +1,120 @@
+using System.Collections.Immutable;
+using EnumsNET;
+using Kentico.Community.Portal.Web.Components.ViewComponents.Pagination;
+using Kentico.Community.Portal.Web.Features.QAndA.Search;
+using Kentico.Community.Portal.Web.Infrastructure.Search;
+using Kentico.Community.Portal.Web.Rendering;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Kentico.Community.Portal.Web.Features.Blog.Search;
+
+public class BlogSearchViewComponent(IMediator mediator, BlogSearchService searchService) : ViewComponent
+{
+ private readonly IMediator mediator = mediator;
+ private readonly BlogSearchService searchService = searchService;
+
+ public async Task InvokeAsync()
+ {
+ var request = new BlogSearchRequest(HttpContext.Request);
+ var taxonomies = await mediator.Send(new BlogPostTaxonomiesQuery());
+ var searchResult = searchService.SearchBlog(request);
+ var model = new BlogSearchViewModel(request, searchResult, taxonomies);
+
+ return View("~/Features/Blog/Search/BlogSearch.cshtml", model);
+ }
+}
+
+public class BlogSearchViewModel : IPagedViewModel
+{
+ public IReadOnlyList BlogPosts { get; }
+ public string? Query { get; }
+ public string SortBy { get; }
+ public ImmutableList DXTopics { get; }
+ public ImmutableList BlogTypes { get; }
+ public int TotalAppliedFilters { get; }
+ [HiddenInput]
+ public int Page { get; set; } = 0;
+ public int TotalPages { get; set; } = 0;
+
+ public Dictionary GetRouteData(int page) =>
+ new()
+ {
+ { "query", Query },
+ { "page", page.ToString() },
+ { "sortBy", SortBy },
+ { "blogTypes", string.Join(",", BlogTypes) }
+ };
+
+ public BlogSearchViewModel(BlogSearchRequest request, BlogSearchResultsViewModel result, BlogPostTaxonomiesQueryResponse taxonomies)
+ {
+ BlogPosts = result
+ .Hits
+ .Select(result => new BlogPostSearchResultViewModel(result))
+ .ToList();
+
+ Page = request.PageNumber;
+ Query = request.SearchText;
+ SortBy = request.SortBy;
+ BlogTypes = [.. taxonomies.Types
+ .Select(x => new FacetOption()
+ {
+ Label = x.DisplayName,
+ Value = x.NormalizedName,
+ Count = result
+ .BlogTypes
+ .FirstOrDefault(y => y.Label.Equals(x.NormalizedName, StringComparison.InvariantCultureIgnoreCase))?.Value ?? 0,
+ IsSelected = request
+ .BlogTypes
+ .Contains(x.NormalizedName, StringComparer.OrdinalIgnoreCase)
+ })
+ .Where(x => x.Count != 0)
+ .OrderBy(f => f.Label)];
+ DXTopics = [.. taxonomies.DXTopics
+ .Select(x => new FacetOption()
+ {
+ Label = x.DisplayName,
+ Value = x.NormalizedName,
+ Count = result
+ .BlogDXTopics
+ .FirstOrDefault(y => y.Label.Equals(x.NormalizedName, StringComparison.InvariantCultureIgnoreCase))?.Value ?? 0,
+ IsSelected = request
+ .DXTopics
+ .Contains(x.NormalizedName, StringComparer.OrdinalIgnoreCase)
+ })
+ .Where(x => x.Count != 0)
+ .OrderBy(f => f.Label)];
+ TotalAppliedFilters = BlogTypes.Count(t => t.IsSelected) + DXTopics.Count(t => t.IsSelected);
+ TotalPages = result?.TotalPages ?? 0;
+ }
+}
+
+public class BlogPostSearchResultViewModel
+{
+ public string Title { get; } = "";
+ public DateTime Date { get; }
+ public Maybe TeaserImage { get; }
+ public BlogPostAuthorViewModel Author { get; }
+ public string ShortDescription { get; } = "";
+ public string LinkPath { get; } = "";
+ public string BlogType { get; } = "";
+ public IEnumerable DXTopics { get; } = [];
+
+ public BlogPostSearchResultViewModel(BlogSearchIndexModel model)
+ {
+ Author = new()
+ {
+ ID = model.AuthorMemberID,
+ Name = model.AuthorName,
+ Photo = Maybe.From(model.AuthorAvatarImage!).Map(i => i.ToImageViewModel()),
+ };
+
+ Title = model.Title;
+ Date = model.PublishedDate;
+ LinkPath = model.Url;
+ ShortDescription = model.ShortDescription;
+ TeaserImage = Maybe.From(model.TeaserImage!).Map(i => i.ToImageViewModel());
+ BlogType = model.BlogType;
+ DXTopics = model.DXTopics;
+ }
+}
diff --git a/src/Kentico.Community.Portal.Web/Features/Members/Badges/MemberBadgeService.cs b/src/Kentico.Community.Portal.Web/Features/Members/Badges/MemberBadgeService.cs
index 4b7a2bc4..272d9590 100644
--- a/src/Kentico.Community.Portal.Web/Features/Members/Badges/MemberBadgeService.cs
+++ b/src/Kentico.Community.Portal.Web/Features/Members/Badges/MemberBadgeService.cs
@@ -1,7 +1,7 @@
using System.Collections.Frozen;
using Kentico.Community.Portal.Core.Modules;
using Kentico.Community.Portal.Web.Features.Accounts;
-using Kentico.Community.Portal.Web.Features.QAndA;
+using Kentico.Community.Portal.Web.Features.QAndA.Search;
using Kentico.Community.Portal.Web.Rendering;
using MediatR;
diff --git a/src/Kentico.Community.Portal.Web/Features/Members/MemberEmailService.cs b/src/Kentico.Community.Portal.Web/Features/Members/MemberEmailService.cs
new file mode 100644
index 00000000..5f253144
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/Features/Members/MemberEmailService.cs
@@ -0,0 +1,148 @@
+using CMS.ContentEngine.Internal;
+using CMS.DataEngine;
+using CMS.EmailEngine;
+using CMS.EmailLibrary;
+using CMS.EmailLibrary.Internal;
+using CMS.EmailMarketing.Internal;
+using Kentico.Community.Portal.Web.Membership;
+using Kentico.Xperience.Admin.DigitalMarketing.Internal;
+
+namespace Kentico.Community.Portal.Web.Features.Members;
+
+public class MemberEmailConfiguration
+{
+ public Dictionary ContextItems { get; }
+ public string EmailConfigurationName { get; }
+
+ public static MemberEmailConfiguration RegistrationConfirmation(string confirmationURL) =>
+ new(new() { { "TOKEN_ConfirmationURL", confirmationURL } }, "MemberRegistrationEmailConfirmation-6wjw8hge");
+
+ public static MemberEmailConfiguration ResetPassword(string resetURL) =>
+ new(new() { { "TOKEN_PasswordResetURL", resetURL } }, "MemberResetPasswordConfirmation-ahw9v8cj");
+
+ private MemberEmailConfiguration(Dictionary contextItems, string configurationName)
+ {
+ ContextItems = contextItems;
+ EmailConfigurationName = configurationName;
+ }
+}
+
+public interface IMemberEmailService
+{
+ Task SendEmail(CommunityMember member, MemberEmailConfiguration configuration);
+}
+
+public class MemberEmailService(
+ IInfoProvider emailConfigurationProvider,
+ IEmailContentResolver emailContentResolver,
+ IEmailService emailService,
+ IEmailTemplateMergeService mergeService,
+ IEmailChannelLanguageRetriever languageRetriever,
+ IContentItemDataInfoRetriever dataRetriever,
+ IEmailChannelSenderEmailProvider senderInfoProvider,
+ IInfoProvider contentItems,
+ IInfoProvider emailChannels,
+ IInfoProvider emailChannelSenders
+) : IMemberEmailService
+{
+ private readonly IInfoProvider emailConfigurationProvider = emailConfigurationProvider;
+ private readonly IEmailContentResolver emailContentResolver = emailContentResolver;
+ private readonly IEmailService emailService = emailService;
+ private readonly IEmailTemplateMergeService mergeService = mergeService;
+ private readonly IEmailChannelLanguageRetriever languageRetriever = languageRetriever;
+ private readonly IContentItemDataInfoRetriever dataRetriever = dataRetriever;
+ private readonly IEmailChannelSenderEmailProvider senderInfoProvider = senderInfoProvider;
+ private readonly IInfoProvider contentItems = contentItems;
+ private readonly IInfoProvider emailChannels = emailChannels;
+ private readonly IInfoProvider emailChannelSenders = emailChannelSenders;
+
+ public async Task SendEmail(CommunityMember member, MemberEmailConfiguration configuration)
+ {
+ var recipient = new Recipient
+ {
+ FirstName = member.FirstName,
+ LastName = member.LastName,
+ Email = member.Email
+ };
+
+ var dataContext = new CustomValueDataContext
+ {
+ Recipient = recipient,
+ Items = configuration.ContextItems
+ };
+
+ var emailConfig = await emailConfigurationProvider
+ .GetAsync(configuration.EmailConfigurationName);
+
+ string mergedTemplate = await mergeService
+ .GetMergedTemplateWithEmailData(emailConfig, false);
+
+ string emailBody = await emailContentResolver.Resolve(
+ emailConfig,
+ mergedTemplate,
+ EmailContentFilterType.Sending,
+ dataContext);
+
+ var contentItem = await contentItems
+ .GetAsync(emailConfig.EmailConfigurationContentItemID);
+ var contentLanguage = await languageRetriever
+ .GetEmailChannelLanguageInfoOrThrow(emailConfig.EmailConfigurationEmailChannelID);
+ var data = await dataRetriever
+ .GetContentItemData(contentItem, contentLanguage.ContentLanguageID, false);
+ var emailFieldValues = new EmailContentTypeSpecificFieldValues(data);
+
+ var emailChannel = (await emailChannels.Get()
+ .WhereEquals(
+ nameof(EmailChannelInfo.EmailChannelID),
+ emailConfig.EmailConfigurationEmailChannelID)
+ .GetEnumerableTypedResultAsync())
+ .FirstOrDefault();
+
+ if (emailChannel is null)
+ {
+ throw new Exception($"There is not email channel for the email configuration [{emailConfig.EmailConfigurationID}]");
+ }
+
+ var sender = await emailChannelSenders
+ .GetAsync(emailFieldValues.EmailSenderID);
+ string senderEmail = await senderInfoProvider
+ .GetEmailAddress(emailChannel.EmailChannelID, sender.EmailChannelSenderName);
+
+ var emailMessage = new EmailMessage
+ {
+ From = $"\"{sender.EmailChannelSenderDisplayName}\" <{senderEmail}>",
+ Recipients = recipient.Email,
+ Subject = emailFieldValues.EmailSubject,
+ Body = emailBody,
+ PlainTextBody = emailFieldValues.EmailPlainText,
+ EmailConfigurationID = emailConfig.EmailConfigurationID,
+ MailoutGuid = dataContext.MailoutGuid
+ };
+
+ await emailService.SendEmail(emailMessage);
+ }
+}
+
+public class CustomValueDataContext : FormAutoresponderEmailDataContext
+{
+ public Dictionary Items { get; set; } = [];
+}
+
+public class CustomValueFilter : IEmailContentFilter
+{
+ public Task Apply(
+ string text,
+ EmailConfigurationInfo email,
+ IEmailDataContext dataContext)
+ {
+ if (dataContext is CustomValueDataContext customValueContext)
+ {
+ foreach (var (key, val) in customValueContext.Items)
+ {
+ text = text.Replace(key, val);
+ }
+ }
+
+ return Task.FromResult(text);
+ }
+}
diff --git a/src/Kentico.Community.Portal.Web/Features/PasswordRecovery/PasswordRecoveryController.cs b/src/Kentico.Community.Portal.Web/Features/PasswordRecovery/PasswordRecoveryController.cs
index 30a32f71..2df101d0 100644
--- a/src/Kentico.Community.Portal.Web/Features/PasswordRecovery/PasswordRecoveryController.cs
+++ b/src/Kentico.Community.Portal.Web/Features/PasswordRecovery/PasswordRecoveryController.cs
@@ -1,5 +1,5 @@
using System.Web;
-using CMS.EmailEngine;
+using Kentico.Community.Portal.Web.Features.Members;
using Kentico.Community.Portal.Web.Features.Registration;
using Kentico.Community.Portal.Web.Infrastructure;
using Kentico.Community.Portal.Web.Membership;
@@ -7,7 +7,6 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
-using Microsoft.Extensions.Options;
namespace Kentico.Community.Portal.Web.Features.PasswordRecovery;
@@ -16,9 +15,8 @@ public class PasswordRecoveryController(
UserManager userManager,
SignInManager signInManager,
IStringLocalizer localizer,
- IOptions systemEmailOptions,
- IEmailService emailService,
- WebPageMetaService metaService) : Controller
+ WebPageMetaService metaService,
+ IMemberEmailService emailService) : Controller
{
/**
* TODO: update View Location Expander to find this via conventions
@@ -26,11 +24,10 @@ public class PasswordRecoveryController(
private const string VIEW_PATH_ERROR = "~/Features/PasswordRecovery/ResetPasswordError.cshtml";
private readonly IStringLocalizer localizer = localizer;
- private readonly IEmailService emailService = emailService;
- private readonly SystemEmailOptions systemEmailOptions = systemEmailOptions.Value;
private readonly UserManager userManager = userManager;
private readonly SignInManager signInManager = signInManager;
private readonly WebPageMetaService metaService = metaService;
+ private readonly IMemberEmailService emailService = emailService;
///
/// Step 1
@@ -58,28 +55,28 @@ public async Task RequestRecoveryEmail(RequestRecoveryEmailViewMod
return PartialView("~/Features/PasswordRecovery/_RequestRecoveryEmailForm.cshtml", model);
}
- var user = await userManager.FindByEmailAsync(model.Email);
- if (user is null)
+ var member = await userManager.FindByEmailAsync(model.Email);
+ if (member is null)
{
return PartialView("~/Features/PasswordRecovery/_RequestRecoveryEmailForm.cshtml", new RequestRecoveryEmailViewModel());
}
- if (!user.Enabled)
+ if (!member.Enabled)
{
return PartialView("~/Features/Registration/EmailConfirmation.cshtml", new EmailConfirmationViewModel
{
State = EmailConfirmationState.Failure_NotYetConfirmed,
Message = "You cannot reset your password until you confirm your email address.",
SendButtonText = "Send confirmation email",
- Username = user.UserName!
+ Username = member.UserName!
});
}
- string token = await userManager.GeneratePasswordResetTokenAsync(user);
+ string token = await userManager.GeneratePasswordResetTokenAsync(member);
string? resetURL = Url.Action(
nameof(SetNewPassword),
"PasswordRecovery",
- new { userId = user.Id, token = HttpUtility.UrlEncode(token) },
+ new { userId = member.Id, token = HttpUtility.UrlEncode(token) },
Request.Scheme);
if (resetURL is null)
@@ -90,17 +87,7 @@ public async Task RequestRecoveryEmail(RequestRecoveryEmailViewMod
}
await emailService
- .SendEmail(new EmailMessage()
- {
- From = $"no-reply@{systemEmailOptions.SendingDomain}",
- Recipients = model.Email,
- Subject = "Password reset request",
- Body = $"""
- To reset your account's password, click here.
- You can also copy and paste this URL into your browser.
- {resetURL}
- """
- });
+ .SendEmail(member, MemberEmailConfiguration.ResetPassword(resetURL));
return PartialView("~/Features/PasswordRecovery/_RequestRecoveryEmailForm.cshtml", new RequestRecoveryEmailViewModel());
}
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearch.cshtml b/src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearch.cshtml
deleted file mode 100644
index 20e717c5..00000000
--- a/src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearch.cshtml
+++ /dev/null
@@ -1,143 +0,0 @@
-@model Kentico.Community.Portal.Web.Features.QAndA.QAndASearchViewModel
-
-
-
-
-
-
-
-
- @if (Model.Questions.Count == 0 && Model.TotalPages == 0)
- {
-
-
No results could be found.
-
- }
-
- @foreach (var question in Model.Questions)
- {
-
-
- @if (question.HasAcceptedResponse)
- {
-
- Has Accepted Response
-
- }
-
-
-
-
- Discussion created
@question.DateCreated.ToString("d", View.Culture) @question.DateCreated.ToString("t")
-
-
-
-
- }
-
-
-
-
-
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearchViewComponent.cs b/src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearchViewComponent.cs
deleted file mode 100644
index afb48440..00000000
--- a/src/Kentico.Community.Portal.Web/Features/QAndA/Components/Search/QAndASearchViewComponent.cs
+++ /dev/null
@@ -1,103 +0,0 @@
-using CMS.ContentEngine;
-using Kentico.Community.Portal.Core;
-using Kentico.Community.Portal.Core.Modules;
-using Kentico.Community.Portal.Web.Components.ViewComponents.Pagination;
-using Kentico.Community.Portal.Web.Features.Members.Badges;
-using Kentico.Community.Portal.Web.Features.QAndA.Search;
-using Kentico.Community.Portal.Web.Infrastructure.Search;
-using Microsoft.AspNetCore.Mvc;
-
-namespace Kentico.Community.Portal.Web.Features.QAndA;
-
-public class QAndASearchViewComponent(
- QAndASearchService searchService,
- ITaxonomyRetriever taxonomyRetriever,
- MemberBadgeService memberBadgeService) : ViewComponent
-{
- private readonly QAndASearchService searchService = searchService;
- private readonly ITaxonomyRetriever taxonomyRetriever = taxonomyRetriever;
- private readonly MemberBadgeService memberBadgeService = memberBadgeService;
-
- public async Task InvokeAsync()
- {
- var request = new QAndASearchRequest(HttpContext.Request);
-
- var searchResult = searchService.SearchQAndA(request);
- var chosenFacets = request.DiscussionType.ToLower().Split(";", StringSplitOptions.RemoveEmptyEntries)?.ToList() ?? [];
- var viewModels = searchResult.Hits.Select(QAndAPostViewModel.GetModel).ToList();
-
- viewModels = await memberBadgeService.AddSelectedBadgesToQAndA(viewModels);
-
- var taxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.QAndADiscussionTypeTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE);
-
- var vm = new QAndASearchViewModel
- {
- Questions = viewModels,
- Page = request.PageNumber,
- SortBy = request.SortBy,
- Query = request.SearchText,
- TotalPages = searchResult.TotalPages,
- OnlyAcceptedResponses = request.OnlyAcceptedResponses,
- DiscussionType = request.DiscussionType,
- DiscussionTypes = [.. taxonomy.Tags
- .Select(x => new FacetOption()
- {
- Label = x.Title,
- Value = searchResult?.Facets?.FirstOrDefault(y => y.Label.Equals(x.Title, StringComparison.InvariantCultureIgnoreCase))?.Value ?? 0,
- IsSelected = chosenFacets.Contains(x.Title, StringComparer.OrdinalIgnoreCase)
- })
- .Where(x => x.Value != 0)
- .OrderBy(f => f.Label)],
- };
-
- return View("~/Features/QAndA/Components/Search/QAndASearch.cshtml", vm);
- }
-}
-
-public class QAndASearchViewModel : IPagedViewModel
-{
- public IReadOnlyList Questions { get; set; } = [];
-
- public string? Query { get; set; } = "";
- [HiddenInput]
- public int Page { get; set; }
- public string SortBy { get; set; } = "";
- [HiddenInput]
- public string DiscussionType { get; set; } = "";
- public List DiscussionTypes { get; set; } = [];
- public bool OnlyAcceptedResponses { get; set; } = false;
- public int TotalPages { get; set; }
-
- public Dictionary GetRouteData(int page) =>
- new()
- {
- { "query", Query },
- { "page", page.ToString() },
- { "sortBy", SortBy }
- };
-}
-
-public class QAndAPostViewModel
-{
- public int ID { get; set; }
- public string Title { get; set; } = "";
- public string LinkPath { get; set; } = "";
- public DateTime DateCreated { get; set; }
- public int ResponseCount { get; set; }
- public DateTime LatestResponseDate { get; set; }
- public bool HasAcceptedResponse { get; set; }
- public QAndAAuthorViewModel Author { get; set; } = new();
-
- public static QAndAPostViewModel GetModel(QAndASearchIndexModel result) => new()
- {
- Title = result.Title,
- DateCreated = result.PublishedDate,
- ResponseCount = result.ResponseCount,
- LatestResponseDate = result.LatestResponseDate,
- HasAcceptedResponse = result.HasAcceptedResponse,
- Author = new(result.AuthorMemberID, result.AuthorFullName, result.AuthorUsername, result.AuthorAttributes),
- LinkPath = result.Url,
- ID = result.ID
- };
-}
-
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndAQuestionPageTaxonomiesQuery.cs b/src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndAQuestionPageTaxonomiesQuery.cs
deleted file mode 100644
index c3383e71..00000000
--- a/src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndAQuestionPageTaxonomiesQuery.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using CMS.ContentEngine;
-using Kentico.Community.Portal.Core;
-using Kentico.Community.Portal.Core.Modules;
-using Kentico.Community.Portal.Core.Operations;
-
-namespace Kentico.Community.Portal.Web.Features.QAndA;
-
-public record QAndATaxonomiesQuery() : IQuery;
-
-public record QAndATaxonomy(Guid Guid, string Value, string DisplayName);
-public record QAndATaxonomiesQueryResponse(IReadOnlyList DiscussionTypes, IReadOnlyList DXTopics);
-public class QAndATaxonomiesQueryHandler(DataItemQueryTools tools, ITaxonomyRetriever taxonomyRetriever) : DataItemQueryHandler(tools)
-{
- private readonly ITaxonomyRetriever taxonomyRetriever = taxonomyRetriever;
-
- public override async Task Handle(QAndATaxonomiesQuery request, CancellationToken cancellationToken = default)
- {
- var discussionTypeTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.QAndADiscussionTypeTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE, cancellationToken);
- var discussionTypeTags = discussionTypeTaxonomy.Tags
- .Select(tag => new QAndATaxonomy(tag.Identifier, tag.Name, tag.Title))
- .ToList();
-
- var dxTopicTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.DXTopicTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE, cancellationToken);
- var dxTopicTags = dxTopicTaxonomy.Tags
- .Select(tag => new QAndATaxonomy(tag.Identifier, tag.Name, tag.Title))
- .ToList();
-
- return new QAndATaxonomiesQueryResponse(discussionTypeTags, dxTopicTags);
- }
-
- protected override ICacheDependencyKeysBuilder AddDependencyKeys(QAndATaxonomiesQuery query, QAndATaxonomiesQueryResponse result, ICacheDependencyKeysBuilder builder) =>
- builder
- .Object(TaxonomyInfo.OBJECT_TYPE, SystemTaxonomies.QAndADiscussionTypeTaxonomy.CodeName)
- .Object(TaxonomyInfo.OBJECT_TYPE, SystemTaxonomies.DXTopicTaxonomy.CodeName)
- .Collection(result.DiscussionTypes, (i, b) => b.Object(TagInfo.OBJECT_TYPE, i.Value))
- .Collection(result.DXTopics, (i, b) => b.Object(TagInfo.OBJECT_TYPE, i.Value));
-}
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndATaxonomiesQuery.cs b/src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndATaxonomiesQuery.cs
new file mode 100644
index 00000000..4f1e41f7
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/Features/QAndA/Operations/QAndATaxonomiesQuery.cs
@@ -0,0 +1,56 @@
+using System.Text.RegularExpressions;
+using CMS.ContentEngine;
+using Kentico.Community.Portal.Core;
+using Kentico.Community.Portal.Core.Modules;
+using Kentico.Community.Portal.Core.Operations;
+
+namespace Kentico.Community.Portal.Web.Features.QAndA;
+
+public record QAndATaxonomiesQuery() : IQuery;
+
+public class QAndATaxonomy
+{
+ public Guid Guid { get; set; }
+ public string Name { get; set; }
+ public string NormalizedName { get; set; }
+ public string DisplayName { get; set; }
+
+ public QAndATaxonomy(Tag tag)
+ {
+ Guid = tag.Identifier;
+ Name = tag.Name;
+ NormalizedName = RegexTools.AlphanumericRegex().Replace(tag.Name, "").ToLowerInvariant();
+ DisplayName = tag.Title;
+ }
+}
+
+public record QAndATaxonomiesQueryResponse(IReadOnlyList Types, IReadOnlyList DXTopics);
+public class QAndATaxonomiesQueryHandler(DataItemQueryTools tools, ITaxonomyRetriever taxonomyRetriever) : DataItemQueryHandler(tools)
+{
+ private readonly ITaxonomyRetriever taxonomyRetriever = taxonomyRetriever;
+
+ public override async Task Handle(QAndATaxonomiesQuery request, CancellationToken cancellationToken = default)
+ {
+ var typeTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.QAndADiscussionTypeTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE, cancellationToken);
+ var typeTags = typeTaxonomy.Tags
+ .Select(tag => new QAndATaxonomy(tag))
+ .ToList();
+
+ var topicsTaxonomy = await taxonomyRetriever.RetrieveTaxonomy(SystemTaxonomies.DXTopicTaxonomy.CodeName, PortalWebSiteChannel.DEFAULT_LANGUAGE, cancellationToken);
+ var topicTags = topicsTaxonomy.Tags
+ .Select(tag => new QAndATaxonomy(tag))
+ .ToList();
+
+ return new QAndATaxonomiesQueryResponse(typeTags, topicTags);
+ }
+
+ protected override ICacheDependencyKeysBuilder AddDependencyKeys(QAndATaxonomiesQuery query, QAndATaxonomiesQueryResponse result, ICacheDependencyKeysBuilder builder) =>
+ builder.Object(TaxonomyInfo.OBJECT_TYPE, SystemTaxonomies.BlogTypeTaxonomy.CodeName)
+ .Collection(result.Types, i => builder.Object(TagInfo.OBJECT_TYPE, i.Name));
+}
+
+public partial class RegexTools
+{
+ [GeneratedRegex(@"[^a-zA-Z0-9]")]
+ public static partial Regex AlphanumericRegex();
+}
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/QAndAQuestionPageController.cs b/src/Kentico.Community.Portal.Web/Features/QAndA/QAndAQuestionPageController.cs
index 8f67df47..f7b31677 100644
--- a/src/Kentico.Community.Portal.Web/Features/QAndA/QAndAQuestionPageController.cs
+++ b/src/Kentico.Community.Portal.Web/Features/QAndA/QAndAQuestionPageController.cs
@@ -254,10 +254,7 @@ public QAndAAuthorViewModel(CommunityMember member)
ID = member.Id;
Username = member.UserName!;
FullName = member.FullName;
- AuthorAttributes = new()
- {
- IsMVP = member.IsMVP
- };
+ AuthorAttributes = new();
}
public QAndAAuthorViewModel(AuthorContent author)
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearch.cshtml b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearch.cshtml
new file mode 100644
index 00000000..36525967
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearch.cshtml
@@ -0,0 +1,204 @@
+@using EnumsNET
+@using Kentico.Community.Portal.Web.Features.QAndA.Search
+
+@model QAndASearchViewModel
+
+
\ No newline at end of file
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchIndexModel.cs b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchIndexModel.cs
index a4a57ebd..24d33c1f 100644
--- a/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchIndexModel.cs
+++ b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchIndexModel.cs
@@ -1,20 +1,21 @@
using CMS.ContentEngine;
using CMS.DataEngine;
using CMS.Membership;
+using EnumsNET;
using Kentico.Community.Portal.Core.Modules;
using Kentico.Community.Portal.Web.Infrastructure.Search;
using Kentico.Community.Portal.Web.Membership;
using Kentico.Xperience.Lucene.Core.Indexing;
using Lucene.Net.Documents;
+using Lucene.Net.Documents.Extensions;
using Lucene.Net.Facet;
+using MediatR;
using Newtonsoft.Json;
namespace Kentico.Community.Portal.Web.Features.QAndA.Search;
public class DiscussionAuthorAttributes()
{
- public bool IsMVP { get; set; }
-
public static DiscussionAuthorAttributes Default { get; } = new();
};
@@ -33,9 +34,12 @@ public class QAndASearchIndexModel
public string AuthorUsername { get; set; } = "";
public string AuthorFullName { get; set; } = "";
public DiscussionAuthorAttributes AuthorAttributes { get; set; } = DiscussionAuthorAttributes.Default;
- public bool HasAcceptedResponse { get; set; }
+ public string DiscussionTypeFacet { get; set; } = "";
public string DiscussionType { get; set; } = "";
- public IEnumerable Topics { get; set; } = [];
+ public List DXTopicsFacet { get; set; } = [];
+ public List DXTopics { get; set; } = [];
+ public string DiscussionStatesFacet { get; set; } = "";
+ public string DiscussionState { get; set; } = "";
public int ResponseCount { get; set; } = 0;
public Document ToDocument()
@@ -51,16 +55,21 @@ public Document ToDocument()
new Int32Field(nameof(AuthorMemberID), AuthorMemberID, Field.Store.YES),
new TextField(nameof(AuthorUsername), AuthorUsername, Field.Store.YES),
new TextField(nameof(AuthorFullName), AuthorFullName, Field.Store.YES),
- new Int32Field(nameof(HasAcceptedResponse), HasAcceptedResponse ? 1 : 0, Field.Store.YES),
new Int32Field(nameof(ResponseCount), ResponseCount, Field.Store.YES),
- new FacetField(nameof(DiscussionType), (string.IsNullOrWhiteSpace(DiscussionType) ? "None" : DiscussionType).ToLowerInvariant()),
+ new TextField(nameof(DiscussionType), string.IsNullOrWhiteSpace(DiscussionType) ? "none" : DiscussionType, Field.Store.YES),
+ new TextField(nameof(DiscussionState), string.IsNullOrWhiteSpace(DiscussionState) ? "none" : DiscussionState, Field.Store.YES),
+ new TextField(nameof(DXTopics), string.Join(';', DXTopics), Field.Store.YES),
};
- if (Topics.Any())
+ _ = indexDocument.AddFacetField(nameof(DiscussionTypeFacet), DiscussionTypeFacet);
+
+ foreach (string topic in DXTopicsFacet)
{
- indexDocument.Add(new FacetField(nameof(Topics), Topics.Select(t => t.ToLowerInvariant()).ToArray()));
+ _ = indexDocument.AddFacetField(nameof(DXTopicsFacet), topic);
}
+ _ = indexDocument.AddFacetField(nameof(DiscussionStatesFacet), DiscussionStatesFacet);
+
return indexDocument;
}
@@ -82,11 +91,6 @@ public static QAndASearchIndexModel FromDocument(Document doc)
? authorMemberID
: 0,
AuthorAttributes = JsonConvert.DeserializeObject(doc.Get(nameof(AuthorAttributes) ?? "{ }")) ?? DiscussionAuthorAttributes.Default,
- HasAcceptedResponse = doc.Get(nameof(HasAcceptedResponse)) switch
- {
- "1" => true,
- "0" or _ => false
- },
ResponseCount = int.TryParse(doc.Get(nameof(ResponseCount)), out int answerCount)
? answerCount
: 0,
@@ -99,7 +103,8 @@ public static QAndASearchIndexModel FromDocument(Document doc)
long.TryParse(doc.Get(nameof(LatestResponseDate)), out long responseVal) ? responseVal : DateTools.TicksToUnixTimeMilliseconds(DefaultTime.Ticks)
)),
DiscussionType = doc.Get(nameof(DiscussionType)) ?? "",
- Topics = doc.GetValues(nameof(Topics)) ?? []
+ DiscussionState = doc.Get(nameof(DiscussionState)) ?? "",
+ DXTopics = [.. doc.Get(nameof(DXTopics)).Split(";")],
};
return model;
@@ -110,16 +115,17 @@ public class QAndASearchIndexingStrategy(
IContentQueryExecutor executor,
WebScraperHtmlSanitizer htmlSanitizer,
IInfoProvider memberProvider,
- ITaxonomyRetriever taxonomyRetriever,
- IInfoProvider answerProvider) : DefaultLuceneIndexingStrategy
+ IInfoProvider answerProvider,
+ IMediator mediator) : DefaultLuceneIndexingStrategy
{
public const string IDENTIFIER = "QANDA_SEARCH";
private readonly IContentQueryExecutor executor = executor;
private readonly WebScraperHtmlSanitizer htmlSanitizer = htmlSanitizer;
- private readonly ITaxonomyRetriever taxonomyRetriever = taxonomyRetriever;
private readonly IInfoProvider memberProvider = memberProvider;
private readonly IInfoProvider answerProvider = answerProvider;
+ private readonly IMediator mediator = mediator;
+
public override async Task MapToLuceneDocumentOrNull(IIndexEventItemModel item)
{
@@ -152,10 +158,36 @@ public class QAndASearchIndexingStrategy(
indexModel.PublishedDate = page.QAndAQuestionPageDateCreated != default
? page.QAndAQuestionPageDateCreated
: DateTime.MinValue;
- indexModel.HasAcceptedResponse = page.QAndAQuestionPageAcceptedAnswerDataGUID != default;
+ indexModel.DiscussionStatesFacet = page.QAndAQuestionPageAcceptedAnswerDataGUID switch
+ {
+ var g when g == default => Enums.AsString(DiscussionStates.NoAcceptedAnswer, EnumFormat.Name)?.ToLowerInvariant() ?? "",
+ _ => Enums.AsString(DiscussionStates.HasAcceptedAnswer, EnumFormat.Name)?.ToLowerInvariant() ?? "",
+ };
+ indexModel.DiscussionState = indexModel.DiscussionStatesFacet;
- indexModel.DiscussionType = await GetDiscussionType(page, item);
- indexModel.Topics = await GetTopics(page, item);
+ var taxonomies = await mediator.Send(new QAndATaxonomiesQuery());
+
+ page.QAndAQuestionPageDiscussionType
+ .TryFirst()
+ .Map(t => t.Identifier)
+ .Bind(id => taxonomies.Types
+ .TryFirst(t => t.Guid == id))
+ .Execute(tag =>
+ {
+ indexModel.DiscussionType = tag.DisplayName;
+ indexModel.DiscussionTypeFacet = tag.NormalizedName;
+ });
+ var dxTopics = page
+ .QAndAQuestionPageDXTopics
+ .Select(tagRef => taxonomies.DXTopics
+ .FirstOrDefault(t => tagRef.Identifier == t.Guid))
+ .WhereNotNull();
+
+ foreach (var tag in dxTopics)
+ {
+ indexModel.DXTopics.Add(tag.DisplayName);
+ indexModel.DXTopicsFacet.Add(tag.NormalizedName);
+ }
var answers = (await answerProvider
.Get()
@@ -176,8 +208,9 @@ public override FacetsConfig FacetsConfigFactory()
{
var facetConfig = new FacetsConfig();
- facetConfig.SetMultiValued(nameof(QAndASearchIndexModel.Topics), true);
- facetConfig.SetMultiValued(nameof(QAndASearchIndexModel.DiscussionType), false);
+ facetConfig.SetMultiValued(nameof(QAndASearchIndexModel.DXTopicsFacet), true);
+ facetConfig.SetMultiValued(nameof(QAndASearchIndexModel.DiscussionTypeFacet), false);
+ facetConfig.SetMultiValued(nameof(QAndASearchIndexModel.DiscussionStatesFacet), false);
return facetConfig;
}
@@ -197,7 +230,7 @@ private static async Task GetAuthor(
if (member is not null)
{
var cm = CommunityMember.FromMemberInfo(member);
- return new(cm.Id, cm.UserName!, cm.FullName, new() { IsMVP = cm.IsMVP });
+ return new(cm.Id, cm.UserName!, cm.FullName, new());
}
var b = new ContentItemQueryBuilder()
@@ -210,18 +243,4 @@ private static async Task GetAuthor(
return new(0, author.AuthorContentCodeName, author.FullName, DiscussionAuthorAttributes.Default);
}
-
- private async Task GetDiscussionType(QAndAQuestionPage page, IIndexEventItemModel item)
- {
- var tags = await taxonomyRetriever.RetrieveTags(page.QAndAQuestionPageDiscussionType.Select(t => t.Identifier), item.LanguageName);
-
- return tags.FirstOrDefault()?.Title ?? "";
- }
-
- private async Task> GetTopics(QAndAQuestionPage page, IIndexEventItemModel item)
- {
- var tags = await taxonomyRetriever.RetrieveTags(page.QAndAQuestionPageDXTopics.Select(t => t.Identifier), item.LanguageName);
-
- return tags.Select(t => t.Title);
- }
}
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchService.cs b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchService.cs
index 86365b96..40088d1c 100644
--- a/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchService.cs
+++ b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchService.cs
@@ -1,9 +1,10 @@
+using System.ComponentModel;
using CMS.Core;
using Kentico.Xperience.Lucene.Core.Indexing;
using Kentico.Xperience.Lucene.Core.Search;
using Lucene.Net.Analysis.Standard;
+using Lucene.Net.Documents;
using Lucene.Net.Facet;
-using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Util;
@@ -17,9 +18,15 @@ public QAndASearchRequest(HttpRequest request)
{
var query = request.Query;
- DiscussionType = query.TryGetValue("discussionType", out var facetValues)
- ? facetValues.ToString()
- : "";
+ DiscussionTypes = query.TryGetValue("discussionTypes", out var facetValues)
+ ? facetValues
+ : [];
+ DXTopics = query.TryGetValue("dxTopics", out var topicValues)
+ ? topicValues
+ : [];
+ DiscussionStates = query.TryGetValue("discussionStates", out var discussionStates)
+ ? discussionStates
+ : [];
SearchText = query.TryGetValue("query", out var queryValues)
? queryValues.ToString()
: "";
@@ -37,10 +44,6 @@ public QAndASearchRequest(HttpRequest request)
? a
: 0
: 0;
-
- OnlyAcceptedResponses = query.TryGetValue(nameof(OnlyAcceptedResponses), out var acceptedResponsesValues)
- && acceptedResponsesValues.Count > 0 && bool.TryParse(acceptedResponsesValues[0], out bool ar)
- && ar;
}
public QAndASearchRequest(string sortBy, int pageSize)
@@ -54,16 +57,70 @@ public QAndASearchRequest(string sortBy, int pageSize)
public int PageNumber { get; } = 1;
public int PageSize { get; } = PAGE_SIZE;
public int AuthorMemberID { get; set; }
- public bool OnlyAcceptedResponses { get; set; }
- public string DiscussionType { get; set; } = "";
+ public IEnumerable DiscussionStates { get; } = [];
+ public IEnumerable DiscussionTypes { get; set; } = [];
+ public IEnumerable DXTopics { get; set; } = [];
- public bool AreFiltersDefault => string.IsNullOrWhiteSpace(SearchText) && AuthorMemberID < 1 && !OnlyAcceptedResponses;
+ public bool AreFiltersDefault =>
+ string.IsNullOrWhiteSpace(SearchText)
+ && AuthorMemberID < 1;
+}
+
+public enum DiscussionStates
+{
+ [Description("Has accepted answer")]
+ HasAcceptedAnswer,
+ [Description("No accepted answer")]
+ NoAcceptedAnswer
}
-public class QAndASearchResultViewModel : LuceneSearchResultModel
+public class QAndASearchResult
{
+ public string Query { get; set; } = "";
+ public IEnumerable Hits { get; set; } = [];
+ public int TotalHits { get; set; }
+ public int TotalPages { get; set; }
+ public int PageSize { get; set; }
+ public int Page { get; set; }
+ public LabelAndValue[] DiscussionTypes { get; set; } = [];
+ public LabelAndValue[] DXTopics { get; set; } = [];
+ public LabelAndValue[] DiscussionStates { get; set; } = [];
public string SortBy { get; set; } = "";
+
+ public static QAndASearchResult Empty(QAndASearchRequest request) =>
+ new()
+ {
+ Page = request.PageNumber,
+ PageSize = request.PageSize,
+ Query = request.SearchText,
+ SortBy = request.SortBy,
+ };
+
+ public QAndASearchResult(TopDocs topDocs, MultiFacets facets, QAndASearchRequest request, Func retrieveDoc)
+ {
+ int pageSize = Math.Max(1, request.PageSize);
+ int pageNumber = Math.Max(1, request.PageNumber);
+ int offset = pageSize * (pageNumber - 1);
+ int limit = pageSize;
+
+ Query = request.SearchText ?? "";
+ Page = pageNumber;
+ PageSize = pageSize;
+ TotalPages = topDocs.TotalHits <= 0 ? 0 : ((topDocs.TotalHits - 1) / pageSize) + 1;
+ TotalHits = topDocs.TotalHits;
+ Hits = topDocs.ScoreDocs
+ .Skip(offset)
+ .Take(limit)
+ .Select(d => QAndASearchIndexModel.FromDocument(retrieveDoc(d)))
+ .ToList();
+ DiscussionTypes = facets.GetTopChildren(100, nameof(QAndASearchIndexModel.DiscussionTypeFacet), [])?.LabelValues.ToArray() ?? [];
+ DXTopics = facets.GetTopChildren(100, nameof(QAndASearchIndexModel.DXTopicsFacet), [])?.LabelValues.ToArray() ?? [];
+ DiscussionStates = facets.GetTopChildren(100, nameof(QAndASearchIndexModel.DiscussionStatesFacet), [])?.LabelValues.ToArray() ?? [];
+ SortBy = request.SortBy;
+ }
+
+ private QAndASearchResult() { }
}
public class QAndASearchService(
@@ -80,65 +137,31 @@ public class QAndASearchService(
private readonly QAndASearchIndexingStrategy qAndASearchStrategy = qAndASearchStrategy;
private readonly ILuceneIndexManager indexManager = indexManager;
- public QAndASearchResultViewModel SearchQAndA(QAndASearchRequest request)
+ public QAndASearchResult SearchQAndA(QAndASearchRequest request)
{
var index = indexManager.GetRequiredIndex(QAndASearchIndexModel.IndexName);
var query = GetQAndATermQuery(request);
- var combinedQuery = new BooleanQuery
+ var combinedQuery = FacetsQuery(request, new BooleanQuery
{
{ query, Occur.MUST }
- };
-
- if (request.DiscussionType is string facet)
- {
- var drillDownQuery = new DrillDownQuery(qAndASearchStrategy.FacetsConfigFactory());
-
- string[] subFacets = facet.Split(';', StringSplitOptions.RemoveEmptyEntries);
-
- foreach (string subFacet in subFacets)
- {
- drillDownQuery.Add(nameof(QAndASearchIndexModel.DiscussionType), subFacet);
- }
-
- combinedQuery.Add(drillDownQuery, Occur.MUST);
- }
+ });
try
{
return luceneSearchService.UseSearcherWithFacets(
index,
- query, 20,
+ combinedQuery, 20,
(searcher, facets) =>
{
var sortOptions = GetSortOption(request.SortBy);
- var chosenSubFacets = new List();
- int pageSize = Math.Max(1, request.PageSize);
- int pageNumber = Math.Max(1, request.PageNumber);
- int offset = pageSize * (pageNumber - 1);
- int limit = pageSize;
TopDocs topDocs = sortOptions is null
? topDocs = searcher.Search(combinedQuery, MAX_RESULTS)
: topDocs = searcher.Search(combinedQuery, MAX_RESULTS, new Sort(sortOptions));
- return new QAndASearchResultViewModel
- {
- Query = request.SearchText ?? "",
- Page = pageNumber,
- PageSize = pageSize,
- TotalPages = topDocs.TotalHits <= 0 ? 0 : ((topDocs.TotalHits - 1) / pageSize) + 1,
- TotalHits = topDocs.TotalHits,
- Hits = topDocs.ScoreDocs
- .Skip(offset)
- .Take(limit)
- .Select(d => QAndASearchIndexModel.FromDocument(searcher.Doc(d.Doc)))
- .ToList(),
- Facet = request.DiscussionType,
- Facets = facets?.GetTopChildren(10, nameof(QAndASearchIndexModel.DiscussionType), [.. chosenSubFacets])?.LabelValues.ToArray(),
- SortBy = request.SortBy
- };
+ return new QAndASearchResult(topDocs, facets, request, (d) => searcher.Doc(d.Doc));
}
);
}
@@ -146,18 +169,7 @@ public QAndASearchResultViewModel SearchQAndA(QAndASearch
{
log.LogException(nameof(QAndASearchService), "Q&A_SEARCH_FAILURE", ex);
- return new QAndASearchResultViewModel
- {
- Facet = null,
- Facets = [],
- Hits = [],
- Page = request.PageNumber,
- PageSize = request.PageSize,
- Query = request.SearchText,
- SortBy = request.SortBy,
- TotalHits = 0,
- TotalPages = 0
- };
+ return QAndASearchResult.Empty(request);
}
}
@@ -195,14 +207,34 @@ private static Query GetQAndATermQuery(QAndASearchRequest request)
booleanQuery = AddToTermQuery(booleanQuery, contentShould, 0.1f);
}
- if (request.OnlyAcceptedResponses)
+ return booleanQuery;
+ }
+
+ private Query FacetsQuery(QAndASearchRequest request, Query query)
+ {
+ if (!request.DiscussionTypes.Any() && !request.DXTopics.Any() && !request.DiscussionStates.Any())
{
- var bytes = new BytesRef(NumericUtils.BUF_SIZE_INT32);
- NumericUtils.Int32ToPrefixCoded(int.Parse("1"), 0, bytes);
- booleanQuery.Add(new TermQuery(new Term(nameof(QAndASearchIndexModel.HasAcceptedResponse), bytes)), Occur.MUST);
+ return query;
}
- return booleanQuery;
+ var drillDownQuery = new DrillDownQuery(qAndASearchStrategy.FacetsConfigFactory(), query);
+
+ foreach (string discussionType in request.DiscussionTypes)
+ {
+ drillDownQuery.Add(nameof(QAndASearchIndexModel.DiscussionTypeFacet), discussionType);
+ }
+
+ foreach (string topic in request.DXTopics)
+ {
+ drillDownQuery.Add(nameof(QAndASearchIndexModel.DXTopicsFacet), topic);
+ }
+
+ foreach (string state in request.DiscussionStates)
+ {
+ drillDownQuery.Add(nameof(QAndASearchIndexModel.DiscussionStatesFacet), state);
+ }
+
+ return drillDownQuery;
}
private static BooleanQuery AddToTermQuery(BooleanQuery query, Query textQueryPart, float boost)
diff --git a/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchViewComponent.cs b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchViewComponent.cs
new file mode 100644
index 00000000..6ae95497
--- /dev/null
+++ b/src/Kentico.Community.Portal.Web/Features/QAndA/Search/QAndASearchViewComponent.cs
@@ -0,0 +1,136 @@
+using System.Collections.Immutable;
+using EnumsNET;
+using Kentico.Community.Portal.Web.Components.ViewComponents.Pagination;
+using Kentico.Community.Portal.Web.Features.Members.Badges;
+using Kentico.Community.Portal.Web.Infrastructure.Search;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Kentico.Community.Portal.Web.Features.QAndA.Search;
+
+public class QAndASearchViewComponent(
+ QAndASearchService searchService,
+ MemberBadgeService memberBadgeService,
+ IMediator mediator) : ViewComponent
+{
+ private readonly QAndASearchService searchService = searchService;
+ private readonly MemberBadgeService memberBadgeService = memberBadgeService;
+ private readonly IMediator mediator = mediator;
+
+ public async Task InvokeAsync()
+ {
+ var request = new QAndASearchRequest(HttpContext.Request);
+
+ var searchResult = searchService.SearchQAndA(request);
+ var viewModels = searchResult.Hits.Select(QAndAPostViewModel.GetModel).ToList();
+ var taxonomies = await mediator.Send(new QAndATaxonomiesQuery());
+
+ viewModels = await memberBadgeService.AddSelectedBadgesToQAndA(viewModels);
+
+ var vm = new QAndASearchViewModel(request, searchResult, viewModels, taxonomies);
+
+ return View("~/Features/QAndA/Search/QAndASearch.cshtml", vm);
+ }
+}
+
+public class QAndASearchViewModel : IPagedViewModel
+{
+ public string Query { get; } = "";
+ public string SortBy { get; } = "";
+ public IReadOnlyList Questions { get; }
+ public ImmutableList DiscussionTypes { get; }
+ public ImmutableList DXTopics { get; }
+ public ImmutableList DiscussionStates { get; }
+ public DiscussionStates DiscussionState { get; }
+ public int TotalAppliedFilters { get; }
+ public int TotalPages { get; set; }
+ [HiddenInput]
+ public int Page { get; set; }
+
+ public Dictionary GetRouteData(int page) =>
+ new()
+ {
+ { "query", Query },
+ { "page", page.ToString() },
+ { "sortBy", SortBy }
+ };
+
+ public QAndASearchViewModel(QAndASearchRequest request, QAndASearchResult result, List viewModels, QAndATaxonomiesQueryResponse taxonomies)
+ {
+ Questions = viewModels;
+ Page = request.PageNumber;
+ SortBy = request.SortBy;
+ Query = request.SearchText;
+ TotalPages = result.TotalPages;
+ DXTopics = [.. taxonomies.DXTopics
+ .Select(x => new FacetOption()
+ {
+ Label = x.DisplayName,
+ Value = x.NormalizedName,
+ Count = result
+ .DXTopics
+ .FirstOrDefault(y => y.Label.Equals(x.NormalizedName, StringComparison.InvariantCultureIgnoreCase))
+ ?.Value ?? 0,
+ IsSelected = request
+ .DXTopics
+ .Contains(x.NormalizedName, StringComparer.OrdinalIgnoreCase)
+ })
+ .Where(x => x.Count != 0)
+ .OrderBy(f => f.Label)];
+ DiscussionTypes = [.. taxonomies.Types
+ .Select(x => new FacetOption()
+ {
+ Label = x.DisplayName,
+ Value = x.NormalizedName,
+ Count = result
+ .DiscussionTypes
+ .FirstOrDefault(y => y.Label.Equals(x.NormalizedName, StringComparison.InvariantCultureIgnoreCase))
+ ?.Value ?? 0,
+ IsSelected = request
+ .DiscussionTypes
+ .Contains(x.NormalizedName, StringComparer.OrdinalIgnoreCase)
+ })
+ .Where(x => x.Count != 0)
+ .OrderBy(f => f.Label)];
+ DiscussionStates = [..Enums.GetMembers()
+ .Select(m => new FacetOption()
+ {
+ Label = m.AsString(EnumFormat.Description) ?? "",
+ Value = (m.AsString(EnumFormat.Name) ?? "").ToLowerInvariant(),
+ Count = result
+ .DiscussionStates
+ .FirstOrDefault(y => y.Label.Equals(m.AsString(EnumFormat.Name), StringComparison.InvariantCultureIgnoreCase))
+ ?.Value ?? 0,
+ IsSelected = request.DiscussionStates.Contains(m.AsString(EnumFormat.Name) ?? "", StringComparer.OrdinalIgnoreCase)
+ })
+ .Where(x => x.Count != 0)
+ .OrderBy(f => f.Label)];
+ TotalAppliedFilters = DXTopics.Count(t => t.IsSelected) + DiscussionTypes.Count(t => t.IsSelected);
+ }
+}
+
+public class QAndAPostViewModel
+{
+ public int ID { get; set; }
+ public string Title { get; set; } = "";
+ public string LinkPath { get; set; } = "";
+ public DateTime DateCreated { get; set; }
+ public int ResponseCount { get; set; }
+ public DateTime LatestResponseDate { get; set; }
+ public bool HasAcceptedResponse { get; set; }
+ public QAndAAuthorViewModel Author { get; set; } = new();
+
+ public static QAndAPostViewModel GetModel(QAndASearchIndexModel result) => new()
+ {
+ Title = result.Title,
+ DateCreated = result.PublishedDate,
+ ResponseCount = result.ResponseCount,
+ LatestResponseDate = result.LatestResponseDate,
+ HasAcceptedResponse = Enums.TryParse(result.DiscussionState, true, out var state)
+ && state == DiscussionStates.HasAcceptedAnswer,
+ Author = new(result.AuthorMemberID, result.AuthorFullName, result.AuthorUsername, result.AuthorAttributes),
+ LinkPath = result.Url,
+ ID = result.ID
+ };
+}
+
diff --git a/src/Kentico.Community.Portal.Web/Features/Registration/RegistrationController.cs b/src/Kentico.Community.Portal.Web/Features/Registration/RegistrationController.cs
index dccb805c..730897ae 100644
--- a/src/Kentico.Community.Portal.Web/Features/Registration/RegistrationController.cs
+++ b/src/Kentico.Community.Portal.Web/Features/Registration/RegistrationController.cs
@@ -1,15 +1,11 @@
-using CMS.ContentEngine;
using CMS.Core;
-using CMS.DataEngine;
-using CMS.EmailEngine;
-using CMS.Websites.Routing;
+using Kentico.Community.Portal.Web.Features.Members;
using Kentico.Community.Portal.Web.Infrastructure;
using Kentico.Community.Portal.Web.Membership;
using Kentico.Community.Portal.Web.Resources;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
-using Microsoft.Extensions.Options;
namespace Kentico.Community.Portal.Web.Features.Registration;
@@ -20,23 +16,17 @@ public class RegistrationController(
UserManager userManager,
CaptchaValidator captchaValidator,
IStringLocalizer localizer,
- IOptions systemEmailOptions,
IEventLogService log,
- IEmailService emailService,
- IInfoProvider channelProvider,
- IWebsiteChannelContext channelContext,
+ IMemberEmailService emailService,
ConsentManager consentManager) : Controller
{
private readonly WebPageMetaService metaService = metaService;
private readonly SignInManager signInManager = signInManager;
private readonly UserManager userManager = userManager;
- private readonly SystemEmailOptions systemEmailOptions = systemEmailOptions.Value;
private readonly CaptchaValidator captchaValidator = captchaValidator;
private readonly IStringLocalizer localizer = localizer;
private readonly IEventLogService log = log;
- private readonly IEmailService emailService = emailService;
- private readonly IInfoProvider channelProvider = channelProvider;
- private readonly IWebsiteChannelContext channelContext = channelContext;
+ private readonly IMemberEmailService emailService = emailService;
private readonly ConsentManager consentManager = consentManager;
[HttpGet]
@@ -176,7 +166,6 @@ public async Task ResendVerificationEmail([FromQuery] string usern
private async Task SendVerificationEmail(CommunityMember member)
{
- var channel = await channelProvider.GetAsync(channelContext.WebsiteChannelName);
string confirmToken = await userManager.GenerateEmailConfirmationTokenAsync(member);
string confirmationURL = Url.Action(nameof(ConfirmEmail), "Registration",
new
@@ -186,17 +175,7 @@ private async Task SendVerificationEmail(CommunityMember member)
},
Request.Scheme) ?? "";
- await emailService.SendEmail(new EmailMessage()
- {
- From = $"no-reply@{systemEmailOptions.SendingDomain}",
- Recipients = member.Email,
- Subject = $"Confirm your email for {channel.ChannelDisplayName}",
- Body = $"""
- To confirm your email address, click here.
- You can also copy and paste this URL into your browser.
- {confirmationURL}
- """
- });
+ await emailService.SendEmail(member, MemberEmailConfiguration.RegistrationConfirmation(confirmationURL));
}
}
diff --git a/src/Kentico.Community.Portal.Web/Features/SEO/RSSFeedController.cs b/src/Kentico.Community.Portal.Web/Features/SEO/RSSFeedController.cs
index cb7500dc..a5401693 100644
--- a/src/Kentico.Community.Portal.Web/Features/SEO/RSSFeedController.cs
+++ b/src/Kentico.Community.Portal.Web/Features/SEO/RSSFeedController.cs
@@ -167,7 +167,7 @@ private async Task BlogPostRSSFeedInternal(SyndicationFeed feed
post.BlogPostContentBlogType
.TryFirst()
- .Bind(tr => blogTags.Items.TryFirst(i => i.Guid == tr.Identifier))
+ .Bind(tr => blogTags.Types.TryFirst(i => i.Guid == tr.Identifier))
.Execute(tag => item.Categories.Add(new SyndicationCategory(tag.DisplayName)));
post.ToImageViewModel()
diff --git a/src/Kentico.Community.Portal.Web/Features/Support/Components/SupportForm.cshtml b/src/Kentico.Community.Portal.Web/Features/Support/Components/SupportForm.cshtml
index f4f45a28..77af60c8 100644
--- a/src/Kentico.Community.Portal.Web/Features/Support/Components/SupportForm.cshtml
+++ b/src/Kentico.Community.Portal.Web/Features/Support/Components/SupportForm.cshtml
@@ -70,8 +70,7 @@
id="DeploymentModel">
-
-
+
diff --git a/src/Kentico.Community.Portal.Web/Infrastructure/Search/FacetOption.cs b/src/Kentico.Community.Portal.Web/Infrastructure/Search/FacetOption.cs
index 40383c87..8b5de925 100644
--- a/src/Kentico.Community.Portal.Web/Infrastructure/Search/FacetOption.cs
+++ b/src/Kentico.Community.Portal.Web/Infrastructure/Search/FacetOption.cs
@@ -3,6 +3,7 @@ namespace Kentico.Community.Portal.Web.Infrastructure.Search;
public class FacetOption
{
public string Label { get; set; } = "";
- public float Value { get; set; }
+ public string Value { get; set; } = "";
+ public float Count { get; set; }
public bool IsSelected { get; set; }
}
diff --git a/src/Kentico.Community.Portal.Web/package-lock.json b/src/Kentico.Community.Portal.Web/package-lock.json
index 823c65ab..2349019c 100644
--- a/src/Kentico.Community.Portal.Web/package-lock.json
+++ b/src/Kentico.Community.Portal.Web/package-lock.json
@@ -17,12 +17,12 @@
},
"devDependencies": {
"@fullhuman/postcss-purgecss": "6.0.0",
- "postcss": "8.4.47",
+ "postcss": "8.4.48",
"postcss-load-config": "6.0.1",
- "postcss-preset-env": "10.0.9",
+ "postcss-preset-env": "10.1.0",
"prettier": "3.3.3",
"sass": "1.80.6",
- "vite": "5.4.10"
+ "vite": "5.4.11"
},
"engines": {
"node": ">=20.16.0 <21"
@@ -853,9 +853,9 @@
}
},
"node_modules/@csstools/css-calc": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.0.4.tgz",
- "integrity": "sha512-8/iCd8lH10gKNsq5detnbGWiFd6PXK2wB8wjE6fHNNhtqvshyMrIJgffwRcw6yl/gzGTH+N1i+KRhjqHxqYTmg==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.0.tgz",
+ "integrity": "sha512-X69PmFOrjTZfN5ijxtI8hZ9kRADFSLrmmQ6hgDJ272Il049WGKpDY64KhrFm/7rbWve0z81QepawzjkKlqkNGw==",
"dev": true,
"funding": [
{
@@ -877,9 +877,9 @@
}
},
"node_modules/@csstools/css-color-parser": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.5.tgz",
- "integrity": "sha512-4Wo8raj9YF3PnZ5iGrAl+BSsk2MYBOEUS/X4k1HL9mInhyCVftEG02MywdvelXlwZGUF2XTQ0qj9Jd398mhqrw==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.6.tgz",
+ "integrity": "sha512-S/IjXqTHdpI4EtzGoNCHfqraXF37x12ZZHA1Lk7zoT5pm2lMjFuqhX/89L7dqX4CcMacKK+6ZCs5TmEGb/+wKw==",
"dev": true,
"funding": [
{
@@ -894,7 +894,7 @@
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.0.1",
- "@csstools/css-calc": "^2.0.4"
+ "@csstools/css-calc": "^2.1.0"
},
"engines": {
"node": ">=18"
@@ -1036,9 +1036,9 @@
}
},
"node_modules/@csstools/postcss-color-function": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.5.tgz",
- "integrity": "sha512-6dHr2NDsBMiZCPkGDi2qMfIbzV2kWV8Dh7SVb1FZGnN/r2TI4TSAkVF8rCG5L70yQZHMcQGB84yp8Zm+RGhoHA==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.6.tgz",
+ "integrity": "sha512-EcvXfC60cTIumzpsxWuvVjb7rsJEHPvqn3jeMEBUaE3JSc4FRuP7mEQ+1eicxWmIrs3FtzMH9gR3sgA5TH+ebQ==",
"dev": true,
"funding": [
{
@@ -1052,7 +1052,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -1066,9 +1066,9 @@
}
},
"node_modules/@csstools/postcss-color-mix-function": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.5.tgz",
- "integrity": "sha512-jgq0oGbit7TxWYP8y2hWWfV64xzcAgJk54PBYZ2fDrRgEDy1l5YMCrFawnn+5JETh/E1jjXPDFhFEYhwr3vA3g==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.6.tgz",
+ "integrity": "sha512-jVKdJn4+JkASYGhyPO+Wa5WXSx1+oUgaXb3JsjJn/BlrtFh5zjocCY7pwWi0nuP24V1fY7glQsxEYcYNy0dMFg==",
"dev": true,
"funding": [
{
@@ -1082,7 +1082,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -1125,9 +1125,9 @@
}
},
"node_modules/@csstools/postcss-exponential-functions": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.4.tgz",
- "integrity": "sha512-xmzFCGTkkLDs7q9vVaRGlnu8s51lRRJzHsaJ/nXmkQuyg0q7gh7rTbJ0bY5sSVet+KB7MTIxRXRUCl2tm7RODA==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.5.tgz",
+ "integrity": "sha512-mi8R6dVfA2nDoKM3wcEi64I8vOYEgQVtVKCfmLHXupeLpACfGAided5ddMt5f+CnEodNu4DifuVwb0I6fQDGGQ==",
"dev": true,
"funding": [
{
@@ -1141,7 +1141,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-calc": "^2.0.4",
+ "@csstools/css-calc": "^2.1.0",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
},
@@ -1180,9 +1180,9 @@
}
},
"node_modules/@csstools/postcss-gamut-mapping": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.5.tgz",
- "integrity": "sha512-VQDayRhC/Mg1fuo8/4F43La5aROgvVyqtCqdNyGvRKi6L1+zXfwQ583nImi7k/gn2GNJH82Bf9mutTuT1GtXzA==",
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.6.tgz",
+ "integrity": "sha512-0ke7fmXfc8H+kysZz246yjirAH6JFhyX9GTlyRnM0exHO80XcA9zeJpy5pOp5zo/AZiC/q5Pf+Hw7Pd6/uAoYA==",
"dev": true,
"funding": [
{
@@ -1196,7 +1196,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
},
@@ -1208,9 +1208,9 @@
}
},
"node_modules/@csstools/postcss-gradients-interpolation-method": {
- "version": "5.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.5.tgz",
- "integrity": "sha512-l3ShDdAt/szbyBh3Jz27MRFt5WPAbnVCMsU7Vs7EbBxJQNgVDrcu1APBB2nPagDJOyhI6/IahuW7nb6grWVTpA==",
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.6.tgz",
+ "integrity": "sha512-Itrbx6SLUzsZ6Mz3VuOlxhbfuyLTogG5DwEF1V8dAi24iMuvQPIHd7Ti+pNDp7j6WixndJGZaoNR0f9VSzwuTg==",
"dev": true,
"funding": [
{
@@ -1224,7 +1224,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -1238,9 +1238,9 @@
}
},
"node_modules/@csstools/postcss-hwb-function": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.5.tgz",
- "integrity": "sha512-bPn/SQyiiYjWkwK2ykc7O9LliMR50YfUGukd6jQI2okHzB7NxNt/IS45tS1Muk7Hhf3B9Lbmg1Ofq36tBmM92Q==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.6.tgz",
+ "integrity": "sha512-927Pqy3a1uBP7U8sTfaNdZVB0mNXzIrJO/GZ8us9219q9n06gOqCdfZ0E6d1P66Fm0fYHvxfDbfcUuwAn5UwhQ==",
"dev": true,
"funding": [
{
@@ -1254,7 +1254,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -1534,9 +1534,9 @@
}
},
"node_modules/@csstools/postcss-media-minmax": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.4.tgz",
- "integrity": "sha512-zgdBOCI9aKoy5GK9tb/3ve0pl7vH0HJg7rfQEWT3TZiIKh7XEWucDSTSwnwgdgtgz50UxrOfbK+C59M+u2fE2Q==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.5.tgz",
+ "integrity": "sha512-sdh5i5GToZOIAiwhdntRWv77QDtsxP2r2gXW/WbLSCoLr00KTq/yiF1qlQ5XX2+lmiFa8rATKMcbwl3oXDMNew==",
"dev": true,
"funding": [
{
@@ -1550,7 +1550,7 @@
],
"license": "MIT",
"dependencies": {
- "@csstools/css-calc": "^2.0.4",
+ "@csstools/css-calc": "^2.1.0",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/media-query-list-parser": "^4.0.2"
@@ -1644,9 +1644,9 @@
}
},
"node_modules/@csstools/postcss-oklab-function": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.5.tgz",
- "integrity": "sha512-19bsJQFyJNSEhpaVq0Mq1E0HDXfx8qMHa/bR1MaHr1UD4DWvM2/J6YXb9OVGS7eFl92Y3c84Yggn9uFv13vsiQ==",
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.6.tgz",
+ "integrity": "sha512-Hptoa0uX+XsNacFBCIQKTUBrFKDiplHan42X73EklG6XmQLG7/aIvxoNhvZ7PvOWMt67Pw3bIlUY2nD6p5vL8A==",
"dev": true,
"funding": [
{
@@ -1660,7 +1660,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -1699,10 +1699,38 @@
"postcss": "^8.4"
}
},
+ "node_modules/@csstools/postcss-random-function": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-1.0.1.tgz",
+ "integrity": "sha512-Ab/tF8/RXktQlFwVhiC70UNfpFQRhtE5fQQoP2pO+KCPGLsLdWFiOuHgSRtBOqEshCVAzR4H6o38nhvRZq8deA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.0",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"node_modules/@csstools/postcss-relative-color-syntax": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.5.tgz",
- "integrity": "sha512-5VrE4hAwv/ZpuL1Yo0ZGGFi1QPpIikp/rzz7LnpQ31ACQVRIA5/M9qZmJbRlZVsJ4bUFSQ3dq6kHSHrCt2uM6Q==",
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.6.tgz",
+ "integrity": "sha512-yxP618Xb+ji1I624jILaYM62uEmZcmbdmFoZHoaThw896sq0vU39kqTTF+ZNic9XyPtPMvq0vyvbgmHaszq8xg==",
"dev": true,
"funding": [
{
@@ -1716,7 +1744,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -1769,10 +1797,38 @@
"node": ">=4"
}
},
+ "node_modules/@csstools/postcss-sign-functions": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.0.0.tgz",
+ "integrity": "sha512-cUpr5W8eookBi5TiLSvx1HL6DFoTTgcj2pmiVNd63y2JHhvtpnJs3sfsFMmLhB42yTRS02tFPsNz3Q5zeN8ZVA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.0",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4"
+ }
+ },
"node_modules/@csstools/postcss-stepped-value-functions": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.4.tgz",
- "integrity": "sha512-JjShuWZkmIOT8EfI7lYjl7V5qM29LNDdnnSo5O7v/InJJHfeiQjtxyAaZzKGXzpkghPrCAcgLfJ+IyqTdXo7IA==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.5.tgz",
+ "integrity": "sha512-G6SJ6hZJkhxo6UZojVlLo14MohH4J5J7z8CRBrxxUYy9JuZiIqUo5TBYyDGcE0PLdzpg63a7mHSJz3VD+gMwqw==",
"dev": true,
"funding": [
{
@@ -1786,7 +1842,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-calc": "^2.0.4",
+ "@csstools/css-calc": "^2.1.0",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
},
@@ -1825,9 +1881,9 @@
}
},
"node_modules/@csstools/postcss-trigonometric-functions": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.4.tgz",
- "integrity": "sha512-nn+gWTZZlSnwbyUtGQCnvBXIx1TX+HVStvIm3221dWGQvp47bB5giMBbuAK4a/UJGBbfDQhGKEbYq++WWM1i1A==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.5.tgz",
+ "integrity": "sha512-/YQThYkt5MLvAmVu7zxjhceCYlKrYddK6LEmK5I4ojlS6BmO9u2yO4+xjXzu2+NPYmHSTtP4NFSamBCMmJ1NJA==",
"dev": true,
"funding": [
{
@@ -1841,7 +1897,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-calc": "^2.0.4",
+ "@csstools/css-calc": "^2.1.0",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
},
@@ -3996,9 +4052,9 @@
}
},
"node_modules/cssdb": {
- "version": "8.1.2",
- "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.1.2.tgz",
- "integrity": "sha512-ba3HmHU/lxy9nfz/fQLA/Ul+/oSdSOXqoR53BDmRvXTfRbkGqHKqr2rSxADYMRF4uD8vZhMlCQ6c5TEfLLkkVA==",
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.2.1.tgz",
+ "integrity": "sha512-KwEPys7lNsC8OjASI8RrmwOYYDcm0JOW9zQhcV83ejYcQkirTEyeAGui8aO2F5PiS6SLpxuTzl6qlMElIdsgIg==",
"dev": true,
"funding": [
{
@@ -5289,9 +5345,9 @@
}
},
"node_modules/picocolors": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
- "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
@@ -5310,9 +5366,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.47",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
- "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
+ "version": "8.4.48",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.48.tgz",
+ "integrity": "sha512-GCRK8F6+Dl7xYniR5a4FYbpBzU8XnZVeowqsQFYdcXuSbChgiks7qybSkbvnaeqv0G0B+dd9/jJgH8kkLDQeEA==",
"dev": true,
"funding": [
{
@@ -5331,7 +5387,7 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.1.0",
+ "picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
@@ -5394,9 +5450,9 @@
}
},
"node_modules/postcss-color-functional-notation": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.5.tgz",
- "integrity": "sha512-zW97tq5t2sSSSZQcIS4y6NDZj79zVv8hrBIJ4PSFZFmMBcjYqCt8sRXFGIYZohCpfFHmimMNqJje2Qd3qqMNdg==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.6.tgz",
+ "integrity": "sha512-wLXvm8RmLs14Z2nVpB4CWlnvaWPRcOZFltJSlcbYwSJ1EDZKsKDhPKIMecCnuU054KSmlmubkqczmm6qBPCBhA==",
"dev": true,
"funding": [
{
@@ -5410,7 +5466,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -5787,9 +5843,9 @@
}
},
"node_modules/postcss-lab-function": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.5.tgz",
- "integrity": "sha512-q2M8CfQbjHxbwv1GPAny05EVuj0WByUgq/OWKgpfbTHnMchtUqsVQgaW1mztjSZ4UPufwuTLB14fmFGsoTE/VQ==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.6.tgz",
+ "integrity": "sha512-HPwvsoK7C949vBZ+eMyvH2cQeMr3UREoHvbtra76/UhDuiViZH6pir+z71UaJQohd7VDSVUdR6TkWYKExEc9aQ==",
"dev": true,
"funding": [
{
@@ -5803,7 +5859,7 @@
],
"license": "MIT-0",
"dependencies": {
- "@csstools/css-color-parser": "^3.0.5",
+ "@csstools/css-color-parser": "^3.0.6",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
@@ -6057,9 +6113,9 @@
}
},
"node_modules/postcss-preset-env": {
- "version": "10.0.9",
- "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.0.9.tgz",
- "integrity": "sha512-mpfJWMAW6szov+ifW9HpNUUZE3BoXoHc4CDzNQHdH2I4CwsqulQ3bpFNUR6zh4tg0BUcqM7UUAbzG4UTel8QYw==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.1.0.tgz",
+ "integrity": "sha512-OfzbinZWpFcmuLB3mabsGa0NArzx5DVVtZ9G1k326iLvU7Jj9q/G3ihBu/Msi0mt96CjrM23HpbuEewDvT71KQ==",
"dev": true,
"funding": [
{
@@ -6074,14 +6130,14 @@
"license": "MIT-0",
"dependencies": {
"@csstools/postcss-cascade-layers": "^5.0.1",
- "@csstools/postcss-color-function": "^4.0.5",
- "@csstools/postcss-color-mix-function": "^3.0.5",
+ "@csstools/postcss-color-function": "^4.0.6",
+ "@csstools/postcss-color-mix-function": "^3.0.6",
"@csstools/postcss-content-alt-text": "^2.0.4",
- "@csstools/postcss-exponential-functions": "^2.0.4",
+ "@csstools/postcss-exponential-functions": "^2.0.5",
"@csstools/postcss-font-format-keywords": "^4.0.0",
- "@csstools/postcss-gamut-mapping": "^2.0.5",
- "@csstools/postcss-gradients-interpolation-method": "^5.0.5",
- "@csstools/postcss-hwb-function": "^4.0.5",
+ "@csstools/postcss-gamut-mapping": "^2.0.6",
+ "@csstools/postcss-gradients-interpolation-method": "^5.0.6",
+ "@csstools/postcss-hwb-function": "^4.0.6",
"@csstools/postcss-ic-unit": "^4.0.0",
"@csstools/postcss-initial": "^2.0.0",
"@csstools/postcss-is-pseudo-class": "^5.0.1",
@@ -6091,27 +6147,29 @@
"@csstools/postcss-logical-overscroll-behavior": "^2.0.0",
"@csstools/postcss-logical-resize": "^3.0.0",
"@csstools/postcss-logical-viewport-units": "^3.0.3",
- "@csstools/postcss-media-minmax": "^2.0.4",
+ "@csstools/postcss-media-minmax": "^2.0.5",
"@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.4",
"@csstools/postcss-nested-calc": "^4.0.0",
"@csstools/postcss-normalize-display-values": "^4.0.0",
- "@csstools/postcss-oklab-function": "^4.0.5",
+ "@csstools/postcss-oklab-function": "^4.0.6",
"@csstools/postcss-progressive-custom-properties": "^4.0.0",
- "@csstools/postcss-relative-color-syntax": "^3.0.5",
+ "@csstools/postcss-random-function": "^1.0.0",
+ "@csstools/postcss-relative-color-syntax": "^3.0.6",
"@csstools/postcss-scope-pseudo-class": "^4.0.1",
- "@csstools/postcss-stepped-value-functions": "^4.0.4",
+ "@csstools/postcss-sign-functions": "^1.0.0",
+ "@csstools/postcss-stepped-value-functions": "^4.0.5",
"@csstools/postcss-text-decoration-shorthand": "^4.0.1",
- "@csstools/postcss-trigonometric-functions": "^4.0.4",
+ "@csstools/postcss-trigonometric-functions": "^4.0.5",
"@csstools/postcss-unset-value": "^4.0.0",
"autoprefixer": "^10.4.19",
"browserslist": "^4.23.1",
"css-blank-pseudo": "^7.0.1",
"css-has-pseudo": "^7.0.1",
"css-prefers-color-scheme": "^10.0.0",
- "cssdb": "^8.1.2",
+ "cssdb": "^8.2.1",
"postcss-attribute-case-insensitive": "^7.0.1",
"postcss-clamp": "^4.1.0",
- "postcss-color-functional-notation": "^7.0.5",
+ "postcss-color-functional-notation": "^7.0.6",
"postcss-color-hex-alpha": "^10.0.0",
"postcss-color-rebeccapurple": "^10.0.0",
"postcss-custom-media": "^11.0.5",
@@ -6124,7 +6182,7 @@
"postcss-font-variant": "^5.0.0",
"postcss-gap-properties": "^6.0.0",
"postcss-image-set-function": "^7.0.0",
- "postcss-lab-function": "^7.0.5",
+ "postcss-lab-function": "^7.0.6",
"postcss-logical": "^8.0.0",
"postcss-nesting": "^13.0.1",
"postcss-opacity-percentage": "^3.0.0",
@@ -6912,9 +6970,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.10",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
- "integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
+ "version": "5.4.11",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+ "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
diff --git a/src/Kentico.Community.Portal.Web/package.json b/src/Kentico.Community.Portal.Web/package.json
index 4da5ba77..a95ecd91 100644
--- a/src/Kentico.Community.Portal.Web/package.json
+++ b/src/Kentico.Community.Portal.Web/package.json
@@ -14,12 +14,12 @@
},
"devDependencies": {
"@fullhuman/postcss-purgecss": "6.0.0",
- "postcss": "8.4.47",
+ "postcss": "8.4.48",
"postcss-load-config": "6.0.1",
- "postcss-preset-env": "10.0.9",
+ "postcss-preset-env": "10.1.0",
"prettier": "3.3.3",
"sass": "1.80.6",
- "vite": "5.4.10"
+ "vite": "5.4.11"
},
"dependencies": {
"@milkdown/crepe": "7.5.7",