diff --git a/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoHeadlineBlock.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoHeadlineBlock.cshtml similarity index 100% rename from src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoHeadlineBlock.cshtml rename to src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoHeadlineBlock.cshtml diff --git a/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoImageBlock.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoImageBlock.cshtml similarity index 100% rename from src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoImageBlock.cshtml rename to src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoImageBlock.cshtml diff --git a/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoRichTextBlock.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoRichTextBlock.cshtml similarity index 100% rename from src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoRichTextBlock.cshtml rename to src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoRichTextBlock.cshtml diff --git a/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoTwoColumnLayoutBlock.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoTwoColumnLayoutBlock.cshtml similarity index 100% rename from src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/Components/umbBlockGridDemoTwoColumnLayoutBlock.cshtml rename to src/Umbraco.Core/EmbeddedResources/BlockGrid/Components/umbBlockGridDemoTwoColumnLayoutBlock.cshtml diff --git a/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/areas.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/areas.cshtml similarity index 100% rename from src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/areas.cshtml rename to src/Umbraco.Core/EmbeddedResources/BlockGrid/areas.cshtml diff --git a/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/default.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/default.cshtml similarity index 100% rename from src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/default.cshtml rename to src/Umbraco.Core/EmbeddedResources/BlockGrid/default.cshtml diff --git a/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/items.cshtml b/src/Umbraco.Core/EmbeddedResources/BlockGrid/items.cshtml similarity index 100% rename from src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/items.cshtml rename to src/Umbraco.Core/EmbeddedResources/BlockGrid/items.cshtml diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index c9208b5bdc..7a793c74cd 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -23,6 +23,7 @@ using Umbraco.Cms.Infrastructure.Services; using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Cms.Infrastructure.Telemetry.Providers; using Umbraco.Cms.Infrastructure.Templates; +using Umbraco.Cms.Infrastructure.Templates.PartialViews; using Umbraco.Extensions; using CacheInstructionService = Umbraco.Cms.Core.Services.Implement.CacheInstructionService; @@ -58,6 +59,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddTransient(); builder.Services.AddSingleton(); + builder.Services.AddTransient(); return builder; } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index a7d0b708df..d0aefade84 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.Common; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; +using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_3_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_0; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_0_1; using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_1_0; @@ -293,5 +294,8 @@ public class UmbracoPlan : MigrationPlan // TO 10.2.0 To("{D0B3D29D-F4D5-43E3-BA67-9D49256F3266}"); To("{79D8217B-5920-4C0E-8E9A-3CF8FA021882}"); + + // To 10.3.0 + To("{56833770-3B7E-4FD5-A3B6-3416A26A7A3F}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs new file mode 100644 index 0000000000..c99f5cfe81 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs @@ -0,0 +1,31 @@ +using Umbraco.Cms.Infrastructure.Templates.PartialViews; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_3_0; + +public class AddBlockGridPartialViews : MigrationBase +{ + private readonly IPartialViewPopulator _partialViewPopulator; + private const string FolderPath = "/Views/Partials/blockgrid"; + private static readonly string[] _filesToAdd = + { + "areas.cshtml", + "default.cshtml", + "items.cshtml", + }; + + public AddBlockGridPartialViews(IMigrationContext context, IPartialViewPopulator partialViewPopulator) : base(context) + => _partialViewPopulator = partialViewPopulator; + + protected override void Migrate() + { + var embeddedBasePath = _partialViewPopulator.CoreEmbeddedPath + ".BlockGrid"; + + foreach (var fileName in _filesToAdd) + { + _partialViewPopulator.CopyPartialViewIfNotExists( + _partialViewPopulator.GetCoreAssembly(), + $"{embeddedBasePath}.{fileName}", + $"{FolderPath}/{fileName}"); + } + } +} diff --git a/src/Umbraco.Infrastructure/Templates/PartialViews/IPartialViewPopulator.cs b/src/Umbraco.Infrastructure/Templates/PartialViews/IPartialViewPopulator.cs new file mode 100644 index 0000000000..5693e53c61 --- /dev/null +++ b/src/Umbraco.Infrastructure/Templates/PartialViews/IPartialViewPopulator.cs @@ -0,0 +1,21 @@ +using System.Reflection; + +namespace Umbraco.Cms.Infrastructure.Templates.PartialViews; + +/// +/// Populates the Partial View file system using other sources, such as RCL. +/// +public interface IPartialViewPopulator +{ + /// + /// Copies a partial view from the assembly path within the provided assembly, to the file system path. But only if it does not exist yet. + /// + /// The assembly to look for embedded resources in. + /// Path to resource as assembly path I.E Umbraco.Cms.Core.EmbeddedResources. + /// The partial view filesystem path to copy the file to, I.E. /Views/Partials/blockgrid. + void CopyPartialViewIfNotExists(Assembly assembly, string embeddedPath, string fileSystemPath); + + Assembly GetCoreAssembly(); + + string CoreEmbeddedPath { get; } +} diff --git a/src/Umbraco.Infrastructure/Templates/PartialViews/PartialViewPopulator.cs b/src/Umbraco.Infrastructure/Templates/PartialViews/PartialViewPopulator.cs new file mode 100644 index 0000000000..4cc8e038c6 --- /dev/null +++ b/src/Umbraco.Infrastructure/Templates/PartialViews/PartialViewPopulator.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using System.Text; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Templates.PartialViews; + +/// +internal sealed class PartialViewPopulator : IPartialViewPopulator +{ + private readonly IFileService _fileService; + + public PartialViewPopulator(IFileService fileService) + { + _fileService = fileService; + } + + public Assembly GetCoreAssembly() => typeof(Constants).Assembly; + + public string CoreEmbeddedPath => "Umbraco.Cms.Core.EmbeddedResources"; + + /// + public void CopyPartialViewIfNotExists(Assembly assembly, string embeddedPath, string fileSystemPath) + { + Stream? content = assembly.GetManifestResourceStream(embeddedPath); + if (content is not null) + { + + // We have to ensure that this is idempotent, so only save the view if it does not already exist + // We don't want to overwrite any changes made. + IPartialView? existingView = _fileService.GetPartialView(fileSystemPath); + if (existingView is null) + { + var view = new PartialView(PartialViewType.PartialView, fileSystemPath) + { + Content = GetTextFromStream(content) + }; + + _fileService.SavePartialView(view); + } + } + } + + private string GetTextFromStream(Stream stream) + { + stream.Seek(0, SeekOrigin.Begin); + var streamReader = new StreamReader(stream, Encoding.UTF8); + return streamReader.ReadToEnd(); + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/BlockGridSampleHelper.cs b/src/Umbraco.Web.BackOffice/Controllers/BlockGridSampleHelper.cs index a2ed4bdb58..c1e976204b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BlockGridSampleHelper.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BlockGridSampleHelper.cs @@ -5,23 +5,31 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Templates.PartialViews; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Extensions; namespace Umbraco.Cms.Web.BackOffice.Controllers; -internal class BlockGridSampleHelper +// Unfortunately this has to be public to be injected into a controller +public sealed class BlockGridSampleHelper { private const string ContainerName = "Umbraco Block Grid Demo"; private readonly IContentTypeService _contentTypeService; private readonly IDataTypeService _dataTypeService; private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly IPartialViewPopulator _partialViewPopulator; - public BlockGridSampleHelper(IContentTypeService contentTypeService, IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) + public BlockGridSampleHelper( + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + IPartialViewPopulator partialViewPopulator) { _contentTypeService = contentTypeService; _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _partialViewPopulator = partialViewPopulator; _dataTypeService = dataTypeService; } @@ -35,7 +43,7 @@ internal class BlockGridSampleHelper /// The function that will perform the actual element creation /// If an error occurs, this message will describe that error /// A mapping table between element aliases and the created element UDIs, or null if an error occurs - public Dictionary? CreateSampleElements(Func> createElement, out string errorMessage) + internal Dictionary? CreateSampleElements(Func> createElement, out string errorMessage) { errorMessage = string.Empty; @@ -161,6 +169,26 @@ internal class BlockGridSampleHelper return elementUdisByAlias; } + internal void CreateSamplePartialViews() + { + var embeddedBasePath = $"{_partialViewPopulator.CoreEmbeddedPath}.BlockGrid.Components"; + var fileSystemBasePath = "/Views/partials/blockgrid/Components"; + var filesToMove = new[] + { + "umbBlockGridDemoHeadlineBlock.cshtml", + "umbBlockGridDemoImageBlock.cshtml", + "umbBlockGridDemoRichTextBlock.cshtml", + "umbBlockGridDemoTwoColumnLayoutBlock.cshtml", + }; + + foreach (var fileName in filesToMove) + { + var embeddedPath = $"{embeddedBasePath}.{fileName}"; + var fileSystemPath = $"{fileSystemBasePath}/{fileName}"; + _partialViewPopulator.CopyPartialViewIfNotExists(_partialViewPopulator.GetCoreAssembly(), embeddedPath, fileSystemPath); + } + } + private EntityContainer? GetOrCreateContainer() { var userId = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Id ?? -1; diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index b2e402eaad..e14ac00fff 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -5,6 +5,7 @@ using System.Xml.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Dictionary; @@ -21,6 +22,7 @@ using Umbraco.Cms.Infrastructure.Packaging; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; using ContentType = Umbraco.Cms.Core.Models.ContentType; @@ -43,6 +45,7 @@ public class ContentTypeController : ContentTypeControllerBase private readonly ILocalizedTextService _localizedTextService; private readonly ILogger _logger; private readonly PackageDataInstallation _packageDataInstallation; + private readonly BlockGridSampleHelper _blockGridSampleHelper; private readonly PropertyEditorCollection _propertyEditors; // TODO: Split this controller apart so that authz is consistent, currently we need to authz each action individually. @@ -52,6 +55,7 @@ public class ContentTypeController : ContentTypeControllerBase private readonly IShortStringHelper _shortStringHelper; private readonly IUmbracoMapper _umbracoMapper; + [Obsolete("Use constructor that takes BlockGridSampleHelper as a parameter")] public ContentTypeController( ICultureDictionary cultureDictionary, IContentTypeService contentTypeService, @@ -71,31 +75,77 @@ public class ContentTypeController : ContentTypeControllerBase IHostingEnvironment hostingEnvironment, EditorValidatorCollection editorValidatorCollection, PackageDataInstallation packageDataInstallation) - : base( + : this( cultureDictionary, - editorValidatorCollection, contentTypeService, mediaTypeService, memberTypeService, umbracoMapper, - localizedTextService) + localizedTextService, + serializer, + propertyEditors, + backofficeSecurityAccessor, + dataTypeService, + shortStringHelper, + fileService, + logger, + contentService, + contentTypeBaseServiceProvider, + hostingEnvironment, + editorValidatorCollection, + packageDataInstallation, + StaticServiceProvider.Instance.GetRequiredService() + ) { - _serializer = serializer; - _propertyEditors = propertyEditors; - _contentTypeService = contentTypeService; - _umbracoMapper = umbracoMapper; - _backofficeSecurityAccessor = backofficeSecurityAccessor; - _dataTypeService = dataTypeService; - _shortStringHelper = shortStringHelper; - _localizedTextService = localizedTextService; - _fileService = fileService; - _logger = logger; - _contentService = contentService; - _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; - _hostingEnvironment = hostingEnvironment; - _packageDataInstallation = packageDataInstallation; } + [ActivatorUtilitiesConstructor] + public ContentTypeController( + ICultureDictionary cultureDictionary, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IUmbracoMapper umbracoMapper, + ILocalizedTextService localizedTextService, + IEntityXmlSerializer serializer, + PropertyEditorCollection propertyEditors, + IBackOfficeSecurityAccessor backofficeSecurityAccessor, + IDataTypeService dataTypeService, + IShortStringHelper shortStringHelper, + IFileService fileService, + ILogger logger, + IContentService contentService, + IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, + IHostingEnvironment hostingEnvironment, + EditorValidatorCollection editorValidatorCollection, + PackageDataInstallation packageDataInstallation, + BlockGridSampleHelper blockGridSampleHelper) + : base( + cultureDictionary, + editorValidatorCollection, + contentTypeService, + mediaTypeService, + memberTypeService, + umbracoMapper, + localizedTextService) + { + _serializer = serializer; + _propertyEditors = propertyEditors; + _contentTypeService = contentTypeService; + _umbracoMapper = umbracoMapper; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + _dataTypeService = dataTypeService; + _shortStringHelper = shortStringHelper; + _localizedTextService = localizedTextService; + _fileService = fileService; + _logger = logger; + _contentService = contentService; + _contentTypeBaseServiceProvider = contentTypeBaseServiceProvider; + _hostingEnvironment = hostingEnvironment; + _packageDataInstallation = packageDataInstallation; + _blockGridSampleHelper = blockGridSampleHelper; + } + [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public int GetCount() => _contentTypeService.Count(); @@ -654,14 +704,16 @@ public class ContentTypeController : ContentTypeControllerBase [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] public ActionResult PostCreateBlockGridSample() { - var sampleHelper = new BlockGridSampleHelper(_contentTypeService, _dataTypeService, _backofficeSecurityAccessor); - Dictionary? elementUdisByAlias = sampleHelper.CreateSampleElements( + Dictionary? elementUdisByAlias = _blockGridSampleHelper.CreateSampleElements( documentTypeSave => PerformPostSave( documentTypeSave, i => _contentTypeService.Get(i), type => _contentTypeService.Save(type)), out string errorMessage); + // Create the partial views if they don't exist + _blockGridSampleHelper.CreateSamplePartialViews(); + return elementUdisByAlias != null ? Ok(elementUdisByAlias) : ValidationProblem(errorMessage); } } diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs index 0d8cc49bdf..994493e761 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.DependencyInjection; using Umbraco.Cms.Infrastructure.Examine.DependencyInjection; +using Umbraco.Cms.Infrastructure.Templates.PartialViews; using Umbraco.Cms.Infrastructure.WebAssets; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Filters; @@ -116,6 +117,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddSingleton(); + builder.Services.AddTransient(); return builder; } diff --git a/src/Umbraco.Web.UI/Views/Partials/blockgrid/areas.cshtml b/src/Umbraco.Web.UI/Views/Partials/blockgrid/areas.cshtml new file mode 100644 index 0000000000..94eef55ad8 --- /dev/null +++ b/src/Umbraco.Web.UI/Views/Partials/blockgrid/areas.cshtml @@ -0,0 +1,19 @@ +@using Umbraco.Extensions +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + if (Model?.Areas.Any() != true) { return; } +} + +
+ @foreach (var area in Model.Areas) + { +
+ @await Html.GetBlockGridItemsHtmlAsync(area) +
+ } +
diff --git a/src/Umbraco.Web.UI/Views/Partials/blockgrid/default.cshtml b/src/Umbraco.Web.UI/Views/Partials/blockgrid/default.cshtml new file mode 100644 index 0000000000..e25839ebb1 --- /dev/null +++ b/src/Umbraco.Web.UI/Views/Partials/blockgrid/default.cshtml @@ -0,0 +1,11 @@ +@using Umbraco.Extensions +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@{ + if (Model?.Any() != true) { return; } +} + +
+ @await Html.GetBlockGridItemsHtmlAsync(Model) +
diff --git a/src/Umbraco.Web.UI/Views/Partials/blockgrid/items.cshtml b/src/Umbraco.Web.UI/Views/Partials/blockgrid/items.cshtml new file mode 100644 index 0000000000..2831c1462e --- /dev/null +++ b/src/Umbraco.Web.UI/Views/Partials/blockgrid/items.cshtml @@ -0,0 +1,39 @@ +@using Umbraco.Cms.Core.Models.Blocks +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage> +@{ + if (Model?.Any() != true) { return; } +} + +
+ @foreach (var item in Model) + { + bool attrForceLeft = item.ForceLeft; + bool attrForceRight = item.ForceRight; +
+ @{ + var partialViewName = "blockgrid/Components/" + item.Content.ContentType.Alias; + try + { + @await Html.PartialAsync(partialViewName, item) + } + catch (InvalidOperationException) + { +

+ Could not render component of type: @(item.Content.ContentType.Alias) +
+ This likely happened because the partial view @partialViewName could not be found. +

+ } + } +
+ } +
diff --git a/templates/Umbraco.Templates.csproj b/templates/Umbraco.Templates.csproj index 2f96bdf61f..10bbd666d1 100644 --- a/templates/Umbraco.Templates.csproj +++ b/templates/Umbraco.Templates.csproj @@ -29,6 +29,10 @@ UmbracoProject\Views\Partials\grid\%(RecursiveDir)%(Filename)%(Extension) UmbracoProject\Views\Partials\grid + + UmbracoProject\Views\Partials\blockgrid\%(RecursiveDir)%(Filename)%(Extension) + UmbracoProject\Views\Partials\blockgrid + UmbracoProject\Views\_ViewImports.cshtml UmbracoProject\Views