diff --git a/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoImageBlock.html b/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoImageBlock.html index 521d7c9b09..c9da7ab143 100644 --- a/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoImageBlock.html +++ b/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoImageBlock.html @@ -19,7 +19,6 @@ justify-content: center; width: 100%; height: 100%; - min-height: 100%; cursor: pointer; color: black; background-color: transparent; @@ -35,7 +34,6 @@ color: #2152A3;// TODO: Set right colors: } img { - pointer-events: none; object-fit: cover; height: 100%; width: 100%; 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.Core/Models/Blocks/BlockGridItem.cs b/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs index 59021f5e38..097882c5ab 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Core.Models.Blocks /// /// Represents a layout item for the Block Grid editor. /// - /// + /// [DataContract(Name = "block", Namespace = "")] public class BlockGridItem : IBlockReference { @@ -116,7 +116,6 @@ namespace Umbraco.Cms.Core.Models.Blocks /// Represents a layout item with a generic content type for the Block List editor. /// /// The type of the content. - /// public class BlockGridItem : BlockGridItem where T : IPublishedElement { @@ -149,7 +148,6 @@ namespace Umbraco.Cms.Core.Models.Blocks /// /// The type of the content. /// The type of the settings. - /// public class BlockGridItem : BlockGridItem where TContent : IPublishedElement where TSettings : IPublishedElement diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs index 26a2e9fb60..ea75f369af 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceFileSources.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.FileProviders; @@ -166,7 +167,7 @@ public class LocalizedTextServiceFileSources } /// - /// returns all xml sources for all culture files found in the folder + /// Returns all xml sources for all culture files found in the folder. /// /// public IDictionary> GetXmlSources() => _xmlSources.Value; @@ -179,7 +180,15 @@ public class LocalizedTextServiceFileSources { result.AddRange( new PhysicalDirectoryContents(_fileSourceFolder.FullName) - .Where(x => !x.IsDirectory && x.Name.EndsWith(".xml"))); + .Where(x => !x.IsDirectory && !x.Name.Contains("user") && x.Name.EndsWith(".xml"))); // Filter out *.user.xml + } + + if (_supplementFileSources is not null) + { + // Get only the .xml files and filter out the user defined language files (*.user.xml) that overwrite the default + result.AddRange(_supplementFileSources + .Where(x => !x.FileInfo.Name.Contains("user") && x.FileInfo.Name.EndsWith(".xml")) + .Select(x => x.FileInfo)); } if (_directoryContents.Exists) @@ -236,8 +245,8 @@ public class LocalizedTextServiceFileSources // now load in supplementary IEnumerable found = _supplementFileSources.Where(x => { - var extension = Path.GetExtension(x.File.FullName); - var fileCultureName = Path.GetFileNameWithoutExtension(x.File.FullName).Replace("_", "-") + var extension = Path.GetExtension(x.FileInfo.Name); + var fileCultureName = Path.GetFileNameWithoutExtension(x.FileInfo.Name).Replace("_", "-") .Replace(".user", string.Empty); return extension.InvariantEquals(".xml") && ( fileCultureName.InvariantEquals(culture.Name) @@ -246,16 +255,16 @@ public class LocalizedTextServiceFileSources foreach (LocalizedTextServiceSupplementaryFileSource supplementaryFile in found) { - using (FileStream fs = supplementaryFile.File.OpenRead()) + using (Stream stream = supplementaryFile.FileInfo.CreateReadStream()) { XDocument xChildDoc; try { - xChildDoc = XDocument.Load(fs); + xChildDoc = XDocument.Load(stream); } catch (Exception ex) { - _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.File.FullName); + _logger.LogError(ex, "Could not load file into XML {File}", supplementaryFile.FileInfo.Name); continue; } diff --git a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs index cff9a55234..3ada83dc3c 100644 --- a/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs +++ b/src/Umbraco.Core/Services/LocalizedTextServiceSupplementaryFileSource.cs @@ -1,14 +1,27 @@ +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; + namespace Umbraco.Cms.Core.Services; public class LocalizedTextServiceSupplementaryFileSource { + [Obsolete("Use other ctor. Will be removed in Umbraco 12")] public LocalizedTextServiceSupplementaryFileSource(FileInfo file, bool overwriteCoreKeys) + : this(new PhysicalFileInfo(file), overwriteCoreKeys) { - File = file ?? throw new ArgumentNullException("file"); + } + + public LocalizedTextServiceSupplementaryFileSource(IFileInfo file, bool overwriteCoreKeys) + { + FileInfo = file ?? throw new ArgumentNullException(nameof(file)); + File = file is PhysicalFileInfo ? new FileInfo(file.PhysicalPath) : null!; OverwriteCoreKeys = overwriteCoreKeys; } + [Obsolete("Use FileInfo instead. Will be removed in Umbraco 12")] public FileInfo File { get; } + public IFileInfo FileInfo { get; } + public bool OverwriteCoreKeys { get; } } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index dd5b77abec..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; } @@ -102,7 +104,14 @@ public static partial class UmbracoBuilderExtensions IServiceProvider container) { IHostingEnvironment hostingEnvironment = container.GetRequiredService(); + + // TODO: (for >= v13) Rethink whether all language files (.xml and .user.xml) should be located in ~/config/lang + // instead of ~/umbraco/config/lang and ~/config/lang. + // Currently when extending Umbraco, a new language file that the backoffice will be available in, should be placed + // in ~/umbraco/config/lang, while 'user' translation files for overrides are in ~/config/lang (according to our docs). + // Such change will be breaking and we would need to document this clearly. var subPath = WebPath.Combine(Constants.SystemDirectories.Umbraco, "config", "lang"); + var mainLangFolder = new DirectoryInfo(hostingEnvironment.MapPathContentRoot(subPath)); return new LocalizedTextServiceFileSources( 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/UmbracoBuilder.LocalizedText.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs index 54e25240e0..60a946a553 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.LocalizedText.cs @@ -1,6 +1,8 @@ +using System.IO; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; @@ -42,20 +44,30 @@ namespace Umbraco.Extensions // gets all langs files in /app_plugins real or virtual locations IEnumerable pluginLangFileSources = GetPluginLanguageFileSources(webFileProvider, Cms.Core.Constants.SystemDirectories.AppPlugins, false); - // user defined langs that overwrite the default, these should not be used by plugin creators + // user defined language files that overwrite the default, these should not be used by plugin creators var userConfigLangFolder = Cms.Core.Constants.SystemDirectories.Config .TrimStart(Cms.Core.Constants.CharArrays.Tilde); - IEnumerable userLangFileSources = contentFileProvider.GetDirectoryContents(userConfigLangFolder) - .Where(x => x.IsDirectory && x.Name.InvariantEquals("lang")) - .Select(x => new DirectoryInfo(x.PhysicalPath)) - .SelectMany(x => x.GetFiles("*.user.xml", SearchOption.TopDirectoryOnly)) - .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, true)); + var configLangFileSources = new List(); + + foreach (IFileInfo langFileSource in contentFileProvider.GetDirectoryContents(userConfigLangFolder)) + { + if (langFileSource.IsDirectory && langFileSource.Name.InvariantEquals("lang")) + { + foreach (IFileInfo langFile in contentFileProvider.GetDirectoryContents($"{userConfigLangFolder}/{langFileSource.Name}")) + { + if (langFile.Name.InvariantEndsWith(".xml") && langFile.PhysicalPath is not null) + { + configLangFileSources.Add(new LocalizedTextServiceSupplementaryFileSource(langFile, true)); + } + } + } + } return localPluginFileSources .Concat(pluginLangFileSources) - .Concat(userLangFileSources); + .Concat(configLangFileSources); } @@ -83,13 +95,12 @@ namespace Umbraco.Extensions foreach (var langFolder in GetLangFolderPaths(fileProvider, pluginFolderPath)) { // request all the files out of the path, these will have physicalPath set. - IEnumerable localizationFiles = fileProvider + IEnumerable localizationFiles = fileProvider .GetDirectoryContents(langFolder) .Where(x => !string.IsNullOrEmpty(x.PhysicalPath)) - .Where(x => x.Name.InvariantEndsWith(".xml")) - .Select(x => new FileInfo(x.PhysicalPath)); + .Where(x => x.Name.InvariantEndsWith(".xml")); - foreach (FileInfo file in localizationFiles) + foreach (IFileInfo file in localizationFiles) { yield return new LocalizedTextServiceSupplementaryFileSource(file, overwriteCoreKeys); } 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.Common/Extensions/BlockGridTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/BlockGridTemplateExtensions.cs index 4cae63426b..ee0375da4f 100644 --- a/src/Umbraco.Web.Common/Extensions/BlockGridTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/BlockGridTemplateExtensions.cs @@ -15,6 +15,21 @@ public static class BlockGridTemplateExtensions public const string DefaultItemsTemplate = "items"; public const string DefaultItemAreasTemplate = "areas"; + #region Async + + /// + /// Renders a block grid model into a grid layout + /// + /// + /// By default this method uses a set of built-in partial views for rendering the blocks and areas in the grid model. + /// These partial views are embedded in the static assets (Umbraco.Cms.StaticAssets), so they won't show up in the + /// Views folder on your local disk. + /// + /// If you need to tweak the grid rendering output, you can copy the partial views from GitHub to your local disk. + /// The partial views are found in "/src/Umbraco.Cms.StaticAssets/Views/Partials/blockgrid/" on GitHub and should + /// be copied to "Views/Partials/BlockGrid/" on your local disk. + /// + /// public static async Task GetBlockGridHtmlAsync(this IHtmlHelper html, BlockGridModel? model, string template = DefaultTemplate) { if (model?.Count == 0) @@ -22,17 +37,69 @@ public static class BlockGridTemplateExtensions return new HtmlString(string.Empty); } - var view = $"{DefaultFolder}{template}"; - return await html.PartialAsync(view, model); + return await html.PartialAsync(DefaultFolderTemplate(template), model); } + /// public static async Task GetBlockGridHtmlAsync(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => await GetBlockGridHtmlAsync(html, property.GetValue() as BlockGridModel, template); + /// public static async Task GetBlockGridHtmlAsync(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) => await GetBlockGridHtmlAsync(html, contentItem, propertyAlias, DefaultTemplate); public static async Task GetBlockGridHtmlAsync(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + { + IPublishedProperty prop = GetRequiredProperty(contentItem, propertyAlias); + return await GetBlockGridHtmlAsync(html, prop.GetValue() as BlockGridModel, template); + } + + public static async Task GetBlockGridItemsHtmlAsync(this IHtmlHelper html, IEnumerable items, string template = DefaultItemsTemplate) + => await html.PartialAsync(DefaultFolderTemplate(template), items); + + public static async Task GetBlockGridItemAreasHtmlAsync(this IHtmlHelper html, BlockGridItem item, string template = DefaultItemAreasTemplate) + => await html.PartialAsync(DefaultFolderTemplate(template), item); + + #endregion + + #region Sync + + /// + public static IHtmlContent GetBlockGridHtml(this IHtmlHelper html, BlockGridModel? model, string template = DefaultTemplate) + { + if (model?.Count == 0) + { + return new HtmlString(string.Empty); + } + + return html.Partial(DefaultFolderTemplate(template), model); + } + + /// + public static IHtmlContent GetBlockGridHtml(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) + => GetBlockGridHtml(html, property.GetValue() as BlockGridModel, template); + + /// + public static IHtmlContent GetBlockGridHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) + => GetBlockGridHtml(html, contentItem, propertyAlias, DefaultTemplate); + + public static IHtmlContent GetBlockGridHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + { + IPublishedProperty prop = GetRequiredProperty(contentItem, propertyAlias); + return GetBlockGridHtml(html, prop.GetValue() as BlockGridModel, template); + } + + public static IHtmlContent GetBlockGridItemsHtml(this IHtmlHelper html, IEnumerable items, string template = DefaultItemsTemplate) + => html.Partial(DefaultFolderTemplate(template), items); + + public static IHtmlContent GetBlockGridItemAreasHtml(this IHtmlHelper html, BlockGridItem item, string template = DefaultItemAreasTemplate) + => html.Partial(DefaultFolderTemplate(template), item); + + #endregion + + private static string DefaultFolderTemplate(string template) => $"{DefaultFolder}{template}"; + + private static IPublishedProperty GetRequiredProperty(IPublishedContent contentItem, string propertyAlias) { ArgumentNullException.ThrowIfNull(propertyAlias); @@ -43,18 +110,12 @@ public static class BlockGridTemplateExtensions nameof(propertyAlias)); } - IPublishedProperty? prop = contentItem.GetProperty(propertyAlias); - if (prop == null) + IPublishedProperty? property = contentItem.GetProperty(propertyAlias); + if (property == null) { throw new InvalidOperationException("No property type found with alias " + propertyAlias); } - return await GetBlockGridHtmlAsync(html, prop.GetValue() as BlockGridModel, template); + return property; } - - public static async Task GetBlockGridItemsHtmlAsync(this IHtmlHelper html, IEnumerable items, string template = DefaultItemsTemplate) - => await html.PartialAsync($"{DefaultFolder}{template}", items); - - public static async Task GetBlockGridItemAreasHtmlAsync(this IHtmlHelper html, BlockGridItem item, string template = DefaultItemAreasTemplate) - => await html.PartialAsync($"{DefaultFolder}{template}", item); } diff --git a/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs index 17b620ab51..edf3055159 100644 --- a/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/BlockListTemplateExtensions.cs @@ -10,6 +10,33 @@ public static class BlockListTemplateExtensions public const string DefaultFolder = "blocklist/"; public const string DefaultTemplate = "default"; + #region Async + + public static async Task GetBlockListHtmlAsync(this IHtmlHelper html, BlockListModel? model, string template = DefaultTemplate) + { + if (model?.Count == 0) + { + return new HtmlString(string.Empty); + } + + return await html.PartialAsync(DefaultFolderTemplate(template), model); + } + + public static async Task GetBlockListHtmlAsync(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) + => await GetBlockListHtmlAsync(html, property.GetValue() as BlockListModel, template); + + public static async Task GetBlockListHtmlAsync(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias) + => await GetBlockListHtmlAsync(html, contentItem, propertyAlias, DefaultTemplate); + + public static async Task GetBlockListHtmlAsync(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + { + IPublishedProperty property = GetRequiredProperty(contentItem, propertyAlias); + return await GetBlockListHtmlAsync(html, property.GetValue() as BlockListModel, template); + } + #endregion + + #region Sync + public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, BlockListModel? model, string template = DefaultTemplate) { if (model?.Count == 0) @@ -17,8 +44,7 @@ public static class BlockListTemplateExtensions return new HtmlString(string.Empty); } - var view = DefaultFolder + template; - return html.Partial(view, model); + return html.Partial(DefaultFolderTemplate(template), model); } public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) @@ -29,10 +55,17 @@ public static class BlockListTemplateExtensions public static IHtmlContent GetBlockListHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) { - if (propertyAlias == null) - { - throw new ArgumentNullException(nameof(propertyAlias)); - } + IPublishedProperty property = GetRequiredProperty(contentItem, propertyAlias); + return GetBlockListHtml(html, property.GetValue() as BlockListModel, template); + } + + #endregion + + private static string DefaultFolderTemplate(string template) => $"{DefaultFolder}{template}"; + + private static IPublishedProperty GetRequiredProperty(IPublishedContent contentItem, string propertyAlias) + { + ArgumentNullException.ThrowIfNull(propertyAlias); if (string.IsNullOrWhiteSpace(propertyAlias)) { @@ -41,12 +74,12 @@ public static class BlockListTemplateExtensions nameof(propertyAlias)); } - IPublishedProperty? prop = contentItem.GetProperty(propertyAlias); - if (prop == null) + IPublishedProperty? property = contentItem.GetProperty(propertyAlias); + if (property == null) { throw new InvalidOperationException("No property type found with alias " + propertyAlias); } - return GetBlockListHtml(html, prop.GetValue() as BlockListModel, template); + return property; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index ae9f0121ca..872aad3f53 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -232,6 +232,7 @@ appendRuntimeData(); init(); + startWatches($scope.content); syncTreeNode($scope.content, $scope.content.path, true); @@ -565,7 +566,6 @@ $scope.page.loading = true; loadContent().then(function () { - startWatches($scope.content); $scope.page.loading = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index 71bf151b89..7ed5e4120f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -64,6 +64,8 @@ setActiveVariant(); } else if (changes.segment && !changes.segment.isFirstChange() && changes.segment.currentValue !== changes.segment.previousValue) { setActiveVariant(); + } else if (changes.content) { + setActiveVariant(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less index 45cba81a0b..fee768235a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less +++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less @@ -130,13 +130,18 @@ umb-block-card { right: 10px; opacity: 0; transition: opacity 120ms; + margin-right: 10px; + .__action { - display: inline-block; + display: inline-flex; + justify-content: center; + align-items: center; border-radius: 50%; width: 28px; height: 28px; background-color: white; color:@ui-action-type; + &:hover { color: @ui-action-type-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-property-info-button/umb-property-info-button.less b/src/Umbraco.Web.UI.Client/src/views/components/umb-property-info-button/umb-property-info-button.less index 33e1156648..a892f1ca38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-property-info-button/umb-property-info-button.less +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-property-info-button/umb-property-info-button.less @@ -3,16 +3,20 @@ umb-property-info-button { display: inline-block; vertical-align: text-bottom; + .control-label + & { + margin-left: 3px; + } + > .__button { position: relative; display: inline-flex; align-items: center; justify-content: center; + text-align: center; width: 15px; height: 15px; - padding-top: 1px; - padding-left: 1px; + padding: 2px 1px 1px 1px; margin-top: -1px; font-size: 12px; font-weight: 700; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less index f23632389c..6cfdb05482 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/blockgridui.less @@ -234,26 +234,29 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-grid__block:not(.--acti &:not(.--hovering-area):focus, &:not(.--hovering-area):focus-within, &.--active { + + /** Avoid displaying hover when dragging-mode */ + --umb-block-grid--block_ui-opacity: calc(1 - var(--umb-block-grid--dragging-mode, 0)); > .umb-block-grid__block--context { - opacity: 1; + opacity: var(--umb-block-grid--block_ui-opacity); } &:not(.--scale-mode) { > .umb-block-grid__block--actions { - opacity: 1; + opacity: var(--umb-block-grid--block_ui-opacity); } > umb-block-grid-block > umb-block-grid-entries > .umb-block-grid__layout-container > .umb-block-grid__area-actions { - opacity: 1; + opacity: var(--umb-block-grid--block_ui-opacity); } } > .umb-block-grid__scale-handler { - opacity: 1; + opacity: var(--umb-block-grid--block_ui-opacity); } > .umb-block-grid__force-left, > .umb-block-grid__force-right { - opacity: 1; + opacity: var(--umb-block-grid--block_ui-opacity); } } @@ -279,7 +282,8 @@ ng-form.ng-invalid > .umb-block-grid__block:not(.--active) > .umb-block-grid__bl .umb-block-grid__block--view { height: 100%; width: 100%; - display: block; + display: flex; + flex-direction: column; } .umb-block-grid__block--context { @@ -583,6 +587,9 @@ umb-block-grid-block { top: 0px; position: absolute; z-index: 1; + + /** Avoid showing inline-create in dragging-mode */ + opacity: calc(1 - var(--umb-block-grid--dragging-mode, 0)); } .umb-block-grid__block--inline-create-button.--above { left: 0; @@ -633,8 +640,6 @@ umb-block-grid-block { border-radius: @baseBorderRadius; box-sizing: border-box; - clear: both;// needed for layouts using float. - &:hover { border-color: transparent; > button { @@ -651,7 +656,6 @@ umb-block-grid-block { > button { position: relative; display: flex; - //width: 100%; align-items: center; justify-content: center; @@ -687,7 +691,7 @@ umb-block-grid-block { &.umb-block-grid__clipboard-button { margin-left: 0; padding: 5px 12px; - font-size: 18px;// Align with block action buttons. + font-size: 18px; border-top-left-radius: 0; border-bottom-left-radius: 0; @@ -739,7 +743,6 @@ umb-block-grid-block { > button { position: relative; display: flex; - //width: 100%; align-items: center; justify-content: center; @@ -779,7 +782,8 @@ umb-block-grid-block { background: transparent; border-radius: 3px; - border: @blueDark solid 1px; + border: solid 1px; + border-color: rgba(@blueDark, .5); border-radius: 3px; height: 100%; @@ -865,9 +869,25 @@ umb-block-grid-block { transition: opacity 240ms; } .umb-block-grid__area.--highlight::after { + /** Avoid displaying highlight when in dragging-mode */ + opacity: calc(1 - var(--umb-block-grid--dragging-mode, 0)); + border-color: @blueDark; + box-shadow: 0 0 0 1px rgba(255, 255, 255, .7), inset 0 0 0 1px rgba(255, 255, 255, .7); +} +.umb-block-grid__area:has( .umb-block-grid__layout-item-placeholder )::after { opacity: 1; border-color: @blueDark; box-shadow: 0 0 0 1px rgba(255, 255, 255, .7), inset 0 0 0 1px rgba(255, 255, 255, .7); + + /* Moved back to edge for this case */ + top: 0; + bottom: 0; + + animation: umb-block-grid__area-after__border-pulse 400ms ease-in-out alternate infinite; + @keyframes umb-block-grid__area-after__border-pulse { + 0% { border-color: rgba(@blueDark, 1); } + 100% { border-color: rgba(@blueDark, 0.66); } + } } .umb-block-grid__scalebox-backdrop { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html index 29b3f4b53f..4b05e4ad43 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.html @@ -260,12 +260,12 @@
- + Define how many layout columns that will be available for areas. If not defined, the number of layout columns defined for the entire layout will be used.
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-allowance-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-allowance-editor.less index 11907dfcb0..9389906fac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-allowance-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-allowance-editor.less @@ -20,7 +20,7 @@ margin: 0 3px; } input { - width: 40px; + width: 60px; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.less index d4a1487b40..bf66c34661 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/umb-block-grid-area-editor.less @@ -163,9 +163,10 @@ Grid part: .umb-block-grid-area-editor__scale-label { position: absolute; display: block; - left: 100%; - margin-left: 6px; + right: 0; + top: 100%; margin-top: 6px; + transform: translateX(50%); z-index: 2; background-color: white; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js index c105b98fcb..c1e2c43619 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentries.component.js @@ -619,6 +619,7 @@ } window.removeEventListener('drag', _onDragMove); window.removeEventListener('dragover', _onDragMove); + document.documentElement.style.setProperty("--umb-block-grid--dragging-mode", 0); if(ghostElIndicateForceLeft) { ghostEl.removeChild(ghostElIndicateForceLeft); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js index 22739d748c..a7c45ecdd8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbblockgridentry.component.js @@ -311,7 +311,8 @@ vm.scaleHandlerKeyUp = function($event) { - let addCol = 0; + + let addColIndex = 0; let addRow = 0; switch ($event.originalEvent.key) { @@ -322,16 +323,23 @@ addRow = 1; break; case 'ArrowLeft': - addCol = -1; + addColIndex = -1; break; case 'ArrowRight': - addCol = 1; + addColIndex = 1; break; } - const newColumnSpan = Math.max(vm.layoutEntry.columnSpan + addCol, 1); - - vm.layoutEntry.columnSpan = closestColumnSpanOption(newColumnSpan, vm.layoutEntry.$block.config.columnSpanOptions, gridColumns.length).columnSpan; + if(addColIndex !== 0) { + if (vm.layoutEntry.$block.config.columnSpanOptions.length > 0) { + const sortOptions = vm.layoutEntry.$block.config.columnSpanOptions.sort((a,b) => (a.columnSpan > b.columnSpan) ? 1 : ((b.columnSpan > a.columnSpan) ? -1 : 0)); + const currentColIndex = sortOptions.findIndex(x => x.columnSpan === vm.layoutEntry.columnSpan); + const newColIndex = Math.min(Math.max(currentColIndex + addColIndex, 0), sortOptions.length-1); + vm.layoutEntry.columnSpan = sortOptions[newColIndex].columnSpan; + } else { + vm.layoutEntry.columnSpan = vm.layoutColumnsInt; + } + } let newRowSpan = Math.max(vm.layoutEntry.rowSpan + addRow, vm.layoutEntry.$block.config.rowMinSpan || 1); if(vm.layoutEntry.$block.config.rowMaxSpan != null) { newRowSpan = Math.min(newRowSpan, vm.layoutEntry.$block.config.rowMaxSpan); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css index 94974a9111..962126b969 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/umbraco-blockgridlayout-flexbox.css @@ -33,4 +33,9 @@ .umb-block-grid__area { --umb-block-grid__area-calc: calc(var(--umb-block-grid--area-column-span) / var(--umb-block-grid--area-grid-columns, 1)); width: calc(var(--umb-block-grid__area-calc) * 100%); +} + + +.umb-block-grid__actions { + clear: both; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js index f0b89a095f..91b1f88444 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js @@ -72,7 +72,7 @@ }); event.stopPropagation(); - } + }; vm.removeBlockByIndex = function (index) { $scope.model.value.splice(index, 1); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html index 3172219434..cdefd12993 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html @@ -8,8 +8,10 @@ ng-repeat="block in model.value" ng-class="{'--isOpen':vm.openBlock === block}" ng-click="vm.openBlockOverlay(block)"> -
- diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index cc324f70d5..58ca11b716 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -26,9 +26,9 @@
- +
- +
@@ -36,7 +36,7 @@
- + Overwrite how this block appears in the BackOffice UI. Pick a .html file containing your presentation. @@ -50,7 +50,7 @@
-
@@ -60,7 +60,7 @@
- +
@@ -71,7 +71,7 @@
-
@@ -81,9 +81,9 @@
- +
- @@ -107,12 +107,12 @@
- +
-
@@ -124,7 +124,7 @@
- +
@@ -137,7 +137,7 @@
-
@@ -157,9 +157,10 @@
- +
@@ -171,9 +172,10 @@
- +
@@ -185,7 +187,7 @@
- +
@@ -196,7 +198,7 @@
-
@@ -218,9 +220,13 @@
- +
- + +
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 diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index d9d26331d6..480c6e60fb 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^1.0.0", - "@umbraco/playwright-testhelpers": "^1.0.3", + "@umbraco/playwright-testhelpers": "^1.0.10", "camelize": "^1.0.0", "dotenv": "^16.0.2", "faker": "^4.1.0", @@ -101,9 +101,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.3.tgz", - "integrity": "sha512-PmUnIaoKitxAC4JWSiPEOPg74Ypt6DNLjUQEATV0n9yVbw5aFQhql+KrdN4F30gFNr1c6Gw6I5iDXzNmq5/zfg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.10.tgz", + "integrity": "sha512-4NTuMbbNWGcawZIuYnDdPUGN4W2F9iw0EvsyJ2Pr5rYj8Rg1PCu2MXW77r27fGhfr31PYDEL6RSL9zp8SyxfJg==", "dependencies": { "@umbraco/json-models-builders": "^1.0.0", "camelize": "^1.0.0", @@ -906,9 +906,9 @@ } }, "@umbraco/playwright-testhelpers": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.3.tgz", - "integrity": "sha512-PmUnIaoKitxAC4JWSiPEOPg74Ypt6DNLjUQEATV0n9yVbw5aFQhql+KrdN4F30gFNr1c6Gw6I5iDXzNmq5/zfg==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-1.0.10.tgz", + "integrity": "sha512-4NTuMbbNWGcawZIuYnDdPUGN4W2F9iw0EvsyJ2Pr5rYj8Rg1PCu2MXW77r27fGhfr31PYDEL6RSL9zp8SyxfJg==", "requires": { "@umbraco/json-models-builders": "^1.0.0", "camelize": "^1.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 7464e119e0..55da15670c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -19,7 +19,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^1.0.0", - "@umbraco/playwright-testhelpers": "^1.0.3", + "@umbraco/playwright-testhelpers": "^1.0.10", "camelize": "^1.0.0", "faker": "^4.1.0", "form-data": "^4.0.0", diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts index ea95f45dc3..c3a78183cf 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/content.spec.ts @@ -290,11 +290,14 @@ test.describe('Content tests', () => { await umbracoUi.setEditorHeaderName(newNodeName); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); await umbracoUi.isSuccessNotificationVisible(); + await page.locator('span:has-text("×")').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.rollback)); // Not a very nice selector, but there's sadly no alternative :( await page.locator('.-selectable.cursor-pointer').first().click(); - // Sadly can't use the button by label key here since there's another one in the DOM + // Sadly can't use the button by label key here since there's another one in the DOM + const helpText = await page.locator('[key="rollback_diffHelp"]'); + await expect(helpText).toBeVisible(); await page.locator('[action="vm.rollback()"]').click(); await umbracoUi.refreshContentTree(); @@ -663,10 +666,10 @@ test.describe('Content tests', () => { .build(); const alias = AliasHelper.toAlias(name); - + // Save grid and get the ID const dataType = await umbracoApi.dataTypes.save(grid) - + // Create a document type using the data type const docType = new DocumentTypeBuilder() .withName(name) @@ -690,7 +693,7 @@ test.describe('Content tests', () => { .build(); await umbracoApi.content.save(contentNode); - + // Ugly wait but we have to wait for cache to rebuild await page.waitForTimeout(1000); @@ -719,7 +722,7 @@ test.describe('Content tests', () => { // Save and publish await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); await umbracoUi.isSuccessNotificationVisible(); - + const expected = `
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts index 1da0c016f0..ba6637c693 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/DataTypes/dataTypes.spec.ts @@ -48,6 +48,7 @@ test.describe('DataTypes', () => { // Save await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); await umbracoUi.isSuccessNotificationVisible(); + await page.locator('span:has-text("×")').click(); // Assert const expected = `

Lorem ipsum dolor sit amet

`; @@ -158,6 +159,7 @@ test.describe('DataTypes', () => { await umbracoUi.setEditorHeaderName('UrlPickerContent'); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); await umbracoUi.isSuccessNotificationVisible(); + await page.locator('span:has-text("×")').click(); await page.locator('.umb-node-preview-add').click(); // Should really try and find a better way to do this, but umbracoTreeItem tries to click the content pane in the background diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts new file mode 100644 index 0000000000..998a738e2f --- /dev/null +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/ModelsBuilder/modelsbuilder.spec.ts @@ -0,0 +1,283 @@ +import {AliasHelper, ApiHelpers, ConstantHelper, test, UiHelpers} from '@umbraco/playwright-testhelpers'; +import { + ContentBuilder, + DocumentTypeBuilder, +} from "@umbraco/json-models-builders"; + +test.describe('Modelsbuilder tests', () => { + + test.beforeEach(async ({page, umbracoApi}) => { + await umbracoApi.login(); + }); + + test('Can create and render content', async ({page, umbracoApi, umbracoUi}) => { + const docTypeName = "TestDocument"; + const docTypeAlias = AliasHelper.toAlias(docTypeName); + const contentName = "Home"; + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName); + + const docType = new DocumentTypeBuilder() + .withName(docTypeName) + .withAlias(docTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(docTypeAlias) + .addTab() + .withName("Content") + .addTextBoxProperty() + .withAlias("title") + .done() + .done() + .build(); + + await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; +@{ +\tLayout = null; +} + +

@Model.Title

`); + + // Time to manually create the content + await umbracoUi.createContentWithDocumentType(docTypeName); + await umbracoUi.setEditorHeaderName(contentName); + // Fortunately for us the input field of a text box has the alias of the property as an id :) + await page.locator("#title").type("Hello world!") + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)); + await umbracoUi.isSuccessNotificationVisible(); + // Ensure that we can render it on the frontend = we can compile the models and views + await umbracoApi.content.verifyRenderedContent("/", "

Hello world!

", true); + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName); + }); + + test('Can update document type without updating view', async ({page, umbracoApi, umbracoUi}) => { + const docTypeName = "TestDocument"; + const docTypeAlias = AliasHelper.toAlias(docTypeName); + const propertyAlias = "title"; + const propertyValue = "Hello world!" + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName); + + const docType = new DocumentTypeBuilder() + .withName(docTypeName) + .withAlias(docTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(docTypeAlias) + .addTab() + .withName("Content") + .addTextBoxProperty() + .withAlias(propertyAlias) + .done() + .done() + .build(); + + const savedDocType = await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; +@{ +\tLayout = null; +} + +

@Model.Title

`); + + const content = new ContentBuilder() + .withContentTypeAlias(savedDocType["alias"]) + .withAction("publishNew") + .addVariant() + .withName("Home") + .withSave(true) + .withPublish(true) + .addProperty() + .withAlias(propertyAlias) + .withValue(propertyValue) + .done() + .done() + .build() + + await umbracoApi.content.save(content); + + // Navigate to the document type + await umbracoUi.goToSection(ConstantHelper.sections.settings); + await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["Document Types", docTypeName])); + // Add a new property (this might cause a version error if the viewcache is not cleared, hence this test + await page.locator('.umb-box-content >> [data-element="property-add"]').click(); + await page.locator('[data-element="property-name"]').type("Second Title"); + await page.locator('[data-element="editor-add"]').click(); + await page.locator('[input-id="datatype-search"]').type("Textstring"); + await page.locator('.umb-card-grid >> [title="Textstring"]').click(); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit)); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); + await umbracoUi.isSuccessNotificationVisible(); + + // Now that the content is updated and the models are rebuilt, ensure that we can still render the frontend. + await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

", true) + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName); + }); + + test('Can update view without updating document type', async ({page, umbracoApi, umbracoUi}) => { + const docTypeName = "TestDocument"; + const docTypeAlias = AliasHelper.toAlias(docTypeName); + const propertyAlias = "title"; + const propertyValue = "Hello world!" + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName); + + const docType = new DocumentTypeBuilder() + .withName(docTypeName) + .withAlias(docTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(docTypeAlias) + .addTab() + .withName("Content") + .addTextBoxProperty() + .withAlias(propertyAlias) + .done() + .done() + .build(); + + const savedDocType = await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; +@{ +\tLayout = null; +} + +

@Model.Title

`); + + const content = new ContentBuilder() + .withContentTypeAlias(savedDocType["alias"]) + .withAction("publishNew") + .addVariant() + .withName("Home") + .withSave(true) + .withPublish(true) + .addProperty() + .withAlias(propertyAlias) + .withValue(propertyValue) + .done() + .done() + .build() + + await umbracoApi.content.save(content); + + // Navigate to the document type + await umbracoUi.goToSection(ConstantHelper.sections.settings); + await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["templates", docTypeName])); + const editor = await page.locator('.ace_content'); + await editor.click(); + // We only have to type out the opening tag, the editor adds the closing tag automatically. + await editor.type("

Edited") + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)) + + await umbracoUi.isSuccessNotificationVisible(); + await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

Edited

", true) + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName); + }); + + test('Can update view and document type', async ({page, umbracoApi, umbracoUi}) => { + const docTypeName = "TestDocument"; + const docTypeAlias = AliasHelper.toAlias(docTypeName); + const propertyAlias = "title"; + const propertyValue = "Hello world!" + const contentName = "Home"; + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName); + + const docType = new DocumentTypeBuilder() + .withName(docTypeName) + .withAlias(docTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(docTypeAlias) + .addTab() + .withName("Content") + .addTextBoxProperty() + .withAlias(propertyAlias) + .done() + .done() + .build(); + + const savedDocType = await umbracoApi.documentTypes.save(docType); + await umbracoApi.templates.edit(docTypeName, `@using Umbraco.Cms.Web.Common.PublishedModels; +@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage +@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels; +@{ +\tLayout = null; +} + +

@Model.Title

`); + + const content = new ContentBuilder() + .withContentTypeAlias(savedDocType["alias"]) + .withAction("publishNew") + .addVariant() + .withName(contentName) + .withSave(true) + .withPublish(true) + .addProperty() + .withAlias(propertyAlias) + .withValue(propertyValue) + .done() + .done() + .build() + + await umbracoApi.content.save(content); + + // Navigate to the document type + await umbracoUi.goToSection(ConstantHelper.sections.settings); + await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["Document Types", docTypeName])); + // Add a new property (this might cause a version error if the viewcache is not cleared, hence this test + await page.locator('.umb-box-content >> [data-element="property-add"]').click(); + await page.locator('[data-element="property-name"]').type("Bod"); + await page.locator('[data-element="editor-add"]').click(); + await page.locator('[input-id="datatype-search"]').type("Textstring"); + await page.locator('.umb-card-grid >> [title="Textstring"]').click(); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.submit)); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); + await umbracoUi.isSuccessNotificationVisible(); + await page.locator('span:has-text("×")').click(); + + // Update the template + await umbracoUi.clickElement(umbracoUi.getTreeItem("settings", ["templates", docTypeName])); + const editor = await page.locator('.ace_content'); + await editor.click(); + // We only have to type out the opening tag, the editor adds the closing tag automatically. + await editor.type("

@Model.Bod") + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)) + await umbracoUi.isSuccessNotificationVisible(); + await page.locator('span:has-text("×")').click(); + + // Navigate to the content section and update the content + await umbracoUi.goToSection(ConstantHelper.sections.content); + await umbracoUi.refreshContentTree(); + await umbracoUi.clickElement(umbracoUi.getTreeItem("content", [contentName])); + await page.locator("#bod").type("Fancy body text"); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.saveAndPublish)) + + await umbracoApi.content.verifyRenderedContent("/", "

" + propertyValue + "

Fancy body text

", true); + + await umbracoApi.content.deleteAllContent(); + await umbracoApi.documentTypes.ensureNameNotExists(docTypeName); + await umbracoApi.templates.ensureNameNotExists(docTypeName) + }); +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts index ba91e6ba82..86eff05eec 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Packages/packages.spec.ts @@ -88,7 +88,8 @@ test.describe('Packages', () => { // Navigate pack to packages and Assert the file is created // Waits until the button download is visible - await page.locator('[label-key="general_download"]').isVisible(); + await expect(await page.locator('[label-key="general_download"]')).toBeVisible({timeout: 60000}); + // Checks if the packages was created const doesExist = await umbracoApi.packages.doesNameExist(packageName); await expect(doesExist).toBe(true); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts index c54907d315..895bff5366 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/macro.spec.ts @@ -31,6 +31,7 @@ test.describe('Macros', () => { // Adds partial view to macro await page.locator('[data-element="property-label-macroPartialViewPickerProperty"]').click(); + await page.locator('[data-element="tree-item-' + partialViewName + '.cshtml"]').click(); await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/relationTypes.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/relationTypes.spec.ts index d1a375ef6e..e49cecde54 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/relationTypes.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Settings/relationTypes.spec.ts @@ -30,11 +30,9 @@ test.describe('Relation Types', () => { await page.selectOption('select[name="relationType-child"]', {label: "Media"}); await form.locator('[name="relationType-isdependency"]').last().click({force: true}); await form.locator('.btn-primary').click(); + await umbracoUi.clickElement(umbracoUi.getButtonByLabelKey(ConstantHelper.buttons.save)) + await umbracoUi.isSuccessNotificationVisible(); - await page.waitForNavigation(); - - expect(page.url()).toContain("#/settings/relationTypes/edit/"); - //Clean up await umbracoApi.relationTypes.ensureNameNotExists(name); }); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 76f030d6bc..e616f11dad 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -20,7 +20,7 @@ - +