diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ea588b..fe7de48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,10 +29,10 @@ jobs: DOTNET_NOLOGO: 1 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: global-json-file: global.json diff --git a/README.md b/README.md index 2cfb1ca..1647dcc 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,7 @@ dotnet add package Kentico.Xperience.TagManager - Top of the body - inserts a script immediately after the opening body tag. - Bottom of the body - inserts a script right before the closing body tag. - Fill in a consent if required. + - Select whether you want to display tags in the Xperience administration preview or Page Builder, or both. This option defaults to None. 5. During rendering the livesite page, the Tag manager module automatically adds custom code snippets with accepted consents to defined locations. 6. To dynamically update the rendered code snippets, for example if a consent is accepted, call javascript function `window.xperience.tagManager.updateCodeSnippets()`. diff --git a/src/Kentico.Xperience.TagManager/Admin/Client/package-lock.json b/src/Kentico.Xperience.TagManager/Admin/Client/package-lock.json index 2c428f5..7508247 100644 --- a/src/Kentico.Xperience.TagManager/Admin/Client/package-lock.json +++ b/src/Kentico.Xperience.TagManager/Admin/Client/package-lock.json @@ -11,8 +11,8 @@ "@kentico/xperience-admin-base": "28.3.1", "@kentico/xperience-admin-components": "28.3.1", "html-react-parser": "^5.1.10", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "react-select": "^5.8.0" }, "devDependencies": { @@ -10228,9 +10228,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dependencies": { "loose-envify": "^1.1.0" }, @@ -10318,15 +10318,15 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-froala-wysiwyg": { @@ -10886,9 +10886,9 @@ "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dependencies": { "loose-envify": "^1.1.0" } diff --git a/src/Kentico.Xperience.TagManager/Admin/Client/package.json b/src/Kentico.Xperience.TagManager/Admin/Client/package.json index af4c0c1..9af38e1 100644 --- a/src/Kentico.Xperience.TagManager/Admin/Client/package.json +++ b/src/Kentico.Xperience.TagManager/Admin/Client/package.json @@ -16,8 +16,8 @@ "@kentico/xperience-admin-base": "28.3.1", "@kentico/xperience-admin-components": "28.3.1", "html-react-parser": "^5.1.10", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "react-select": "^5.8.0" }, "devDependencies": { diff --git a/src/Kentico.Xperience.TagManager/Admin/CodeSnippetConfigurationModel.cs b/src/Kentico.Xperience.TagManager/Admin/CodeSnippetConfigurationModel.cs index 22223eb..da69810 100644 --- a/src/Kentico.Xperience.TagManager/Admin/CodeSnippetConfigurationModel.cs +++ b/src/Kentico.Xperience.TagManager/Admin/CodeSnippetConfigurationModel.cs @@ -27,7 +27,7 @@ internal class CodeSnippetConfigurationModel [VisibleIfEqualTo(nameof(TagType), CustomSnippetFactory.TAG_TYPE_NAME)] public string? Code { get; set; } - [RadioGroupComponent(Label = "Tag location", Order = 5, Options = CodeSnippetLocationsExtensions.FormComponentOptions)] + [RadioGroupComponent(Label = "Tag location", Order = 5, Options = CodeSnippetExtensions.LocationFormComponentOptions)] [VisibleIfEqualTo(nameof(TagType), CustomSnippetFactory.TAG_TYPE_NAME)] public string? Location { get; set; } @@ -38,6 +38,9 @@ internal class CodeSnippetConfigurationModel [ObjectIdSelectorComponent(objectType: ConsentInfo.OBJECT_TYPE, Label = "Consent", Order = 6, Placeholder = "No consent needed")] public IEnumerable ConsentIDs { get; set; } = []; + [DropDownComponent(Label = "Kentico administration Display Mode", Options = CodeSnippetExtensions.DisplayModeFormComponentOptions, Order = 7)] + public string DisplayMode { get; set; } = "None"; + public void MapToChannelCodeSnippetInfo(ChannelCodeSnippetItemInfo info) { info.ChannelCodeSnippetItemChannelId = ChannelIDs.FirstOrDefault(); @@ -47,13 +50,6 @@ public void MapToChannelCodeSnippetInfo(ChannelCodeSnippetItemInfo info) info.ChannelCodeSnippetItemName = Name; info.ChannelCodeSnippetItemIdentifier = TagIdentifier; info.ChannelCodeSnippetItemCode = Code; + info.ChannelCodeSnippetAdministrationDisplayMode = DisplayMode; } } - -internal static class CodeSnippetLocationsExtensions -{ - public const string FormComponentOptions = $"{nameof(CodeSnippetLocations.HeadTop)};Insert at the top of the head\r\n" + - $"{nameof(CodeSnippetLocations.HeadBottom)};Insert at the bottom of the head\r\n" + - $"{nameof(CodeSnippetLocations.BodyTop)};Insert at the top of the body\r\n" + - $"{nameof(CodeSnippetLocations.BodyBottom)};Insert at the bottom of the body\r\n"; -} diff --git a/src/Kentico.Xperience.TagManager/Admin/CodeSnippetExtensions.cs b/src/Kentico.Xperience.TagManager/Admin/CodeSnippetExtensions.cs new file mode 100644 index 0000000..592cd90 --- /dev/null +++ b/src/Kentico.Xperience.TagManager/Admin/CodeSnippetExtensions.cs @@ -0,0 +1,16 @@ +using Kentico.Xperience.TagManager.Rendering; + +namespace Kentico.Xperience.TagManager.Admin; + +internal static class CodeSnippetExtensions +{ + public const string LocationFormComponentOptions = $"{nameof(CodeSnippetLocations.HeadTop)};Insert at the top of the head\r\n" + + $"{nameof(CodeSnippetLocations.HeadBottom)};Insert at the bottom of the head\r\n" + + $"{nameof(CodeSnippetLocations.BodyTop)};Insert at the top of the body\r\n" + + $"{nameof(CodeSnippetLocations.BodyBottom)};Insert at the bottom of the body\r\n"; + + public const string DisplayModeFormComponentOptions = $"{nameof(CodeSnippetAdministrationDisplayMode.None)};Do not display in Administration\r\n" + + $"{nameof(CodeSnippetAdministrationDisplayMode.PreviewOnly)};Display in the Preview view mode only\r\n" + + $"{nameof(CodeSnippetAdministrationDisplayMode.PageBuilderOnly)};Display in the Page Builder only\r\n" + + $"{nameof(CodeSnippetAdministrationDisplayMode.Both)}Display in the Preview view mode and the Page Builder"; +} diff --git a/src/Kentico.Xperience.TagManager/Admin/InfoModels/ChannelCodeSnippetItem/ChannelCodeSnippetItemInfo.generated.cs b/src/Kentico.Xperience.TagManager/Admin/InfoModels/ChannelCodeSnippetItem/ChannelCodeSnippetItemInfo.generated.cs index c6c548d..4aba8fc 100644 --- a/src/Kentico.Xperience.TagManager/Admin/InfoModels/ChannelCodeSnippetItem/ChannelCodeSnippetItemInfo.generated.cs +++ b/src/Kentico.Xperience.TagManager/Admin/InfoModels/ChannelCodeSnippetItem/ChannelCodeSnippetItemInfo.generated.cs @@ -6,6 +6,7 @@ using CMS.Helpers; using CMS.ContentEngine; using CMS.DataProtection; + using Kentico.Xperience.TagManager.Admin; [assembly: RegisterObjectType(typeof(ChannelCodeSnippetItemInfo), ChannelCodeSnippetItemInfo.OBJECT_TYPE)] @@ -120,7 +121,7 @@ public virtual string ChannelCodeSnippetItemLocation /// - /// Channel code snippet GTMID. + /// Channel code snippet third party identifier. /// [DatabaseField] public virtual string ChannelCodeSnippetItemIdentifier @@ -140,6 +141,16 @@ public virtual string ChannelCodeSnippetItemCode set => SetValue(nameof(ChannelCodeSnippetItemCode), value, String.Empty); } + /// + /// Channel code snippet administration display mode. + /// + [DatabaseField] + public virtual string ChannelCodeSnippetAdministrationDisplayMode + { + get => ValidationHelper.GetString(GetValue(nameof(ChannelCodeSnippetAdministrationDisplayMode)), String.Empty); + set => SetValue(nameof(ChannelCodeSnippetAdministrationDisplayMode), value, String.Empty); + } + /// /// Channel code snippet consent ID. diff --git a/src/Kentico.Xperience.TagManager/Admin/TagManagerModuleInstaller.cs b/src/Kentico.Xperience.TagManager/Admin/TagManagerModuleInstaller.cs index 69ac8f5..d8ad0a4 100644 --- a/src/Kentico.Xperience.TagManager/Admin/TagManagerModuleInstaller.cs +++ b/src/Kentico.Xperience.TagManager/Admin/TagManagerModuleInstaller.cs @@ -137,6 +137,18 @@ private static void InstallChannelCodeSnippetClass(ResourceInfo resourceInfo) }; formInfo.AddFormItem(formItem); + formItem = new FormFieldInfo + { + Name = nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetAdministrationDisplayMode), + Visible = false, + Precision = 0, + Size = 200, + DataType = FieldDataType.Text, + Enabled = true, + AllowEmpty = true + }; + formInfo.AddFormItem(formItem); + formItem = new FormFieldInfo { Name = nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetItemType), diff --git a/src/Kentico.Xperience.TagManager/Admin/UIPages/CodeSnippetEditPage.cs b/src/Kentico.Xperience.TagManager/Admin/UIPages/CodeSnippetEditPage.cs index deaa7d2..407583e 100644 --- a/src/Kentico.Xperience.TagManager/Admin/UIPages/CodeSnippetEditPage.cs +++ b/src/Kentico.Xperience.TagManager/Admin/UIPages/CodeSnippetEditPage.cs @@ -41,6 +41,7 @@ protected override CodeSnippetConfigurationModel Model ChannelIDs = [info.ChannelCodeSnippetItemChannelId], Name = info.ChannelCodeSnippetItemName, Code = info.ChannelCodeSnippetItemCode, + DisplayMode = info.ChannelCodeSnippetAdministrationDisplayMode, TagType = info.ChannelCodeSnippetItemType, ConsentIDs = info.ChannelCodeSnippetItemConsentId == 0 ? [] : [info.ChannelCodeSnippetItemConsentId], TagIdentifier = info.ChannelCodeSnippetItemIdentifier, diff --git a/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetAdministrationDisplayMode.cs b/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetAdministrationDisplayMode.cs new file mode 100644 index 0000000..3e734c6 --- /dev/null +++ b/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetAdministrationDisplayMode.cs @@ -0,0 +1,9 @@ +namespace Kentico.Xperience.TagManager.Rendering; + +public enum CodeSnippetAdministrationDisplayMode +{ + None = default, + PreviewOnly, + PageBuilderOnly, + Both +} diff --git a/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetDto.cs b/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetDto.cs index d3fd2d5..2f49d65 100644 --- a/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetDto.cs +++ b/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetDto.cs @@ -5,4 +5,5 @@ public class CodeSnippetDto public int ID { get; init; } public string? Code { get; init; } public CodeSnippetLocations Location { get; init; } + public CodeSnippetAdministrationDisplayMode DisplayMode { get; set; } } diff --git a/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetTagHelperComponent.cs b/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetTagHelperComponent.cs index 6eea5db..0b3c8ca 100644 --- a/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetTagHelperComponent.cs +++ b/src/Kentico.Xperience.TagManager/Rendering/CodeSnippetTagHelperComponent.cs @@ -1,6 +1,11 @@ using CMS.ContactManagement; +using Kentico.Content.Web.Mvc; +using Kentico.PageBuilder.Web.Mvc; +using Kentico.Web.Mvc; + using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -18,14 +23,17 @@ internal class CodeSnippetTagHelperComponent : TagHelperComponent private readonly IChannelCodeSnippetsService codeSnippetsContext; private readonly IUrlHelperFactory urlHelperFactory; private readonly IFileVersionProvider fileVersionProvider; + private readonly IHttpContextAccessor httpContextAccessor; public CodeSnippetTagHelperComponent( IChannelCodeSnippetsService codeSnippetsContext, IUrlHelperFactory urlHelperFactory, - IFileVersionProvider fileVersionProvider) + IFileVersionProvider fileVersionProvider, + IHttpContextAccessor httpContextAccessor) { this.codeSnippetsContext = codeSnippetsContext; this.urlHelperFactory = urlHelperFactory; + this.httpContextAccessor = httpContextAccessor; this.fileVersionProvider = fileVersionProvider; } @@ -40,27 +48,53 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu { var contact = ContactManagementContext.CurrentContact; + var codeSnippets = await codeSnippetsContext.GetConsentedCodeSnippets(contact); + if (string.Equals(context.TagName, HeadTag, StringComparison.OrdinalIgnoreCase)) { - ProcessHead(output, await codeSnippetsContext.GetConsentedCodeSnippets(contact)); + ProcessHead(output, codeSnippets, httpContextAccessor.HttpContext); } if (string.Equals(context.TagName, BodyTag, StringComparison.OrdinalIgnoreCase)) { - ProcessBody(output, await codeSnippetsContext.GetConsentedCodeSnippets(contact)); + ProcessBody(output, codeSnippets, httpContextAccessor.HttpContext); } } private static void ProcessHead( TagHelperOutput output, - ILookup codeSnippets) + ILookup codeSnippets, + HttpContext? httpContext) { - foreach (var codeSnippet in codeSnippets[CodeSnippetLocations.HeadTop]) + bool isEditMode = httpContext.Kentico().PageBuilder().EditMode; + bool isPreviewMode = httpContext.Kentico().Preview().Enabled; + + var headTopSnippets = codeSnippets[CodeSnippetLocations.HeadTop]; + var headBottomSnippets = codeSnippets[CodeSnippetLocations.HeadBottom]; + + if (isEditMode) + { + headTopSnippets = headTopSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PageBuilderOnly); + + headBottomSnippets = headBottomSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PageBuilderOnly); + } + else if (isPreviewMode) + { + headTopSnippets = headTopSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PreviewOnly); + + headBottomSnippets = headBottomSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PreviewOnly); + } + + foreach (var codeSnippet in headTopSnippets) { output.PreContent.AppendHtml(codeSnippet.Code); } - foreach (var codeSnippet in codeSnippets[CodeSnippetLocations.HeadBottom]) + foreach (var codeSnippet in headBottomSnippets) { output.PostContent.AppendHtml(codeSnippet.Code); } @@ -68,14 +102,38 @@ private static void ProcessHead( private void ProcessBody( TagHelperOutput output, - ILookup codeSnippets) + ILookup codeSnippets, + HttpContext? httpContext) { - foreach (var codeSnippet in codeSnippets[CodeSnippetLocations.BodyTop]) + bool isEditMode = httpContext.Kentico().PageBuilder().EditMode; + bool isPreviewMode = httpContext.Kentico().Preview().Enabled; + + var bodyTopSnippets = codeSnippets[CodeSnippetLocations.BodyTop]; + var bodyBottomSnippets = codeSnippets[CodeSnippetLocations.BodyBottom]; + + if (isEditMode) + { + bodyTopSnippets = bodyTopSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PageBuilderOnly); + + bodyBottomSnippets = bodyBottomSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PageBuilderOnly); + } + else if (isPreviewMode) + { + bodyTopSnippets = bodyTopSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PreviewOnly); + + bodyBottomSnippets = bodyBottomSnippets.Where(x => x.DisplayMode is CodeSnippetAdministrationDisplayMode.Both or + CodeSnippetAdministrationDisplayMode.PreviewOnly); + } + + foreach (var codeSnippet in bodyTopSnippets) { output.PreContent.AppendHtml(codeSnippet.Code); } - foreach (var codeSnippet in codeSnippets[CodeSnippetLocations.BodyBottom]) + foreach (var codeSnippet in bodyBottomSnippets) { output.PostContent.AppendHtml(codeSnippet.Code); } diff --git a/src/Kentico.Xperience.TagManager/Rendering/DefaultChannelCodeSnippetsService.cs b/src/Kentico.Xperience.TagManager/Rendering/DefaultChannelCodeSnippetsService.cs index 3ff7e31..304151a 100644 --- a/src/Kentico.Xperience.TagManager/Rendering/DefaultChannelCodeSnippetsService.cs +++ b/src/Kentico.Xperience.TagManager/Rendering/DefaultChannelCodeSnippetsService.cs @@ -17,6 +17,7 @@ internal class DefaultChannelCodeSnippetsService : IChannelCodeSnippetsService { private readonly IConsentAgreementService consentAgreementService; private readonly IWebsiteChannelContext channelContext; + private readonly IChannelCodeSnippetItemInfoProvider codeSnippetInfoProvider; private readonly IProgressiveCache cache; @@ -70,6 +71,7 @@ async Task> GetCodeSnippetsInterna .Columns(nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetItemLocation), nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetItemCode), nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetItemConsentId), + nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetAdministrationDisplayMode), nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetItemIdentifier), nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetItemID), nameof(ChannelCodeSnippetItemInfo.ChannelCodeSnippetItemType), @@ -99,13 +101,19 @@ private static IEnumerable CreateCodeSnippet(ChannelCodeSnippetI var tags = new List(); + if (!Enum.TryParse(snippetInfo.ChannelCodeSnippetAdministrationDisplayMode, out CodeSnippetAdministrationDisplayMode displayMode)) + { + displayMode = CodeSnippetAdministrationDisplayMode.None; + } + if (snippetSettings.TagTypeName != CustomSnippetFactory.TAG_TYPE_NAME) { tags.AddRange(snippetFactory.CreateCodeSnippets(snippetInfo.ChannelCodeSnippetItemIdentifier).Select(x => new CodeSnippetDto { Location = x.Location, Code = x.Code, - ID = snippetInfo.ChannelCodeSnippetItemID + ID = snippetInfo.ChannelCodeSnippetItemID, + DisplayMode = displayMode })); } else @@ -117,6 +125,7 @@ private static IEnumerable CreateCodeSnippet(ChannelCodeSnippetI Location = Enum.TryParse(snippetInfo.ChannelCodeSnippetItemLocation, out CodeSnippetLocations location) ? location : throw new InvalidOperationException("Invalid Channel Tag Location."), + DisplayMode = displayMode }); tags.Add(tag); @@ -130,7 +139,8 @@ private static CodeSnippetDto AdjustCustomCodeSnippet(CodeSnippetDto codeSnippet { Code = codeSnippet.Code != null ? AddSnippetIds(codeSnippet.ID, codeSnippet.Code!) : codeSnippet.Code, ID = codeSnippet.ID, - Location = codeSnippet.Location + Location = codeSnippet.Location, + DisplayMode = codeSnippet.DisplayMode }; private static string AddSnippetIds(int codeSnippetId, string codeSnippet) =>