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

- } - -
-
-
-
- - - -
-
-
-
- @foreach (var blogType in Model.BlogTypes) - { - string colorClass = blogType.IsSelected - ? "secondary" - : "gray"; - string selectedAttr = blogType.IsSelected - ? "facet-selected" - : ""; - - } -
-
- - -
- - -
- - @if (Model.BlogPosts.Count == 0 && Model.TotalPages == 0) - { -
-

No results could be found.

-
- } - - @foreach (var post in Model.BlogPosts) - { -
-
-
-

- @post.Title -

-
- -
-

- @post.ShortDescription -

-
- @if (post.Taxonomy is string taxonomy) - { -
-
- @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 + +
+
+ +
+
+ +
+
Type
+ @foreach (var blogType in Model.BlogTypes) + { +
+ + +
+ } +
+ +
+
Topic
+ @foreach (var topic in Model.DXTopics) + { +
+ + +
+ } +
+
+ +
+
+
+ + +
+
+ +
+ + + +
+
+
+ +
+ +
+
Type
+ @foreach (var blogType in Model.BlogTypes) + { +
+ + +
+ } +
+ +
+
Topic
+ @foreach (var topic in Model.DXTopics) + { +
+ + +
+ } +
+
+ + @if (Model.BlogPosts.Count == 0 && Model.TotalPages == 0) + { +
+

No results could be found.

+
+ } + + @foreach (var post in Model.BlogPosts) + { +
+
+
+

+ @post.Title +

+
+
+ @post.BlogType +
+
+
+ +
+

@post.ShortDescription

+
+ +
+
+ @foreach(string topic in post.DXTopics) + { + @topic + } +
+
+ +
+
+ } + + + +
+
+
+
\ 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 - -
-
- -
-
-
-
-
- - -
-
- - @foreach (var discussionType in Model.DiscussionTypes) - { - string colorClass = discussionType.IsSelected - ? "secondary" - : "gray"; - string selectedAttr = discussionType.IsSelected - ? "facet-selected" - : ""; - - } -
-
-
- - -
- -
- @* - Using the long form modeling binding instead of asp-for so that we don't generate the hidden input - to handle submitting a value when the checkbox is unchecked. This would be fine for a POST, but - it clutters up the URL with a GET - - See: https://www.learnrazorpages.com/razor-pages/forms/checkboxes#razor-checkboxes - *@ - - -
- -
-
- - -
- - @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 -
- } -
-

- @question.Title -

-
- -
- - 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 + +
+
+ + +
+
+
+
Type
+ @foreach (var discussionType in Model.DiscussionTypes) + { +
+ + +
+ } +
+ +
+
Answers
+ @foreach (var state in Model.DiscussionStates) + { +
+ + +
+ } +
+ + @if (Model.DXTopics.Count > 0) + { +
+
Topic
+ @foreach (var topic in Model.DXTopics) + { +
+ + +
+ } +
+ } +
+ +
+
+
+ + +
+ +
+ +
+ + + +
+
+ +
+ +
+
Type
+ @foreach (var discussionType in Model.DiscussionTypes) + { +
+ + +
+ } +
+ +
+
Answers
+ @foreach (var state in Model.DiscussionStates) + { +
+ + +
+ } +
+ + @if (Model.DXTopics.Count > 0) + { +
+
Topic
+ @foreach (var topic in Model.DXTopics) + { +
+ + +
+ } +
+ } +
+
+ + @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 +
+ } +
+

+ @question.Title +

+
+ +
+ + 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/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",