diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/ServerEventExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/ServerEventExtensions.cs index be9eab77e8..4764207158 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/ServerEventExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/ServerEventExtensions.cs @@ -51,6 +51,7 @@ internal static class ServerEventExtensions builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); + builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); builder.AddNotificationAsyncHandler(); @@ -82,6 +83,7 @@ internal static class ServerEventExtensions { builder.EventSourceAuthorizers() .Append() + .Append() .Append() .Append() .Append() diff --git a/src/Umbraco.Cms.Api.Management/ServerEvents/Authorizers/DocumentBlueprintEventAuthorizer.cs b/src/Umbraco.Cms.Api.Management/ServerEvents/Authorizers/DocumentBlueprintEventAuthorizer.cs new file mode 100644 index 0000000000..1df0b83eb5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ServerEvents/Authorizers/DocumentBlueprintEventAuthorizer.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Api.Management.ServerEvents.Authorizers; + +public class DocumentBlueprintEventAuthorizer : EventSourcePolicyAuthorizer +{ + public DocumentBlueprintEventAuthorizer(IAuthorizationService authorizationService) + : base(authorizationService) + { + } + + public override IEnumerable AuthorizableEventSources => [Constants.ServerEvents.EventSource.DocumentBlueprint]; + + protected override string Policy => AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes; +} diff --git a/src/Umbraco.Cms.Api.Management/ServerEvents/ServerEventSender.cs b/src/Umbraco.Cms.Api.Management/ServerEvents/ServerEventSender.cs index 59835f44e6..4e301dfefb 100644 --- a/src/Umbraco.Cms.Api.Management/ServerEvents/ServerEventSender.cs +++ b/src/Umbraco.Cms.Api.Management/ServerEvents/ServerEventSender.cs @@ -32,6 +32,7 @@ internal sealed class ServerEventSender : INotificationAsyncHandler, INotificationAsyncHandler, INotificationAsyncHandler, + INotificationAsyncHandler, INotificationAsyncHandler, INotificationAsyncHandler, INotificationAsyncHandler, @@ -86,15 +87,18 @@ internal sealed class ServerEventSender : { foreach (T entity in notification.DeletedEntities) { - await _serverEventRouter.RouteEventAsync(new ServerEvent - { - EventType = Constants.ServerEvents.EventType.Deleted, - EventSource = source, - Key = entity.Key, - }); + await RouteDeletedEvent(source, entity); } } + private async Task RouteDeletedEvent(string source, T entity) where T : IEntity => + await _serverEventRouter.RouteEventAsync(new ServerEvent + { + EventType = Constants.ServerEvents.EventType.Deleted, + EventSource = source, + Key = entity.Key, + }); + private async Task NotifyTrashedAsync(MovedToRecycleBinNotification notification, string source) where T : IEntity { @@ -194,6 +198,14 @@ internal sealed class ServerEventSender : public async Task HandleAsync(ContentDeletedNotification notification, CancellationToken cancellationToken) => await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.Document); + public async Task HandleAsync(ContentDeletedBlueprintNotification notification, CancellationToken cancellationToken) + { + foreach (Core.Models.IContent entity in notification.DeletedBlueprints) + { + await RouteDeletedEvent(Constants.ServerEvents.EventSource.DocumentBlueprint, entity); + } + } + public async Task HandleAsync(ContentTypeDeletedNotification notification, CancellationToken cancellationToken) => await NotifyDeletedAsync(notification, Constants.ServerEvents.EventSource.DocumentType); diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index 4b934f65a2..e7c44eff13 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -13,6 +13,8 @@ public static partial class Constants public const int LabelDateTime = -94; public const int LabelTime = -98; public const int LabelDecimal = -99; + public const int LabelBytes = -104; + public const int LabelPixels = -105; public const int Textarea = -89; public const int Textbox = -88; @@ -219,6 +221,16 @@ public static partial class Constants /// public const string LabelDecimal = "8f1ef1e1-9de4-40d3-a072-6673f631ca64"; + /// + /// Guid for Label as bytes + /// + public const string LabelBytes = "ba5bdbe6-ab3e-46a8-82b3-2c45f10bc47f"; + + /// + /// Guid for Label as pixels + /// + public const string LabelPixels = "5eb57825-e15e-4fc7-8e37-fca65cdafbde"; + /// /// Guid for Content Picker /// diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index f1ddbc49bc..4174196cad 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -205,7 +205,7 @@ internal sealed class DatabaseDataCreator { [Constants.Security.AdminGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T", ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], [Constants.Security.EditorGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T", ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], - [Constants.Security.WriterGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":" , ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], + [Constants.Security.WriterGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":", ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], [Constants.Security.TranslatorGroupKey] = [ActionUpdate.ActionLetter, ActionBrowse.ActionLetter, ActionDocumentPropertyRead.ActionLetter, ActionDocumentPropertyWrite.ActionLetter], }; @@ -320,6 +320,10 @@ internal sealed class DatabaseDataCreator InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelBytes, 40, Constants.DataTypes.Guids.LabelBytes, + "Label (bytes)"); + InsertDataTypeNodeDto(Constants.DataTypes.LabelPixels, 41, Constants.DataTypes.Guids.LabelPixels, + "Label (pixels)"); ConditionalInsert( Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes, @@ -1454,7 +1458,7 @@ internal sealed class DatabaseDataCreator { Id = 7, UniqueId = new Guid("A68D453B-1F62-44F4-9F71-0B6BBD43C355"), - DataTypeId = Constants.DataTypes.LabelInt, + DataTypeId = Constants.DataTypes.LabelPixels, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, @@ -1462,7 +1466,7 @@ internal sealed class DatabaseDataCreator SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in pixels", + Description = null, Variations = (byte)ContentVariation.Nothing, }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, @@ -1470,7 +1474,7 @@ internal sealed class DatabaseDataCreator { Id = 8, UniqueId = new Guid("854087F6-648B-40ED-BC98-B8A9789E80B9"), - DataTypeId = Constants.DataTypes.LabelInt, + DataTypeId = Constants.DataTypes.LabelPixels, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, @@ -1478,7 +1482,7 @@ internal sealed class DatabaseDataCreator SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in pixels", + Description = null, Variations = (byte)ContentVariation.Nothing, }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, @@ -1486,15 +1490,15 @@ internal sealed class DatabaseDataCreator { Id = 9, UniqueId = new Guid("BD4C5ACE-26E3-4A8B-AF1A-E8206A35FA07"), - DataTypeId = Constants.DataTypes.LabelBigint, + DataTypeId = Constants.DataTypes.LabelBytes, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, - Name = "Size", + Name = "File size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in bytes", + Description = null, Variations = (byte)ContentVariation.Nothing, }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, @@ -1502,11 +1506,11 @@ internal sealed class DatabaseDataCreator { Id = 10, UniqueId = new Guid("F7786FE8-724A-4ED0-B244-72546DB32A92"), - DataTypeId = -92, + DataTypeId = Constants.DataTypes.LabelString, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, - Name = "Type", + Name = "File extension", SortOrder = 0, Mandatory = false, ValidationRegExp = null, @@ -1538,11 +1542,11 @@ internal sealed class DatabaseDataCreator { Id = 25, UniqueId = new Guid("3531C0A3-4E0A-4324-A621-B9D3822B071F"), - DataTypeId = -92, + DataTypeId = Constants.DataTypes.LabelString, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, - Name = "Type", + Name = "File extension", SortOrder = 0, Mandatory = false, ValidationRegExp = null, @@ -1554,15 +1558,15 @@ internal sealed class DatabaseDataCreator { Id = 26, UniqueId = new Guid("F9527050-59BC-43E4-8FA8-1658D1319FF5"), - DataTypeId = Constants.DataTypes.LabelBigint, + DataTypeId = Constants.DataTypes.LabelBytes, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, - Name = "Size", + Name = "File size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in bytes", + Description = null, Variations = (byte)ContentVariation.Nothing, }); } @@ -1590,11 +1594,11 @@ internal sealed class DatabaseDataCreator { Id = 41, UniqueId = new Guid("EDD2B3FD-1E57-4E57-935E-096DEFCCDC9B"), - DataTypeId = -92, + DataTypeId = Constants.DataTypes.LabelString, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Extension, - Name = "Type", + Name = "File extension", SortOrder = 0, Mandatory = false, ValidationRegExp = null, @@ -1606,15 +1610,15 @@ internal sealed class DatabaseDataCreator { Id = 42, UniqueId = new Guid("180EEECF-1F00-409E-8234-BBA967E08B0A"), - DataTypeId = Constants.DataTypes.LabelBigint, + DataTypeId = Constants.DataTypes.LabelBytes, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Bytes, - Name = "Size", + Name = "File size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in bytes", + Description = null, Variations = (byte)ContentVariation.Nothing, }); } @@ -1642,11 +1646,11 @@ internal sealed class DatabaseDataCreator { Id = 44, UniqueId = new Guid("1BEE433F-A21A-4031-8E03-AF01BB8D2DE9"), - DataTypeId = -92, + DataTypeId = Constants.DataTypes.LabelString, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Extension, - Name = "Type", + Name = "File extension", SortOrder = 0, Mandatory = false, ValidationRegExp = null, @@ -1658,15 +1662,15 @@ internal sealed class DatabaseDataCreator { Id = 45, UniqueId = new Guid("3CBF538A-29AB-4317-A9EB-BBCDF1A54260"), - DataTypeId = Constants.DataTypes.LabelBigint, + DataTypeId = Constants.DataTypes.LabelBytes, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Bytes, - Name = "Size", + Name = "File size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in bytes", + Description = null, Variations = (byte)ContentVariation.Nothing, }); } @@ -1694,11 +1698,11 @@ internal sealed class DatabaseDataCreator { Id = 47, UniqueId = new Guid("EF1B4AF7-36DE-45EB-8C18-A2DE07319227"), - DataTypeId = -92, + DataTypeId = Constants.DataTypes.LabelString, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Extension, - Name = "Type", + Name = "File extension", SortOrder = 0, Mandatory = false, ValidationRegExp = null, @@ -1710,15 +1714,15 @@ internal sealed class DatabaseDataCreator { Id = 48, UniqueId = new Guid("AAB7D00C-7209-4337-BE3F-A4421C8D79A0"), - DataTypeId = Constants.DataTypes.LabelBigint, + DataTypeId = Constants.DataTypes.LabelBytes, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Bytes, - Name = "Size", + Name = "File size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in bytes", + Description = null, Variations = (byte)ContentVariation.Nothing, }); } @@ -1746,11 +1750,11 @@ internal sealed class DatabaseDataCreator { Id = 50, UniqueId = new Guid("0F25A89E-2EB7-49BC-A7B4-759A7E4C69F2"), - DataTypeId = -92, + DataTypeId = Constants.DataTypes.LabelString, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Extension, - Name = "Type", + Name = "File extension", SortOrder = 0, Mandatory = false, ValidationRegExp = null, @@ -1762,15 +1766,15 @@ internal sealed class DatabaseDataCreator { Id = 51, UniqueId = new Guid("09A07AFF-861D-4769-A2B0-C165EBD43D39"), - DataTypeId = Constants.DataTypes.LabelBigint, + DataTypeId = Constants.DataTypes.LabelBytes, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Bytes, - Name = "Size", + Name = "File size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, - Description = "in bytes", + Description = null, Variations = (byte)ContentVariation.Nothing, }); } @@ -1943,7 +1947,7 @@ internal sealed class DatabaseDataCreator EditorAlias = Constants.PropertyEditors.Aliases.RichText, EditorUiAlias = "Umb.PropertyEditorUi.Tiptap", DbType = "Ntext", - Configuration = "{\"extensions\": [\"Umb.Tiptap.Embed\", \"Umb.Tiptap.Link\", \"Umb.Tiptap.Figure\", \"Umb.Tiptap.Image\", \"Umb.Tiptap.Subscript\", \"Umb.Tiptap.Superscript\", \"Umb.Tiptap.Table\", \"Umb.Tiptap.Underline\", \"Umb.Tiptap.TextAlign\", \"Umb.Tiptap.MediaUpload\"], \"maxImageSize\": 500, \"overlaySize\": \"medium\", \"toolbar\": [[[\"Umb.Tiptap.Toolbar.SourceEditor\"], [\"Umb.Tiptap.Toolbar.Bold\", \"Umb.Tiptap.Toolbar.Italic\", \"Umb.Tiptap.Toolbar.Underline\"], [\"Umb.Tiptap.Toolbar.TextAlignLeft\", \"Umb.Tiptap.Toolbar.TextAlignCenter\", \"Umb.Tiptap.Toolbar.TextAlignRight\"], [\"Umb.Tiptap.Toolbar.BulletList\", \"Umb.Tiptap.Toolbar.OrderedList\"], [\"Umb.Tiptap.Toolbar.Blockquote\", \"Umb.Tiptap.Toolbar.HorizontalRule\"], [\"Umb.Tiptap.Toolbar.Link\", \"Umb.Tiptap.Toolbar.Unlink\"], [\"Umb.Tiptap.Toolbar.MediaPicker\", \"Umb.Tiptap.Toolbar.EmbeddedMedia\"]]]}", + Configuration = "{\"extensions\": [\"Umb.Tiptap.RichTextEssentials\", \"Umb.Tiptap.Anchor\", \"Umb.Tiptap.Block\", \"Umb.Tiptap.Blockquote\", \"Umb.Tiptap.Bold\", \"Umb.Tiptap.BulletList\", \"Umb.Tiptap.CodeBlock\", \"Umb.Tiptap.Embed\", \"Umb.Tiptap.Figure\", \"Umb.Tiptap.Heading\", \"Umb.Tiptap.HorizontalRule\", \"Umb.Tiptap.HtmlAttributeClass\", \"Umb.Tiptap.HtmlAttributeDataset\", \"Umb.Tiptap.HtmlAttributeId\", \"Umb.Tiptap.HtmlAttributeStyle\", \"Umb.Tiptap.HtmlTagDiv\", \"Umb.Tiptap.HtmlTagSpan\", \"Umb.Tiptap.Image\", \"Umb.Tiptap.Italic\", \"Umb.Tiptap.Link\", \"Umb.Tiptap.MediaUpload\", \"Umb.Tiptap.OrderedList\", \"Umb.Tiptap.Strike\", \"Umb.Tiptap.Subscript\", \"Umb.Tiptap.Superscript\", \"Umb.Tiptap.Table\", \"Umb.Tiptap.TextAlign\", \"Umb.Tiptap.TextDirection\", \"Umb.Tiptap.TextIndent\", \"Umb.Tiptap.TrailingNode\", \"Umb.Tiptap.Underline\"], \"maxImageSize\": 500, \"overlaySize\": \"medium\", \"toolbar\": [[[\"Umb.Tiptap.Toolbar.SourceEditor\"], [\"Umb.Tiptap.Toolbar.Bold\", \"Umb.Tiptap.Toolbar.Italic\", \"Umb.Tiptap.Toolbar.Underline\"], [\"Umb.Tiptap.Toolbar.TextAlignLeft\", \"Umb.Tiptap.Toolbar.TextAlignCenter\", \"Umb.Tiptap.Toolbar.TextAlignRight\"], [\"Umb.Tiptap.Toolbar.BulletList\", \"Umb.Tiptap.Toolbar.OrderedList\"], [\"Umb.Tiptap.Toolbar.Blockquote\", \"Umb.Tiptap.Toolbar.HorizontalRule\"], [\"Umb.Tiptap.Toolbar.Link\", \"Umb.Tiptap.Toolbar.Unlink\"], [\"Umb.Tiptap.Toolbar.MediaPicker\", \"Umb.Tiptap.Toolbar.EmbeddedMedia\"]]]}", }); } @@ -1995,6 +1999,10 @@ internal sealed class DatabaseDataCreator "Umb.PropertyEditorUi.Label", "Decimal", "{\"umbracoDataValueType\":\"DECIMAL\"}"); InsertDataTypeDto(Constants.DataTypes.LabelTime, Constants.PropertyEditors.Aliases.Label, "Umb.PropertyEditorUi.Label", "Date", "{\"umbracoDataValueType\":\"TIME\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelBytes, Constants.PropertyEditors.Aliases.Label, + "Umb.PropertyEditorUi.Label", "Nvarchar", "{\"umbracoDataValueType\":\"BIGINT\", \"labelTemplate\":\"{=value | bytes}\"}"); + InsertDataTypeDto(Constants.DataTypes.LabelPixels, Constants.PropertyEditors.Aliases.Label, + "Umb.PropertyEditorUi.Label", "Integer", "{\"umbracoDataValueType\":\"INT\", \"labelTemplate\":\"{=value}px\"}"); if (_database.Exists(Constants.DataTypes.DateTime)) { diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index b2d416c5cc..99dc6ef104 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -122,12 +122,16 @@ public class UmbracoPlan : MigrationPlan // To 16.2.0 To("{741C22CF-5FB8-4343-BF79-B97A58C2CCBA}"); - To("{BE11D4D3-3A1F-4598-90D4-B548BD188C48}"); // To 17.0.0 To("{17D5F6CA-CEB8-462A-AF86-4B9C3BF91CF1}"); To("{EB1E50B7-CD5E-4B6B-B307-36237DD2C506}"); To("{1847C7FF-B021-44EB-BEB0-A77A4376A6F2}"); To("{7208B20D-6BFC-472E-9374-85EEA817B27D}"); + To("{BE11D4D3-3A1F-4598-90D4-B548BD188C48}"); // Originally was V_16_2_0.AddDocumentUrlLock, now moved to a pre-migration. + + // To 16.3.0 + To("{A917FCBC-C378-4A08-A36C-220C581A6581}"); + To("{FB7073AF-DFAF-4AC1-800D-91F9BD5B5238}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs index d9774aa1ea..380262c23a 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPremigrationPlan.cs @@ -67,5 +67,11 @@ public class UmbracoPremigrationPlan : MigrationPlan To("{B9133686-B758-404D-AF12-708AA80C7E44}"); To("{EEB1F012-B44D-4AB4-8756-F7FB547345B4}"); To("{0F49E1A4-AFD8-4673-A91B-F64E78C48174}"); + + // To 16.2.0 + // - This needs to be a pre-migration as it adds a lock to the process for rebuilding document URLs, which is + // called by a migration for 15. By using a pre-migration we ensure the lock record is in place when migrating + // through 15 versions to the latest. + To("{5ECCE7A7-2EFC-47A5-A081-FFD94D9F79AA}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/AddRichTextEditorCapabilities.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/AddRichTextEditorCapabilities.cs new file mode 100644 index 0000000000..55219d0d9c --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/AddRichTextEditorCapabilities.cs @@ -0,0 +1,61 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_3_0; + +[Obsolete("Remove in Umbraco 18.")] +public class AddRichTextEditorCapabilities : AsyncMigrationBase +{ + private readonly IDataTypeService _dataTypeService; + + public AddRichTextEditorCapabilities(IMigrationContext context, IDataTypeService dataTypeService) + : base(context) + { + _dataTypeService = dataTypeService; + } + + protected override async Task MigrateAsync() + { + IEnumerable dataTypes = await _dataTypeService.GetByEditorUiAlias("Umb.PropertyEditorUi.Tiptap"); + + foreach (IDataType dataType in dataTypes) + { + HashSet extensions = new(); + + if (dataType.ConfigurationData.TryGetValue("extensions", out var tmp) && tmp is List existing) + { + extensions.UnionWith(existing); + } + + string[] newExtensions = + [ + "Umb.Tiptap.RichTextEssentials", + "Umb.Tiptap.Anchor", + "Umb.Tiptap.Block", + "Umb.Tiptap.Blockquote", + "Umb.Tiptap.Bold", + "Umb.Tiptap.BulletList", + "Umb.Tiptap.CodeBlock", + "Umb.Tiptap.Heading", + "Umb.Tiptap.HorizontalRule", + "Umb.Tiptap.HtmlAttributeClass", + "Umb.Tiptap.HtmlAttributeDataset", + "Umb.Tiptap.HtmlAttributeId", + "Umb.Tiptap.HtmlAttributeStyle", + "Umb.Tiptap.HtmlTagDiv", + "Umb.Tiptap.HtmlTagSpan", + "Umb.Tiptap.Italic", + "Umb.Tiptap.OrderedList", + "Umb.Tiptap.Strike", + "Umb.Tiptap.TrailingNode", + ]; + + extensions.UnionWith(newExtensions); + + dataType.ConfigurationData["extensions"] = extensions.ToArray(); + + _ = await _dataTypeService.UpdateAsync(dataType, Constants.Security.SuperUserKey); + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs new file mode 100644 index 0000000000..efa48f00f2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_16_3_0/MigrateMediaTypeLabelProperties.cs @@ -0,0 +1,211 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_16_3_0; + +[Obsolete("Remove in Umbraco 18.")] +public class MigrateMediaTypeLabelProperties : AsyncMigrationBase +{ + private readonly IMediaTypeService _mediaTypeService; + + private readonly InstallDefaultDataSettings? _dataTypeSettings; + private readonly InstallDefaultDataSettings? _mediaTypeSettings; + + private readonly Guid _labelBytesDataTypeKey = new(Constants.DataTypes.Guids.LabelBytes); + private readonly Guid _labelPixelsDataTypeKey = new(Constants.DataTypes.Guids.LabelPixels); + + public MigrateMediaTypeLabelProperties( + IMigrationContext context, + IMediaTypeService mediaTypeService, + IOptionsMonitor installDefaultDataSettings) + : base(context) + { + _mediaTypeService = mediaTypeService; + + _dataTypeSettings = installDefaultDataSettings.Get(Constants.Configuration.NamedOptions.InstallDefaultData.DataTypes); + _mediaTypeSettings = installDefaultDataSettings.Get(Constants.Configuration.NamedOptions.InstallDefaultData.MediaTypes); + } + + protected override async Task MigrateAsync() + { + if (_dataTypeSettings?.InstallData == InstallDefaultDataOption.None) + { + return; + } + + if (_mediaTypeSettings?.InstallData == InstallDefaultDataOption.None) + { + return; + } + + ToggleIndentityInsertForNodes(true); + try + { + IfNotExistsCreateBytesLabel(); + IfNotExistsCreatePixelsLabel(); + } + finally + { + ToggleIndentityInsertForNodes(false); + } + + await MigrateMediaTypeLabels(); + } + + private void ToggleIndentityInsertForNodes(bool toggleOn) + { + if (SqlSyntax.SupportsIdentityInsert()) + { + Database.Execute(new Sql($"SET IDENTITY_INSERT {SqlSyntax.GetQuotedTableName(NodeDto.TableName)} {(toggleOn ? "ON" : "OFF")} ")); + } + } + + private void IfNotExistsCreateBytesLabel() + { + if (Database.Exists(Constants.DataTypes.LabelBytes)) + { + return; + } + + var nodeDto = new NodeDto + { + NodeId = Constants.DataTypes.LabelBytes, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1," + Constants.DataTypes.LabelBytes, + SortOrder = 40, + UniqueId = _labelBytesDataTypeKey, + Text = "Label (bytes)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }; + + _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + + var dataTypeDto = new DataTypeDto + { + NodeId = Constants.DataTypes.LabelBytes, + EditorAlias = Constants.PropertyEditors.Aliases.Label, + EditorUiAlias = "Umb.PropertyEditorUi.Label", + DbType = nameof(ValueStorageType.Nvarchar), + Configuration = "{\"umbracoDataValueType\":\"BIGINT\", \"labelTemplate\":\"{=value | bytes}\"}", + }; + + _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + + private void IfNotExistsCreatePixelsLabel() + { + if (Database.Exists(Constants.DataTypes.LabelPixels)) + { + return; + } + + var nodeDto = new NodeDto + { + NodeId = Constants.DataTypes.LabelPixels, + Trashed = false, + ParentId = -1, + UserId = -1, + Level = 1, + Path = "-1," + Constants.DataTypes.LabelPixels, + SortOrder = 41, + UniqueId = _labelPixelsDataTypeKey, + Text = "Label (pixels)", + NodeObjectType = Constants.ObjectTypes.DataType, + CreateDate = DateTime.Now, + }; + + _ = Database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, nodeDto); + + var dataTypeDto = new DataTypeDto + { + NodeId = Constants.DataTypes.LabelPixels, + EditorAlias = Constants.PropertyEditors.Aliases.Label, + EditorUiAlias = "Umb.PropertyEditorUi.Label", + DbType = nameof(ValueStorageType.Integer), + Configuration = "{\"umbracoDataValueType\":\"INT\", \"labelTemplate\":\"{=value}px\"}", + }; + + _ = Database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, dataTypeDto); + } + + private async Task MigrateMediaTypeLabels() + { + // update all media types with the new data-type references + IMediaType[] allMediaTypes = _mediaTypeService.GetAll().ToArray(); + foreach (IMediaType mediaType in allMediaTypes) + { + bool updated = false; + + foreach (IPropertyType propertyType in mediaType.PropertyTypes) + { + switch (propertyType.Alias) + { + case Constants.Conventions.Media.Bytes when propertyType.DataTypeId == Constants.DataTypes.LabelBigint: + propertyType.DataTypeId = Constants.DataTypes.LabelBytes; + propertyType.DataTypeKey = _labelBytesDataTypeKey; + + if (propertyType.Name == "Size") + { + propertyType.Name = "File size"; + } + + if (propertyType.Description == "in bytes") + { + propertyType.Description = null; + } + + updated = true; + + break; + + case Constants.Conventions.Media.Height when propertyType.DataTypeId == Constants.DataTypes.LabelInt: + case Constants.Conventions.Media.Width when propertyType.DataTypeId == Constants.DataTypes.LabelInt: + propertyType.DataTypeId = Constants.DataTypes.LabelPixels; + propertyType.DataTypeKey = _labelPixelsDataTypeKey; + + if (propertyType.Description == "in pixels") + { + propertyType.Description = null; + } + + updated = true; + + break; + + case Constants.Conventions.Media.Extension when propertyType.DataTypeId == Constants.DataTypes.LabelString: + if (propertyType.Name == "Type") + { + propertyType.Name = "File extension"; + } + + updated = true; + + break; + + default: + break; + } + } + + if (updated) + { + Attempt attempt = await _mediaTypeService.UpdateAsync(mediaType, Constants.Security.SuperUserKey); + if (!attempt.Success) + { + Logger.LogError(attempt.Exception, $"Failed to update media type '{mediaType.Alias}' during migration."); + } + } + } + } +} diff --git a/src/Umbraco.Infrastructure/Serialization/JsonUdiConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonUdiConverter.cs index 0528f5e983..b909d9b3cc 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonUdiConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonUdiConverter.cs @@ -15,9 +15,14 @@ public sealed class JsonUdiConverter : JsonConverter /// public override Udi? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => reader.GetString() is string value - ? UdiParser.Parse(value) - : null; + { + if (reader.GetString() is string value && string.IsNullOrWhiteSpace(value) is false) + { + return UdiParser.Parse(value); + } + + return null; + } /// public override void Write(Utf8JsonWriter writer, Udi value, JsonSerializerOptions options) diff --git a/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs b/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs index 625f5594c4..5a674aa699 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactory.cs @@ -40,14 +40,18 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory public IPublishedContent? ToIPublishedContent(ContentCacheNode contentCacheNode, bool preview) { var cacheKey = $"{nameof(PublishedContentFactory)}DocumentCache_{contentCacheNode.Id}_{preview}"; - IPublishedContent? publishedContent = _appCaches.RequestCache.GetCacheItem(cacheKey); - if (publishedContent is not null) + IPublishedContent? publishedContent = null; + if (_appCaches.RequestCache.IsAvailable) { - _logger.LogDebug( - "Using cached IPublishedContent for document {ContentCacheNodeName} ({ContentCacheNodeId}).", - contentCacheNode.Data?.Name ?? "No Name", - contentCacheNode.Id); - return publishedContent; + publishedContent = _appCaches.RequestCache.GetCacheItem(cacheKey); + if (publishedContent is not null) + { + _logger.LogDebug( + "Using cached IPublishedContent for document {ContentCacheNodeName} ({ContentCacheNodeId}).", + contentCacheNode.Data?.Name ?? "No Name", + contentCacheNode.Id); + return publishedContent; + } } _logger.LogDebug( @@ -74,7 +78,7 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory publishedContent ??= GetPublishedContentAsDraft(publishedContent); } - if (publishedContent is not null) + if (_appCaches.RequestCache.IsAvailable && publishedContent is not null) { _appCaches.RequestCache.Set(cacheKey, publishedContent); } @@ -86,14 +90,18 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory public IPublishedContent? ToIPublishedMedia(ContentCacheNode contentCacheNode) { var cacheKey = $"{nameof(PublishedContentFactory)}MediaCache_{contentCacheNode.Id}"; - IPublishedContent? publishedContent = _appCaches.RequestCache.GetCacheItem(cacheKey); - if (publishedContent is not null) + IPublishedContent? publishedContent = null; + if (_appCaches.RequestCache.IsAvailable) { - _logger.LogDebug( - "Using cached IPublishedContent for media {ContentCacheNodeName} ({ContentCacheNodeId}).", - contentCacheNode.Data?.Name ?? "No Name", - contentCacheNode.Id); - return publishedContent; + publishedContent = _appCaches.RequestCache.GetCacheItem(cacheKey); + if (publishedContent is not null) + { + _logger.LogDebug( + "Using cached IPublishedContent for media {ContentCacheNodeName} ({ContentCacheNodeId}).", + contentCacheNode.Data?.Name ?? "No Name", + contentCacheNode.Id); + return publishedContent; + } } _logger.LogDebug( @@ -115,7 +123,7 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory publishedContent = GetModel(contentNode, false); - if (publishedContent is not null) + if (_appCaches.RequestCache.IsAvailable && publishedContent is not null) { _appCaches.RequestCache.Set(cacheKey, publishedContent); } @@ -127,15 +135,19 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory public IPublishedMember ToPublishedMember(IMember member) { string cacheKey = $"{nameof(PublishedContentFactory)}MemberCache_{member.Id}"; - IPublishedMember? publishedMember = _appCaches.RequestCache.GetCacheItem(cacheKey); - if (publishedMember is not null) + IPublishedMember? publishedMember = null; + if (_appCaches.RequestCache.IsAvailable) { - _logger.LogDebug( - "Using cached IPublishedMember for member {MemberName} ({MemberId}).", - member.Username, - member.Id); + publishedMember = _appCaches.RequestCache.GetCacheItem(cacheKey); + if (publishedMember is not null) + { + _logger.LogDebug( + "Using cached IPublishedMember for member {MemberName} ({MemberId}).", + member.Username, + member.Id); - return publishedMember; + return publishedMember; + } } _logger.LogDebug( @@ -169,7 +181,10 @@ internal sealed class PublishedContentFactory : IPublishedContentFactory contentData); publishedMember = new PublishedMember(member, contentNode, _elementsCache, _variationContextAccessor); - _appCaches.RequestCache.Set(cacheKey, publishedMember); + if (_appCaches.RequestCache.IsAvailable) + { + _appCaches.RequestCache.Set(cacheKey, publishedMember); + } return publishedMember; } diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs index 83f063a2b4..0ba4b6873b 100644 --- a/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedProperty.cs @@ -96,13 +96,23 @@ internal sealed class PublishedProperty : PublishedPropertyBase _content.VariationContextAccessor.ContextualizeVariation(_variations, _content.Id, _propertyTypeAlias, ref culture, ref segment); var value = GetSourceValue(culture, segment); - var hasValue = PropertyType.IsValue(value, PropertyValueLevel.Source); - if (hasValue.HasValue) + var isValue = PropertyType.IsValue(value, PropertyValueLevel.Source); + if (isValue.HasValue) { - return hasValue.Value; + return isValue.Value; } - return PropertyType.IsValue(GetInterValue(culture, segment), PropertyValueLevel.Object) ?? false; + value = GetInterValue(culture, segment); + isValue = PropertyType.IsValue(value, PropertyValueLevel.Inter); + if (isValue.HasValue) + { + return isValue.Value; + } + + value = GetValue(culture, segment); + isValue = PropertyType.IsValue(value, PropertyValueLevel.Object); + + return isValue ?? false; } public override object? GetSourceValue(string? culture = null, string? segment = null) diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts index 1cfd0f26bd..6a81876800 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/index.ts @@ -127,7 +127,7 @@ export { Underline, } from '@tiptap/extension-underline'; -/** @deprecated No longer used internally. This will be removed in Umbraco 18. [LK] */ +/** @deprecated No longer used internally. This will be removed in Umbraco 17. [LK] */ export { StarterKit } from '@tiptap/starter-kit'; // CUSTOM EXTENSIONS diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 9011b904c5..628b7998a6 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1070,17 +1070,35 @@ export const data: Array = [ alias: 'extensions', value: [ 'Umb.Tiptap.RichTextEssentials', + 'Umb.Tiptap.Anchor', + 'Umb.Tiptap.Block', + 'Umb.Tiptap.Blockquote', + 'Umb.Tiptap.Bold', + 'Umb.Tiptap.BulletList', + 'Umb.Tiptap.CodeBlock', 'Umb.Tiptap.Embed', 'Umb.Tiptap.Figure', + 'Umb.Tiptap.Heading', + 'Umb.Tiptap.HorizontalRule', + 'Umb.Tiptap.HtmlAttributeClass', + 'Umb.Tiptap.HtmlAttributeDataset', + 'Umb.Tiptap.HtmlAttributeId', + 'Umb.Tiptap.HtmlAttributeStyle', + 'Umb.Tiptap.HtmlTagDiv', + 'Umb.Tiptap.HtmlTagSpan', 'Umb.Tiptap.Image', + 'Umb.Tiptap.Italic', 'Umb.Tiptap.Link', 'Umb.Tiptap.MediaUpload', + 'Umb.Tiptap.OrderedList', + 'Umb.Tiptap.Strike', 'Umb.Tiptap.Subscript', 'Umb.Tiptap.Superscript', 'Umb.Tiptap.Table', 'Umb.Tiptap.TextAlign', 'Umb.Tiptap.TextDirection', 'Umb.Tiptap.TextIndent', + 'Umb.Tiptap.TrailingNode', 'Umb.Tiptap.Underline', 'Umb.Tiptap.WordCount', ], diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index ba2f45c35e..7a68f3056a 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -968,7 +968,7 @@ export const data: Array = [

Some value for the RTE with an external link and an internal link foo foo

The following tests the embed plugin:

-

+

Some value for the RTE with an external link and an internal link.

diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/global-components/content-type-workspace-editor-header.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/global-components/content-type-workspace-editor-header.element.ts index aed4ff718d..5495090dc7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/global-components/content-type-workspace-editor-header.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/global-components/content-type-workspace-editor-header.element.ts @@ -126,6 +126,7 @@ export class UmbContentTypeWorkspaceEditorHeaderElement extends UmbLitElement { display: flex; flex: 1 1 auto; flex-direction: column; + gap: 1px; } #name { @@ -135,7 +136,6 @@ export class UmbContentTypeWorkspaceEditorHeaderElement extends UmbLitElement { #description { width: 100%; - margin-top: 1px; --uui-input-height: var(--uui-size-8); --uui-input-border-color: transparent; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts index 9558578e39..83c4e57277 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-container-structure-helper.class.ts @@ -55,6 +55,7 @@ export class UmbContentTypeContainerStructureHelper (a.sortOrder || 0) - (b.sortOrder || 0)); this.#legacyMergedChildContainers.sortBy((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts index 6d97aaef57..4c1ad32a4d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/structure/content-type-structure-manager.class.ts @@ -941,12 +941,13 @@ export class UmbContentTypeStructureManager< for (const container of containers) { const path = getContainerChainKey(container, containerByIdCache, chainCache); const key = path?.join('|') ?? null; + const isOwner = this.isOwnerContainer(container.id); if (!mergedMap.has(key)) { // Store the first occurrence mergedMap.set(key, { key: key, ids: [container.id], - ownerId: this.isOwnerContainer(container.id) ? container.id : undefined, + ownerId: isOwner ? container.id : undefined, parentIds: new Set([container.parent?.id ?? null]), path: path, type: container.type, @@ -958,7 +959,11 @@ export class UmbContentTypeStructureManager< const existing = mergedMap.get(key)!; existing.ids.push(container.id); existing.parentIds.add(container.parent?.id ?? null); - existing.ownerId ??= this.isOwnerContainer(container.id) ? container.id : undefined; + existing.ownerId ??= isOwner ? container.id : undefined; + if (isOwner) { + // If this is the owner container, then we should update the sort order to ensure it is the one from the owner instance: [NL] + existing.sortOrder = container.sortOrder; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts index 9f632ab6fc..7b540e20bd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content-type/workspace/views/design/content-type-design-editor.element.ts @@ -37,9 +37,10 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements getUniqueOfElement: (element) => element.getAttribute('data-umb-tab-key'), getUniqueOfModel: (tab) => tab.key, identifier: 'content-type-tabs-sorter', - itemSelector: 'uui-tab', + itemSelector: 'uui-tab:not(#root-tab)', containerSelector: 'uui-tab-group', disabledItemSelector: ':not([sortable])', + ignorerSelector: 'uui-input', resolvePlacement: (args) => args.relatedRect.left + args.relatedRect.width * 0.5 > args.pointerX, onChange: ({ model }) => { this._tabs = model; @@ -475,7 +476,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements // TODO: Localize this: if (this._sortModeActive) return; return html` - + Add tab @@ -492,6 +493,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements ${this._compositionRepositoryAlias ? html` ` : ''} - + ${sortButtonText} @@ -571,26 +578,28 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements const tabName = hasTabName ? tab.name : 'Unnamed'; const tabId = tab.ownerId ?? tab.ids[0]; if (this._sortModeActive) { - return html`
+ return html`
${ownedTab - ? html` ${tabName} + ? html` ${this.localize.string(tabName)} this.#changeOrderNumber(tab, e)}>` - : html`${tab.name!}`} + : html`${this.localize.string(tabName)}`}
`; } if (tabActive && ownedTab) { - return html`
+ return html`
- ${hasTabName ? tab.name : 'Unnamed'} ${this.renderDeleteFor( - tab, - )} + ${hasTabName ? this.localize.string(tabName) : 'Unnamed'} + ${this.renderDeleteFor(tab)}
`; } else { - return html`
${tab.name!}
`; + return html`
+ ${this.localize.string(tabName)} +
`; } } @@ -621,6 +631,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements renderDeleteFor(tab: UmbPropertyTypeContainerMergedModel) { return html` uui-input { + pointer-events: auto; + } + .not-active uui-button { pointer-events: auto; } @@ -721,7 +744,7 @@ export class UmbContentTypeDesignEditorElement extends UmbLitElement implements gap: var(--uui-size-space-3); } - .invaild { + .invalid { color: var(--uui-color-danger, var(--uui-color-invalid)); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-context.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-context.interface.ts index 5df6b41d25..85f73bb743 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-context.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-context.interface.ts @@ -1,9 +1,8 @@ -import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbContentCollectionManager } from './manager/content-collection-manager.controller.js'; import type { UmbContentTypeModel, UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; import type { UmbEntityWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; export interface UmbContentCollectionWorkspaceContext extends UmbEntityWorkspaceContext { - contentTypeHasCollection: Observable; - getCollectionAlias(): string; + collection: UmbContentCollectionManager; structure: UmbContentTypeStructureManager; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-view.element.ts index 55fe2cf374..ce6d73d497 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace-view.element.ts @@ -1,13 +1,9 @@ import { UMB_CONTENT_COLLECTION_WORKSPACE_CONTEXT } from './content-collection-workspace.context-token.js'; import { customElement, html, nothing, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import { UmbDataTypeDetailRepository } from '@umbraco-cms/backoffice/data-type'; -import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; -import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type'; -import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; import type { UmbCollectionConfiguration } from '@umbraco-cms/backoffice/collection'; +import type { UmbWorkspaceViewElement } from '@umbraco-cms/backoffice/workspace'; -const elementName = 'umb-content-collection-workspace-view'; @customElement('umb-content-collection-workspace-view') export class UmbContentCollectionWorkspaceViewElement extends UmbLitElement implements UmbWorkspaceViewElement { @state() @@ -19,39 +15,18 @@ export class UmbContentCollectionWorkspaceViewElement extends UmbLitElement impl @state() private _collectionAlias?: string; - @state() - private _documentUnique?: string; - - #dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); - constructor() { super(); - this.#observeConfig(); - } - async #observeConfig() { this.consumeContext(UMB_CONTENT_COLLECTION_WORKSPACE_CONTEXT, (workspaceContext) => { - this._collectionAlias = workspaceContext?.getCollectionAlias(); - this._documentUnique = workspaceContext?.getUnique() ?? ''; + this._collectionAlias = workspaceContext?.collection.getCollectionAlias(); this.observe( - workspaceContext?.structure.ownerContentType, - async (contentType) => { - if (!contentType || !contentType.collection) return; - - const dataTypeUnique = contentType.collection.unique; - - if (dataTypeUnique) { - await this.#dataTypeDetailRepository.requestByUnique(dataTypeUnique); - this.observe( - await this.#dataTypeDetailRepository.byUnique(dataTypeUnique), - (dataType) => { - if (!dataType) return; - this._config = this.#mapDataTypeConfigToCollectionConfig(dataType); - this._loading = false; - }, - '_observeConfigDataType', - ); + workspaceContext?.collection.collectionConfig, + (config) => { + if (config) { + this._config = config; + this._loading = false; } }, '_observeConfigContentType', @@ -59,19 +34,6 @@ export class UmbContentCollectionWorkspaceViewElement extends UmbLitElement impl }); } - #mapDataTypeConfigToCollectionConfig(dataType: UmbDataTypeDetailModel): UmbCollectionConfiguration { - const config = new UmbPropertyEditorConfigCollection(dataType.values); - const pageSize = Number(config.getValueByAlias('pageSize')); - return { - unique: this._documentUnique, - layouts: config?.getValueByAlias('layouts'), - orderBy: config?.getValueByAlias('orderBy') ?? 'updateDate', - orderDirection: config?.getValueByAlias('orderDirection') ?? 'asc', - pageSize: isNaN(pageSize) ? 50 : pageSize, - userDefinedProperties: config?.getValueByAlias('includeProperties'), - }; - } - override render() { if (this._loading) return nothing; return html``; @@ -82,6 +44,6 @@ export { UmbContentCollectionWorkspaceViewElement as element }; declare global { interface HTMLElementTagNameMap { - [elementName]: UmbContentCollectionWorkspaceViewElement; + 'umb-content-collection-workspace-view': UmbContentCollectionWorkspaceViewElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace.context-token.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace.context-token.ts index eab9a2a5bb..5ce70e1ce8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace.context-token.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/content-collection-workspace.context-token.ts @@ -10,5 +10,5 @@ export const UMB_CONTENT_COLLECTION_WORKSPACE_CONTEXT = new UmbContextToken< 'UmbWorkspaceContext', undefined, (context): context is UmbContentCollectionWorkspaceContext => - (context as UmbContentCollectionWorkspaceContext).contentTypeHasCollection !== undefined, + (context as UmbContentCollectionWorkspaceContext).collection?.hasCollection !== undefined, ); diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/index.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/index.ts index 0ff461eb07..91bf96e532 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/index.ts @@ -1,2 +1,3 @@ export type * from './content-collection-workspace-context.interface.js'; export * from './content-collection-workspace.context-token.js'; +export * from './manager/content-collection-manager.controller.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/manager/content-collection-manager.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/manager/content-collection-manager.controller.ts new file mode 100644 index 0000000000..c80a039ad0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/manager/content-collection-manager.controller.ts @@ -0,0 +1,108 @@ +import { UmbBooleanState, UmbObjectState } from '@umbraco-cms/backoffice/observable-api'; +import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UmbDataTypeDetailRepository, type UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type'; +import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor'; +import type { ManifestWorkspaceView, UmbEntityWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; +import type { UmbCollectionConfiguration } from '@umbraco-cms/backoffice/collection'; +import type { UmbContentTypeModel, UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; + +type partialManifestWorkspaceView = Omit, 'meta'> & { + meta: Partial; +}; + +export class UmbContentCollectionManager< + ContentTypeDetailModelType extends UmbContentTypeModel = UmbContentTypeModel, +> extends UmbControllerBase { + #host: UmbEntityWorkspaceContext & UmbControllerHost; + + #collectionAlias?: string; + + #collectionConfig = new UmbObjectState(undefined); + readonly collectionConfig = this.#collectionConfig.asObservable(); + + #manifestOverrides = new UmbObjectState(undefined); + readonly manifestOverrides = this.#manifestOverrides.asObservable(); + + #hasCollection = new UmbBooleanState(false); + readonly hasCollection = this.#hasCollection.asObservable(); + + #dataTypeDetailRepository = new UmbDataTypeDetailRepository(this); + + constructor( + host: UmbEntityWorkspaceContext & UmbControllerHost, + structureManager: UmbContentTypeStructureManager, + collectionAlias?: string, + ) { + super(host); + + this.#host = host; + this.#collectionAlias = collectionAlias; + + this.observe( + collectionAlias ? structureManager.ownerContentType : undefined, + async (contentType) => { + this.#hasCollection.setValue(!!contentType?.collection); + + const dataTypeUnique = contentType?.collection?.unique; + if (dataTypeUnique) { + this.#dataTypeDetailRepository.requestByUnique(dataTypeUnique); + this.observe( + await this.#dataTypeDetailRepository.byUnique(dataTypeUnique), + (dataType) => { + this.#gotDataType(dataType); + }, + '_observeConfigDataType', + ); + } + }, + null, + ); + } + + getCollectionAlias() { + return this.#collectionAlias; + } + + #gotDataType(dataType?: UmbDataTypeDetailModel): void { + if (!dataType) { + this.#collectionConfig.setValue(undefined); + this.#manifestOverrides.setValue(undefined); + return; + } + + const config = new UmbPropertyEditorConfigCollection(dataType.values); + const pageSize = Number(config.getValueByAlias('pageSize')); + + this.#collectionConfig.setValue({ + unique: this.#host.getUnique(), + layouts: config?.getValueByAlias('layouts'), + orderBy: config?.getValueByAlias('orderBy') ?? 'updateDate', + orderDirection: config?.getValueByAlias('orderDirection') ?? 'asc', + pageSize: isNaN(pageSize) ? 50 : pageSize, + userDefinedProperties: config?.getValueByAlias('includeProperties'), + }); + + const overrides: partialManifestWorkspaceView = { + alias: 'Umb.WorkspaceView.Content.Collection', + meta: {}, + }; + + const overrideIcon = config?.getValueByAlias('icon'); + if (overrideIcon && overrideIcon !== '') { + overrides.meta!.icon = overrideIcon; + } + + const overrideLabel = config?.getValueByAlias('tabName'); + if (overrideLabel && overrideLabel !== '') { + overrides.meta!.label = overrideLabel; + } + + const showFirst = config?.getValueByAlias('showContentFirst'); + if (showFirst === true) { + overrides.weight = 150; + } + + this.#manifestOverrides.setValue(overrides); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/manifests.ts index c2a2a34eca..5b14e35e11 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/manifests.ts @@ -1,4 +1,6 @@ +import { UMB_WORKSPACE_HAS_CONTENT_COLLECTION_CONDITION_ALIAS } from './workspace-has-content-collection/constants.js'; import { manifests as workspaceHasContentCollectionManifests } from './workspace-has-content-collection/manifests.js'; +import { UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ @@ -19,5 +21,26 @@ export const manifests: Array = }, }, }, + { + type: 'workspaceView', + kind: 'contentCollection', + alias: 'Umb.WorkspaceView.Content.Collection', + name: 'Content Workspace Collection View', + weight: 1000, + meta: { + label: 'Collection', + pathname: 'collection', + icon: 'icon-grid', + }, + conditions: [ + { + alias: UMB_WORKSPACE_HAS_CONTENT_COLLECTION_CONDITION_ALIAS, + }, + { + alias: UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS, + match: false, + }, + ], + }, ...workspaceHasContentCollectionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/workspace-has-content-collection/workspace-has-content-collection.condition.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/workspace-has-content-collection/workspace-has-content-collection.condition.ts index 9cb170922e..5f9a1312c6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/workspace-has-content-collection/workspace-has-content-collection.condition.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/collection/workspace-has-content-collection/workspace-has-content-collection.condition.ts @@ -18,7 +18,7 @@ export class UmbWorkspaceHasContentCollectionCondition this.consumeContext(UMB_CONTENT_COLLECTION_WORKSPACE_CONTEXT, (context) => { this.observe( - context?.contentTypeHasCollection, + context?.collection.hasCollection, (hasCollection) => { this.permitted = hasCollection ?? false; }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts index 4459544348..7d4dd3a84f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/content-detail-workspace-base.ts @@ -1,37 +1,35 @@ import type { UmbContentDetailModel, UmbElementValueModel } from '../types.js'; +import { UmbContentCollectionManager } from '../collection/index.js'; import { UmbContentWorkspaceDataManager } from '../manager/index.js'; import { UmbMergeContentVariantDataController } from '../controller/merge-content-variant-data.controller.js'; import type { UmbContentVariantPickerData, UmbContentVariantPickerValue } from '../variant-picker/index.js'; import type { UmbContentPropertyDatasetContext } from '../property-dataset-context/index.js'; import type { UmbContentValidationRepository } from '../repository/content-validation-repository.interface.js'; +import type { UmbContentCollectionWorkspaceContext } from '../collection/content-collection-workspace-context.interface.js'; import type { UmbContentWorkspaceContext } from './content-workspace-context.interface.js'; import { UmbContentDetailValidationPathTranslator } from './content-detail-validation-path-translator.js'; import { UmbContentValidationToHintsManager } from './content-validation-to-hints.manager.js'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbDetailRepository, UmbDetailRepositoryConstructor } from '@umbraco-cms/backoffice/repository'; -import { - UmbEntityDetailWorkspaceContextBase, - UmbWorkspaceSplitViewManager, - type UmbEntityDetailWorkspaceContextArgs, - type UmbEntityDetailWorkspaceContextCreateArgs, - type UmbSaveableWorkspaceContext, -} from '@umbraco-cms/backoffice/workspace'; -import { - UmbContentTypeStructureManager, - type UmbContentTypeModel, - type UmbPropertyTypeModel, -} from '@umbraco-cms/backoffice/content-type'; -import { - UmbVariantId, - type UmbEntityVariantModel, - type UmbEntityVariantOptionModel, -} from '@umbraco-cms/backoffice/variant'; -import { UmbDeprecation, UmbReadOnlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; -import { UmbDataTypeDetailRepository, UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type'; import { appendToFrozenArray, mergeObservables, UmbArrayState } from '@umbraco-cms/backoffice/observable-api'; -import { UmbLanguageCollectionRepository, type UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; -import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; import { firstValueFrom, map } from '@umbraco-cms/backoffice/external/rxjs'; +import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; +import { UmbContentTypeStructureManager } from '@umbraco-cms/backoffice/content-type'; +import { UmbDataTypeDetailRepository, UmbDataTypeItemRepositoryManager } from '@umbraco-cms/backoffice/data-type'; +import { UmbDeprecation, UmbReadOnlyVariantGuardManager } from '@umbraco-cms/backoffice/utils'; +import { UmbEntityDetailWorkspaceContextBase, UmbWorkspaceSplitViewManager } from '@umbraco-cms/backoffice/workspace'; +import { + UmbEntityUpdatedEvent, + UmbRequestReloadChildrenOfEntityEvent, + UmbRequestReloadStructureForEntityEvent, +} from '@umbraco-cms/backoffice/entity-action'; +import { UmbHintContext } from '@umbraco-cms/backoffice/hint'; +import { UmbLanguageCollectionRepository } from '@umbraco-cms/backoffice/language'; +import { + UmbPropertyValuePresetVariantBuilderController, + UmbVariantPropertyGuardManager, +} from '@umbraco-cms/backoffice/property'; +import { UmbSegmentCollectionRepository } from '@umbraco-cms/backoffice/segment'; +import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UMB_VALIDATION_CONTEXT, UMB_VALIDATION_EMPTY_LOCALIZATION_KEY, @@ -39,23 +37,22 @@ import { UmbServerModelValidatorContext, UmbValidationController, } from '@umbraco-cms/backoffice/validation'; -import type { UmbModalToken } from '@umbraco-cms/backoffice/modal'; -import { umbOpenModal } from '@umbraco-cms/backoffice/modal'; -import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; -import { - UmbEntityUpdatedEvent, - UmbRequestReloadChildrenOfEntityEvent, - UmbRequestReloadStructureForEntityEvent, -} from '@umbraco-cms/backoffice/entity-action'; import type { ClassConstructor } from '@umbraco-cms/backoffice/extension-api'; -import { - UmbPropertyValuePresetVariantBuilderController, - UmbVariantPropertyGuardManager, - type UmbPropertyTypePresetModel, - type UmbPropertyTypePresetModelTypeModel, -} from '@umbraco-cms/backoffice/property'; -import { UmbSegmentCollectionRepository, type UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment'; -import { UmbHintContext, type UmbVariantHint } from '@umbraco-cms/backoffice/hint'; +import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbContentTypeModel, UmbPropertyTypeModel } from '@umbraco-cms/backoffice/content-type'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbDetailRepository, UmbDetailRepositoryConstructor } from '@umbraco-cms/backoffice/repository'; +import type { + UmbEntityDetailWorkspaceContextArgs, + UmbEntityDetailWorkspaceContextCreateArgs, + UmbSaveableWorkspaceContext, +} from '@umbraco-cms/backoffice/workspace'; +import type { UmbEntityVariantModel, UmbEntityVariantOptionModel } from '@umbraco-cms/backoffice/variant'; +import type { UmbLanguageDetailModel } from '@umbraco-cms/backoffice/language'; +import type { UmbPropertyTypePresetModel, UmbPropertyTypePresetModelTypeModel } from '@umbraco-cms/backoffice/property'; +import type { UmbModalToken } from '@umbraco-cms/backoffice/modal'; +import type { UmbSegmentCollectionItemModel } from '@umbraco-cms/backoffice/segment'; +import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; export interface UmbContentDetailWorkspaceContextArgs< DetailModelType extends UmbContentDetailModel, @@ -71,6 +68,7 @@ export interface UmbContentDetailWorkspaceContextArgs< ignoreValidationResultOnSubmit?: boolean; contentVariantScaffold: VariantModelType; contentTypePropertyName: string; + collectionAlias?: string; saveModalToken?: UmbModalToken, UmbContentVariantPickerValue>; } @@ -102,7 +100,8 @@ export abstract class UmbContentDetailWorkspaceContextBase< extends UmbEntityDetailWorkspaceContextBase implements UmbContentWorkspaceContext, - UmbSaveableWorkspaceContext + UmbSaveableWorkspaceContext, + UmbContentCollectionWorkspaceContext { public readonly IS_CONTENT_WORKSPACE_CONTEXT = true as const; @@ -140,6 +139,8 @@ export abstract class UmbContentDetailWorkspaceContextBase< /* Split View */ readonly splitView = new UmbWorkspaceSplitViewManager(); + readonly collection: UmbContentCollectionManager; + /* Hints */ readonly hints = new UmbHintContext(this); @@ -210,6 +211,12 @@ export abstract class UmbContentDetailWorkspaceContextBase< x ? x.variesByCulture || x.variesBySegment : undefined, ); + this.collection = new UmbContentCollectionManager( + this, + this.structure, + args.collectionAlias, + ); + new UmbContentValidationToHintsManager( this, this.structure, diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts index 357e2eb0ac..ec378b6737 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/collection.element.ts @@ -5,8 +5,7 @@ import { customElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbExtensionElementAndApiSlotElementBase } from '@umbraco-cms/backoffice/extension-registry'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; -const elementName = 'umb-collection'; -@customElement(elementName) +@customElement('umb-collection') export class UmbCollectionElement< ConfigType extends UmbCollectionConfiguration = UmbCollectionConfiguration, FilterType extends UmbCollectionFilterModel = UmbCollectionFilterModel, @@ -62,6 +61,6 @@ export class UmbCollectionElement< declare global { interface HTMLElementTagNameMap { - [elementName]: UmbCollectionElement; + 'umb-collection': UmbCollectionElement; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts index 3314fef940..c06c607b27 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/collection/types.ts @@ -1,5 +1,6 @@ import type { ManifestCollection } from './extensions/types.js'; import type { Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import type { UmbEntityUnique } from '@umbraco-cms/backoffice/entity'; import type { UmbPaginationManager } from '@umbraco-cms/backoffice/utils'; export type * from './action/create/types.js'; @@ -17,7 +18,7 @@ export interface UmbCollectionBulkActionPermissions { } export interface UmbCollectionConfiguration { - unique?: string; + unique?: UmbEntityUnique; dataTypeId?: string; /** @deprecated No longer used internally. This will be removed in Umbraco 17. [LK] */ allowedEntityBulkActions?: UmbCollectionBulkActionPermissions; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts index a91c9ac5d4..4e8094719b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-with-alias/input-with-alias.element.ts @@ -131,6 +131,9 @@ export class UmbInputWithAliasElement extends UmbFormControlMixin>[]); - public readonly views = this.#views.asObservable(); + + #manifests = new UmbBasicState>([]); + #overrides = new UmbBasicState>>([]); + public readonly views = mergeObservables( + [this.#manifests.asObservable(), this.#overrides.asObservable()], + ([manifests, overrides]): Array => { + let contexts = this.#contexts; + + // remove ones that are no longer contained in the workspaceViews (And thereby make the new array): + const contextsToKeep = contexts.filter( + (view) => !manifests.some((manifest) => manifest.alias === view.manifest.alias), + ); + + const hasDiff = contextsToKeep.length !== manifests.length; + if (hasDiff) { + contexts = [...contextsToKeep]; + + // Add ones that are new: + manifests + .filter((manifest) => !contextsToKeep.some((x) => x.manifest.alias === manifest.alias)) + .forEach((manifest) => { + const context = new UmbWorkspaceViewContext(this, manifest); + context.setVariantId(this.#variantId); + context.hints.inheritFrom(this.#hints); + contexts.push(context); + }); + } + + // Apply overrides: + contexts.forEach((context) => { + const override = overrides.find((x) => x.alias === context.manifest.alias); + if (override) { + // Test to see if there is a change, to avoid unnecessary updates, this prevents re-setting the manifest again and again. [NL] + const overrideKeys = Object.keys(override) as Array; + const hasOverrideDiff = overrideKeys.some((key) => context.manifest[key] !== override[key]); + if (hasOverrideDiff) { + context.manifest = { + ...context.manifest, + ...(override as ManifestWorkspaceView), + meta: { ...context.manifest.meta, ...override.meta }, + }; + } + } + }); + + // sort contexts to match manifests weights: + contexts.sort((a, b): number => (b.manifest.weight || 0) - (a.manifest.weight || 0)); + + this.#contexts = contexts; + return contexts; + }, + // Custom memoize method, to check context instance and manifest instance: + (previousValue: Array, currentValue: Array): boolean => { + return ( + previousValue === currentValue && + currentValue.some( + (x) => x.manifest === previousValue.find((y) => y.manifest.alias === x.manifest.alias)?.manifest, + ) + ); + }, + ); + // A storage and cache for the current contexts, to enable communicating to them and to avoid re-initializing them every time there is a change of manifests/overrides. [NL] + #contexts = new Array(); #variantId?: UmbVariantId; #hints = new UmbHintController(this, {}); @@ -31,46 +95,25 @@ export class UmbWorkspaceEditorContext extends UmbContextBase { 'workspaceView', null, (workspaceViews) => { - const oldViews = this.#views.getValue(); - - // remove ones that are no longer contained in the workspaceViews (And thereby make the new array): - const viewsToKeep = oldViews.filter( - (view) => !workspaceViews.some((x) => x.manifest.alias === view.manifest.alias), - ); - - const hasDiff = viewsToKeep.length !== workspaceViews.length; - - if (hasDiff) { - const newViews = [...viewsToKeep]; - - // Add ones that are new: - workspaceViews - .filter((view) => !viewsToKeep.some((x) => x.manifest.alias === view.manifest.alias)) - .forEach((view) => { - const context = new UmbWorkspaceViewContext(this, view.manifest); - context.setVariantId(this.#variantId); - context.hints.inheritFrom(this.#hints); - newViews.push(context); - }); - - this.#views.setValue(newViews); - } + this.#manifests.setValue(workspaceViews.map((controller) => controller.manifest)); }, - 'initViewApis', - {}, ).asPromise(); } setVariantId(variantId: UmbVariantId | undefined): void { this.#variantId = variantId; this.#hints.updateScaffold({ variantId }); - this.#views.getValue().forEach((view) => { + this.#contexts.forEach((view) => { view.hints.updateScaffold({ variantId }); }); } + setOverrides(overrides?: Array>): void { + this.#overrides.setValue(overrides ?? []); + } + async getViewContext(alias: string): Promise { await this.#init; - return this.#views.getValue().find((view) => view.manifest.alias === alias); + return this.#contexts.find((view) => view.manifest.alias === alias); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts index 72276e6ac8..300a8fdc15 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-editor.element.ts @@ -1,12 +1,14 @@ import { UMB_WORKSPACE_VIEW_PATH_PATTERN } from '../../paths.js'; +import type { ManifestWorkspaceView } from '../../types.js'; import { UmbWorkspaceEditorContext } from './workspace-editor.context.js'; import type { UmbWorkspaceViewContext } from './workspace-view.context.js'; import { css, customElement, html, nothing, property, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { createExtensionElement } from '@umbraco-cms/backoffice/extension-api'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; +import type { UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils'; import type { UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbRoute, UmbRouterSlotInitEvent, UmbRouterSlotChangeEvent } from '@umbraco-cms/backoffice/router'; import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import type { UmbVariantHint } from '@umbraco-cms/backoffice/hint'; @@ -56,6 +58,11 @@ export class UmbWorkspaceEditorElement extends UmbLitElement { } private _variantId?: UmbVariantId | undefined; + @property({ attribute: false }) + public set overrides(value: Array> | undefined) { + this.#navigationContext.setOverrides(value); + } + @state() private _workspaceViews: Array = []; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts index a268bde27f..eafe506578 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-editor/workspace-view.context.ts @@ -4,6 +4,7 @@ import { UmbViewContext } from '@umbraco-cms/backoffice/view'; export class UmbWorkspaceViewContext extends UmbViewContext { public readonly IS_WORKSPACE_VIEW_CONTEXT = true as const; + // Note: manifest can change later, but because we currently only use the alias from it, it's not something we need to handle. [NL] public manifest: ManifestWorkspaceView; constructor(host: UmbControllerHost, manifest: ManifestWorkspaceView) { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts index 789994afc5..d852da05b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-footer/workspace-footer.element.ts @@ -28,9 +28,6 @@ function ExtensionApiArgsMethod( // TODO: stop naming this something with layout. as its not just an layout. it hooks up with extensions. @customElement('umb-workspace-footer') export class UmbWorkspaceFooterLayoutElement extends UmbLitElement { - @state() - private _withinModal = false; - @state() private _modalContext?: UmbModalContext; @@ -63,13 +60,12 @@ export class UmbWorkspaceFooterLayoutElement extends UmbLitElement { label=${this._isNew ? 'Cancel' : 'Close'} @click=${this.#rejectModal}>` : ''} + - - `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts index 86cf1f63bd..61b0f317c0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/components/workspace-split-view/workspace-split-view.element.ts @@ -1,20 +1,22 @@ +import type { ManifestWorkspaceView } from '../../types.js'; import { UmbWorkspaceSplitViewContext } from './workspace-split-view.context.js'; import { css, - html, customElement, - property, + html, ifDefined, + nothing, + property, state, when, - nothing, } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; // import local components import './workspace-split-view-variant-selector.element.js'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; /** * @@ -30,6 +32,9 @@ export class UmbWorkspaceSplitViewElement extends UmbLitElement { @property({ attribute: 'back-path' }) public backPath?: string; + @property({ attribute: false }) + public overrides?: Array>; + @property({ type: Number }) public set splitViewIndex(index: number) { this.splitViewContext.setSplitViewIndex(index); @@ -79,6 +84,7 @@ export class UmbWorkspaceSplitViewElement extends UmbLitElement { back-path=${ifDefined(this.backPath)} .hideNavigation=${!this.displayNavigation} .variantId=${this._variantId} + .overrides=${this.overrides} .enforceNoFooter=${true}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view.element.ts index bd82b1d110..7a1eed8c6c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace-split-view.element.ts @@ -1,8 +1,9 @@ import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from './document-workspace.context-token.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, nothing, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbActiveVariant } from '@umbraco-cms/backoffice/workspace'; +import { css, customElement, html, ifDefined, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { ManifestWorkspaceView, UmbActiveVariant } from '@umbraco-cms/backoffice/workspace'; +import type { UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils'; import './document-workspace-split-view-variant-selector.element.js'; @@ -17,6 +18,9 @@ export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement { @state() private _icon?: string; + @state() + private _overrides?: Array>; + constructor() { super(); @@ -25,6 +29,7 @@ export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement { this._workspaceContext = context; this.#observeActiveVariantInfo(); this.#observeIcon(); + this.#observeCollectionOverrides(); }); } @@ -44,6 +49,12 @@ export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement { }); } + #observeCollectionOverrides() { + this.observe(this._workspaceContext?.collection.manifestOverrides, (overrides) => { + this._overrides = overrides ? [overrides] : undefined; + }); + } + override render() { return this._variants ? html`
@@ -53,8 +64,9 @@ export class UmbDocumentWorkspaceSplitViewElement extends UmbLitElement { view.index + '_' + (view.culture ?? '') + '_' + (view.segment ?? '') + '_' + this._variants!.length, (view) => html` + .displayNavigation=${view.index === this._variants!.length - 1} + .overrides=${this._overrides} + .splitViewIndex=${view.index}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index c92309e575..a1b1a2ca4d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -19,26 +19,23 @@ import { UMB_DOCUMENT_PUBLISHING_WORKSPACE_CONTEXT, UmbDocumentPublishingReposit import { UmbDocumentValidationRepository } from '../repository/validation/index.js'; import { UMB_DOCUMENT_CONFIGURATION_CONTEXT } from '../index.js'; import { UMB_DOCUMENT_DETAIL_MODEL_VARIANT_SCAFFOLD, UMB_DOCUMENT_WORKSPACE_ALIAS } from './constants.js'; -import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { ensurePathEndsWithSlash, UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import { UmbContentDetailWorkspaceContextBase } from '@umbraco-cms/backoffice/content'; +import { UmbDocumentBlueprintDetailRepository } from '@umbraco-cms/backoffice/document-blueprint'; +import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; import { - type UmbPublishableWorkspaceContext, UmbWorkspaceIsNewRedirectController, UmbWorkspaceIsNewRedirectControllerAlias, } from '@umbraco-cms/backoffice/workspace'; -import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import { UmbDocumentBlueprintDetailRepository } from '@umbraco-cms/backoffice/document-blueprint'; -import { - UmbContentDetailWorkspaceContextBase, - type UmbContentCollectionWorkspaceContext, - type UmbContentWorkspaceContext, -} from '@umbraco-cms/backoffice/content'; -import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type'; -import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; -import { ensurePathEndsWithSlash, UmbDeprecation } from '@umbraco-cms/backoffice/utils'; -import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; import { UMB_SERVER_CONTEXT } from '@umbraco-cms/backoffice/server'; -import { observeMultiple } from '@umbraco-cms/backoffice/observable-api'; +import type { UmbContentWorkspaceContext } from '@umbraco-cms/backoffice/content'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import type { UmbDocumentTypeDetailModel } from '@umbraco-cms/backoffice/document-type'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbPublishableWorkspaceContext } from '@umbraco-cms/backoffice/workspace'; import type { UmbVariantPropertyGuardRule } from '@umbraco-cms/backoffice/property'; type ContentModel = UmbDocumentDetailModel; @@ -52,8 +49,7 @@ export class UmbDocumentWorkspaceContext > implements UmbContentWorkspaceContext, - UmbPublishableWorkspaceContext, - UmbContentCollectionWorkspaceContext + UmbPublishableWorkspaceContext { /** * The publishing repository for the document workspace. @@ -63,10 +59,16 @@ export class UmbDocumentWorkspaceContext public readonly publishingRepository = new UmbDocumentPublishingRepository(this); readonly isTrashed = this._data.createObservablePartOfCurrent((data) => data?.isTrashed); + readonly contentTypeUnique = this._data.createObservablePartOfCurrent((data) => data?.documentType.unique); + + /* + * @deprecated Use `collection.hasCollection` instead, will be removed in v.18 + */ readonly contentTypeHasCollection = this._data.createObservablePartOfCurrent( (data) => !!data?.documentType.collection, ); + readonly contentTypeIcon = this._data.createObservablePartOfCurrent((data) => data?.documentType.icon || null); readonly templateId = this._data.createObservablePartOfCurrent((data) => data?.template?.unique || null); @@ -78,6 +80,7 @@ export class UmbDocumentWorkspaceContext super(host, { entityType: UMB_DOCUMENT_ENTITY_TYPE, workspaceAlias: UMB_DOCUMENT_WORKSPACE_ALIAS, + collectionAlias: UMB_DOCUMENT_COLLECTION_ALIAS, detailRepositoryAlias: UMB_DOCUMENT_DETAIL_REPOSITORY_ALIAS, contentTypeDetailRepository: UmbDocumentTypeDetailRepository, contentValidationRepository: UmbDocumentValidationRepository, @@ -248,6 +251,7 @@ export class UmbDocumentWorkspaceContext }); } + /** @deprecated will be removed in v.18 */ getCollectionAlias() { return UMB_DOCUMENT_COLLECTION_ALIAS; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts index ccae31b036..3c5c1e6506 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/manifests.ts @@ -1,14 +1,8 @@ import { UMB_DOCUMENT_ENTITY_TYPE } from '../entity.js'; import { UMB_DOCUMENT_WORKSPACE_ALIAS } from './constants.js'; import { manifests as actionManifests } from './actions/manifests.js'; -import { - UMB_CONTENT_HAS_PROPERTIES_WORKSPACE_CONDITION, - UMB_WORKSPACE_HAS_CONTENT_COLLECTION_CONDITION_ALIAS, -} from '@umbraco-cms/backoffice/content'; -import { - UMB_WORKSPACE_CONDITION_ALIAS, - UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS, -} from '@umbraco-cms/backoffice/workspace'; +import { UMB_CONTENT_HAS_PROPERTIES_WORKSPACE_CONDITION } from '@umbraco-cms/backoffice/content'; +import { UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; export const manifests: Array = [ { @@ -21,30 +15,6 @@ export const manifests: Array = [ entityType: UMB_DOCUMENT_ENTITY_TYPE, }, }, - { - type: 'workspaceView', - kind: 'contentCollection', - alias: 'Umb.WorkspaceView.Document.Collection', - name: 'Document Workspace Collection View', - meta: { - label: 'Collection', - pathname: 'collection', - icon: 'icon-grid', - }, - conditions: [ - { - alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: UMB_DOCUMENT_WORKSPACE_ALIAS, - }, - { - alias: UMB_WORKSPACE_HAS_CONTENT_COLLECTION_CONDITION_ALIAS, - }, - { - alias: UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS, - match: false, - }, - ], - }, { type: 'workspaceView', kind: 'contentEditor', diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts index d43932d72c..a4627ec205 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/manifests.ts @@ -1,14 +1,7 @@ import { UMB_MEDIA_WORKSPACE_ALIAS } from './constants.js'; -import { - UmbSubmitWorkspaceAction, - UMB_WORKSPACE_CONDITION_ALIAS, - UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS, -} from '@umbraco-cms/backoffice/workspace'; +import { UmbSubmitWorkspaceAction, UMB_WORKSPACE_CONDITION_ALIAS } from '@umbraco-cms/backoffice/workspace'; +import { UMB_CONTENT_HAS_PROPERTIES_WORKSPACE_CONDITION } from '@umbraco-cms/backoffice/content'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS } from '@umbraco-cms/backoffice/recycle-bin'; -import { - UMB_CONTENT_HAS_PROPERTIES_WORKSPACE_CONDITION, - UMB_WORKSPACE_HAS_CONTENT_COLLECTION_CONDITION_ALIAS, -} from '@umbraco-cms/backoffice/content'; export const manifests: Array = [ { @@ -21,30 +14,6 @@ export const manifests: Array = [ entityType: 'media', }, }, - { - type: 'workspaceView', - kind: 'contentCollection', - alias: 'Umb.WorkspaceView.Media.Collection', - name: 'Media Workspace Collection View', - meta: { - label: 'Collection', - pathname: 'collection', - icon: 'icon-grid', - }, - conditions: [ - { - alias: UMB_WORKSPACE_CONDITION_ALIAS, - match: UMB_MEDIA_WORKSPACE_ALIAS, - }, - { - alias: UMB_WORKSPACE_HAS_CONTENT_COLLECTION_CONDITION_ALIAS, - }, - { - alias: UMB_WORKSPACE_ENTITY_IS_NEW_CONDITION_ALIAS, - match: false, - }, - ], - }, { type: 'workspaceView', kind: 'contentEditor', diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts index ba25bcaf1d..232288ee41 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace-split-view.element.ts @@ -1,8 +1,9 @@ import { UMB_MEDIA_WORKSPACE_CONTEXT } from './media-workspace.context-token.js'; -import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; -import { css, html, nothing, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import type { UmbActiveVariant } from '@umbraco-cms/backoffice/workspace'; +import { css, customElement, html, ifDefined, nothing, repeat, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { ManifestWorkspaceView, UmbActiveVariant } from '@umbraco-cms/backoffice/workspace'; +import type { UmbDeepPartialObject } from '@umbraco-cms/backoffice/utils'; @customElement('umb-media-workspace-split-view') export class UmbMediaWorkspaceSplitViewElement extends UmbLitElement { @@ -15,6 +16,9 @@ export class UmbMediaWorkspaceSplitViewElement extends UmbLitElement { @state() private _icon?: string; + @state() + private _overrides?: Array>; + constructor() { super(); @@ -23,6 +27,7 @@ export class UmbMediaWorkspaceSplitViewElement extends UmbLitElement { this._workspaceContext = context; this.#observeActiveVariantInfo(); this.#observeIcon(); + this.#observeCollectionOverrides(); }); } @@ -44,6 +49,12 @@ export class UmbMediaWorkspaceSplitViewElement extends UmbLitElement { }); } + #observeCollectionOverrides() { + this.observe(this._workspaceContext?.collection.manifestOverrides, (overrides) => { + this._overrides = overrides ? [overrides] : undefined; + }); + } + override render() { return this._variants ? html`
@@ -53,8 +64,9 @@ export class UmbMediaWorkspaceSplitViewElement extends UmbLitElement { view.index + '_' + (view.culture ?? '') + '_' + (view.segment ?? '') + '_' + this._variants!.length, (view) => html` + .displayNavigation=${view.index === this._variants!.length - 1} + .overrides=${this._overrides} + .splitViewIndex=${view.index}> `, diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index c8a61f1a46..4668315e9b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -8,19 +8,15 @@ import { UmbMediaValidationRepository } from '../repository/validation/media-val import { UMB_MEDIA_COLLECTION_ALIAS } from '../collection/constants.js'; import type { UmbMediaDetailRepository } from '../repository/index.js'; import { UMB_MEDIA_WORKSPACE_ALIAS, UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD } from './constants.js'; -import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbContentDetailWorkspaceContextBase, type UmbContentWorkspaceContext } from '@umbraco-cms/backoffice/content'; +import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; import { UmbWorkspaceIsNewRedirectController, UmbWorkspaceIsNewRedirectControllerAlias, } from '@umbraco-cms/backoffice/workspace'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbMediaTypeDetailModel } from '@umbraco-cms/backoffice/media-type'; -import { - UmbContentDetailWorkspaceContextBase, - type UmbContentCollectionWorkspaceContext, - type UmbContentWorkspaceContext, -} from '@umbraco-cms/backoffice/content'; -import { UmbIsTrashedEntityContext } from '@umbraco-cms/backoffice/recycle-bin'; +import type { UmbVariantId } from '@umbraco-cms/backoffice/variant'; type ContentModel = UmbMediaDetailModel; type ContentTypeModel = UmbMediaTypeDetailModel; @@ -32,11 +28,12 @@ export class UmbMediaWorkspaceContext ContentTypeModel, UmbMediaVariantModel > - implements - UmbContentWorkspaceContext, - UmbContentCollectionWorkspaceContext + implements UmbContentWorkspaceContext { readonly contentTypeUnique = this._data.createObservablePartOfCurrent((data) => data?.mediaType.unique); + /* + * @deprecated Use `collection.hasCollection` instead, will be removed in v.18 + */ readonly contentTypeHasCollection = this._data.createObservablePartOfCurrent((data) => !!data?.mediaType.collection); readonly contentTypeIcon = this._data.createObservablePartOfCurrent((data) => data?.mediaType.icon); @@ -51,6 +48,7 @@ export class UmbMediaWorkspaceContext contentValidationRepository: UmbMediaValidationRepository, contentVariantScaffold: UMB_MEMBER_DETAIL_MODEL_VARIANT_SCAFFOLD, contentTypePropertyName: 'mediaType', + collectionAlias: UMB_MEDIA_COLLECTION_ALIAS, }); this.observe( @@ -124,6 +122,9 @@ export class UmbMediaWorkspaceContext }); } + /* + * @deprecated Use `collection.getCollectionAlias()` instead. Will be removed in v.18 + */ public getCollectionAlias() { return UMB_MEDIA_COLLECTION_ALIAS; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/property-editor-ui-collection.element.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/property-editor-ui-collection.element.ts index ec35f968ec..9f2afbf372 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/property-editor-ui-collection.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/collection/property-editor-ui-collection.element.ts @@ -2,7 +2,7 @@ import type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement, } from '@umbraco-cms/backoffice/property-editor'; -import { html, customElement, property, state } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UMB_DOCUMENT_COLLECTION_ALIAS } from '@umbraco-cms/backoffice/document'; import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property'; @@ -14,6 +14,9 @@ import type { UmbCollectionConfiguration } from '@umbraco-cms/backoffice/collect */ @customElement('umb-property-editor-ui-collection') export class UmbPropertyEditorUICollectionElement extends UmbLitElement implements UmbPropertyEditorUiElement { + #workspaceContext?: typeof UMB_CONTENT_COLLECTION_WORKSPACE_CONTEXT.TYPE; + #propertyContext?: typeof UMB_PROPERTY_CONTEXT.TYPE; + @property() value?: string; @@ -31,22 +34,37 @@ export class UmbPropertyEditorUICollectionElement extends UmbLitElement implemen super(); this.consumeContext(UMB_CONTENT_COLLECTION_WORKSPACE_CONTEXT, (workspaceContext) => { - this._collectionAlias = workspaceContext?.getCollectionAlias() ?? UMB_DOCUMENT_COLLECTION_ALIAS; + this.#workspaceContext = workspaceContext; + this._collectionAlias = workspaceContext?.collection.getCollectionAlias() ?? UMB_DOCUMENT_COLLECTION_ALIAS; + this.#gotContexts(); + }); - this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { - this.observe(propertyContext?.alias, async (propertyAlias) => { - if (propertyAlias) { - // Gets the Data Type ID for the current property. - const property = await workspaceContext!.structure.getPropertyStructureByAlias(propertyAlias); - const unique = workspaceContext!.getUnique(); - if (unique && property && this._config) { - this._config.unique = unique; - this._config.dataTypeId = property.dataType.unique; - this.requestUpdate('_config'); - } - } - }); - }); + this.consumeContext(UMB_PROPERTY_CONTEXT, (propertyContext) => { + this.#propertyContext = propertyContext; + this.#gotContexts(); + }); + } + + #gotContexts() { + if (!this.#workspaceContext || !this.#propertyContext) return; + + this.observe(this.#propertyContext?.alias, async (propertyAlias) => { + if (this.#workspaceContext && propertyAlias) { + // Gets the Data Type ID for the current property. + const property = await this.#workspaceContext.structure.getPropertyStructureByAlias(propertyAlias); + if (!this.#workspaceContext) { + // We got destroyed in the meantime. + return; + } + + const unique = this.#workspaceContext.getUnique(); + if (unique && property && this._config) { + // TODO: Handle case where config might not be set when this executes during initialization, its not likely but it is fragile to assume this. [NL] + this._config.unique = unique; + this._config.dataTypeId = property.dataType.unique; + this.requestUpdate('_config'); + } + } }); } @@ -67,6 +85,12 @@ export class UmbPropertyEditorUICollectionElement extends UmbLitElement implemen if (!this._config?.unique || !this._config?.dataTypeId) return html``; return html``; } + + override destroy(): void { + super.destroy(); + this.#workspaceContext = undefined; + this.#propertyContext = undefined; + } } export default UmbPropertyEditorUICollectionElement; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/detail/template-detail.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/detail/template-detail.server.data-source.ts index c6920483d7..f0fe1ab340 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/detail/template-detail.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/templates/repository/detail/template-detail.server.data-source.ts @@ -43,7 +43,7 @@ export class UmbTemplateServerDataSource implements UmbDetailDataSource @@ -66,19 +67,22 @@ export class UmbTemplateWorkspaceContext override async load(unique: string) { const response = await super.load(unique); - if (response.data) { - this.setMasterTemplate(response.data.masterTemplate?.unique ?? null); - } + await this.setMasterTemplate(response.data?.masterTemplate?.unique ?? null); return response; } - async create(parent: any) { - const data = await this.createScaffold({ parent }); + async create(parent: UmbEntityModel) { + const data = await this.createScaffold({ + parent, + preset: { + masterTemplate: parent.unique ? { unique: parent.unique } : null, + }, + }); + + // Set or reset the master template + // This is important to reset when a new template is created so the UI reflects the correct state + await this.setMasterTemplate(parent.unique); - if (data) { - if (!parent) return; - await this.setMasterTemplate(parent.unique); - } return data; } @@ -98,20 +102,21 @@ export class UmbTemplateWorkspaceContext return this.getData()?.content ? this.getLayoutBlockRegexPattern().test(this.getData()?.content as string) : false; } - async setMasterTemplate(id: string | null) { - if (id === null) { + async setMasterTemplate(unique: string | null) { + if (unique === null) { this.#masterTemplate.setValue(null); - this.#updateMasterTemplateLayoutBlock(); - return null; + } else { + // We need the whole template model if the unique id is provided + const { data } = await this.itemRepository.requestItems([unique]); + if (data) { + this.#masterTemplate.setValue(data[0]); + } } - const { data } = await this.itemRepository.requestItems([id]); - if (data) { - this.#masterTemplate.setValue(data[0]); - this.#updateMasterTemplateLayoutBlock(); - return data[0]; - } - return null; + this.#updateMasterTemplateLayoutBlock(); + this._data.updateCurrent({ masterTemplate: unique ? { unique } : null }); + + return unique; } #updateMasterTemplateLayoutBlock = () => { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-family/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-family/manifests.ts index 9adf88c448..6e6ea6392b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-family/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-family/manifests.ts @@ -1,14 +1,4 @@ -const UMB_MENU_TIPTAP_FONT_FAMILY_ALIAS = 'Umb.Menu.Tiptap.FontFamily'; - -const menu: Array = [ - { - type: 'menu', - alias: UMB_MENU_TIPTAP_FONT_FAMILY_ALIAS, - name: 'Tiptap Font Family Menu', - }, -]; - -const toolbarExtensions: Array = [ +export const manifests: Array = [ { type: 'tiptapToolbarExtension', kind: 'menu', @@ -23,7 +13,6 @@ const toolbarExtensions: Array = [ { label: 'Cursive', appearance: { style: 'font-family: cursive;' }, data: 'cursive' }, { label: 'Fantasy', appearance: { style: 'font-family: fantasy;' }, data: 'fantasy' }, ], - menu: UMB_MENU_TIPTAP_FONT_FAMILY_ALIAS, meta: { alias: 'umbFontFamily', icon: 'icon-ruler-alt', @@ -31,5 +20,3 @@ const toolbarExtensions: Array = [ }, }, ]; - -export const manifests: Array = [...menu, ...toolbarExtensions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-size/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-size/manifests.ts index d4c3e7b0b7..52c0c21d2a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-size/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/font-size/manifests.ts @@ -1,14 +1,4 @@ -const UMB_MENU_TIPTAP_FONT_SIZE_ALIAS = 'Umb.Menu.Tiptap.FontSize'; - -const menu: Array = [ - { - type: 'menu', - alias: UMB_MENU_TIPTAP_FONT_SIZE_ALIAS, - name: 'Tiptap Font Size Menu', - }, -]; - -const toolbarExtensions: Array = [ +export const manifests: Array = [ { type: 'tiptapToolbarExtension', kind: 'menu', @@ -27,7 +17,6 @@ const toolbarExtensions: Array = [ { label: '26pt', data: '26pt' }, { label: '48pt', data: '48pt' }, ], - menu: UMB_MENU_TIPTAP_FONT_SIZE_ALIAS, meta: { alias: 'umbFontSize', icon: 'icon-ruler', @@ -35,5 +24,3 @@ const toolbarExtensions: Array = [ }, }, ]; - -export const manifests: Array = [...menu, ...toolbarExtensions]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/property-editor-ui-tiptap-statusbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/property-editor-ui-tiptap-statusbar-configuration.element.ts index 59fa606500..f5559b825b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/property-editor-ui-tiptap-statusbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/statusbar-configuration/property-editor-ui-tiptap-statusbar-configuration.element.ts @@ -310,6 +310,7 @@ export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement div { display: flex; gap: var(--uui-size-1); + align-items: flex-end; } } } @@ -369,6 +370,7 @@ export class UmbPropertyEditorUiTiptapStatusbarConfigurationElement div { display: flex; gap: var(--uui-size-1); + align-items: flex-end; } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts index e7612de8f9..bb000b6d7c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/property-editor-ui-tiptap-toolbar-configuration.element.ts @@ -364,6 +364,7 @@ export class UmbPropertyEditorUiTiptapToolbarConfigurationElement div { display: flex; gap: var(--uui-size-1); + align-items: flex-end; } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts index ff04c85202..cc7b02d45c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/toolbar-configuration/tiptap-toolbar-group-configuration.element.ts @@ -153,6 +153,7 @@ export class UmbTiptapToolbarGroupConfigurationElement< div { display: flex; gap: var(--uui-size-1); + align-items: flex-end; } uui-symbol-expand { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/types.ts index 3a6c58697a..27f3244c38 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/types.ts @@ -3,7 +3,7 @@ import type { UmbBlockRteLayoutModel } from '@umbraco-cms/backoffice/block-rte'; export type * from './extensions/types.js'; -// TODO: Rename this type: +/** @deprecated No longer used internally. This will be removed in Umbraco 17. [LK] */ export interface UmbPropertyEditorUiValueType { markup: string; blocks: UmbBlockValueType; diff --git a/src/Umbraco.Web.UI.Client/src/packages/ufm/components/ufm-render/ufm-render.element.ts b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/ufm-render/ufm-render.element.ts index f0337968d1..75b2755ccd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/ufm/components/ufm-render/ufm-render.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/ufm/components/ufm-render/ufm-render.element.ts @@ -64,7 +64,7 @@ export class UmbUfmRenderElement extends UmbLitElement { } code { - font-family: var(--uui-font-monospace); + font-family: var(--uui-font-monospace, monospace); white-space: pre-wrap; background-color: var(--uui-color-background); border-radius: var(--uui-border-radius); diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index bb1da8d0fc..08dac24f0f 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": "^2.0.37", - "@umbraco/playwright-testhelpers": "^16.0.41", + "@umbraco/playwright-testhelpers": "^16.0.42", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,9 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "16.0.41", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.41.tgz", - "integrity": "sha512-FmpL3ucj+3lZKxuL1JWyFJeU5DBL6nRsREejhQd/gUMtoJJpXhPUJ/jorqnWEwPYcvGWSBS/Inly0q+Akuf9Ow==", + "version": "16.0.42", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-16.0.42.tgz", + "integrity": "sha512-ePKl8gtELoIMEV57E3N4VumfKNkuOTFo/LYH7ePhseCcm5oUh1Cc/RVqvlXYsdfBTiMfZ7x7Nu4lOSv15D2Z3Q==", "license": "MIT", "dependencies": { "@umbraco/json-models-builders": "2.0.38", diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 43b8727f80..32076a6012 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.37", - "@umbraco/playwright-testhelpers": "^16.0.41", + "@umbraco/playwright-testhelpers": "^16.0.42", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts index f64de77c09..b16ede4a08 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockGrid/ContentWithBlockGrid.spec.ts @@ -391,7 +391,7 @@ test('can add a variant block element with invariant RTE Tiptap in the content', const customRTEDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customRTEDataTypeName); const customElementTypeId = await umbracoApi.documentType.createDefaultElementType(customElementTypeName, groupName, customRTEDataTypeName, customRTEDataTypeId); const customDataTypeId = await umbracoApi.dataType.createBlockGridWithPermissions(customDataTypeName, customElementTypeId, true, true); - const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'testGroup', true); + const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocumentWithCulture(contentName, documentTypeId, 'en-US'); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); @@ -416,4 +416,4 @@ test('can add a variant block element with invariant RTE Tiptap in the content', await umbracoApi.dataType.ensureNameNotExists(customRTEDataTypeName); await umbracoApi.documentType.ensureNameNotExists(customElementTypeName); await umbracoApi.language.ensureNameNotExists('Danish'); -}); \ No newline at end of file +}); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts index af21b576a5..7fed44f119 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/BlockList/ContentWithBlockList.spec.ts @@ -185,7 +185,7 @@ test('can add settings model for the block in the content', async ({umbracoApi, const textAreaDataTypeName = 'Textarea'; const textAreaData = await umbracoApi.dataType.getByName(textAreaDataTypeName); const settingsElementTypeId = await umbracoApi.documentType.createDefaultElementType(settingModelName, groupName, textAreaDataTypeName, textAreaData.id); - const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithContentAndSettingsElementType(customDataTypeName, elementTypeId, settingsElementTypeId, true); + const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithContentAndSettingsElementType(customDataTypeName, elementTypeId, settingsElementTypeId); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); @@ -194,6 +194,7 @@ test('can add settings model for the block in the content', async ({umbracoApi, // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickAddBlockElementButton(); + await umbracoUi.content.clickTextButtonWithName(elementTypeName); await umbracoUi.content.enterTextstring(contentBlockInputText); await umbracoUi.content.clickAddBlockSettingsTabButton(); await umbracoUi.content.enterTextArea(settingBlockInputText); @@ -266,7 +267,7 @@ test('can add an invariant block element with invariant RTE Tiptap in the conten const customElementTypeName = 'BlockListWithRTEElement'; const customRTEDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customRTEDataTypeName); const customElementTypeId = await umbracoApi.documentType.createDefaultElementType(customElementTypeName, groupName, customRTEDataTypeName, customRTEDataTypeId); - const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(customDataTypeName, customElementTypeId, true, true); + const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(customDataTypeName, customElementTypeId); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocument(contentName, documentTypeId); await umbracoUi.goToBackOffice(); @@ -300,7 +301,7 @@ test('can add a variant block element with variant RTE Tiptap in the content', a await umbracoApi.language.createDanishLanguage(); const customRTEDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customRTEDataTypeName); const customElementTypeId = await umbracoApi.documentType.createDefaultElementType(customElementTypeName, groupName, customRTEDataTypeName, customRTEDataTypeId); - const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(customDataTypeName, customElementTypeId, true, true); + const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(customDataTypeName, customElementTypeId); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'testGroup', true); await umbracoApi.document.createDefaultDocumentWithCulture(contentName, documentTypeId, 'en-US'); await umbracoUi.goToBackOffice(); @@ -336,8 +337,8 @@ test('can add a variant block element with invariant RTE Tiptap in the content', await umbracoApi.language.createDanishLanguage(); const customRTEDataTypeId = await umbracoApi.dataType.createDefaultTiptapDataType(customRTEDataTypeName); const customElementTypeId = await umbracoApi.documentType.createDefaultElementType(customElementTypeName, groupName, customRTEDataTypeName, customRTEDataTypeId); - const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(customDataTypeName, customElementTypeId, true, true); - const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId, 'testGroup', true); + const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithABlock(customDataTypeName, customElementTypeId); + const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); await umbracoApi.document.createDefaultDocumentWithCulture(contentName, documentTypeId, 'en-US'); await umbracoUi.goToBackOffice(); await umbracoUi.content.goToSection(ConstantHelper.sections.content); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts index 8a88f7feeb..61c9327620 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithMediaPicker.spec.ts @@ -175,12 +175,10 @@ test('can add a media image to a media picker in variant content, remove it and await umbracoUi.content.clickChooseButtonAndSelectMediaWithKey(mediaFileId); await umbracoUi.content.clickChooseModalButton(); await umbracoUi.content.clickSaveAndPublishButton(); - await umbracoUi.content.clickContainerSaveAndPublishButton(); await umbracoUi.content.isSuccessStateVisibleForSaveAndPublishButton(); // Removes media item from the media picker await umbracoUi.content.removeMediaPickerByName(mediaFileName); await umbracoUi.content.clickSaveAndPublishButton(); - await umbracoUi.content.clickContainerSaveAndPublishButton(); await umbracoUi.content.isSuccessStateVisibleForSaveAndPublishButton(); // Adds media item to a media picker again diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs index 66ac4278a3..4ff77c86fa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs @@ -250,7 +250,7 @@ internal sealed class DataTypeDefinitionRepositoryTest : UmbracoIntegrationTest Assert.That(dataTypeDefinitions, Is.Not.Null); Assert.That(dataTypeDefinitions.Any(), Is.True); Assert.That(dataTypeDefinitions.Any(x => x == null), Is.False); - Assert.That(dataTypeDefinitions.Length, Is.EqualTo(34)); + Assert.That(dataTypeDefinitions.Length, Is.EqualTo(36)); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactoryTests.cs index 3d7be44d69..f9f604658c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/Factories/PublishedContentFactoryTests.cs @@ -32,9 +32,9 @@ internal sealed class PublishedContentFactoryTests : UmbracoIntegrationTestWithC { var requestCache = new DictionaryAppCache(); var appCaches = new AppCaches( - NoAppCache.Instance, - requestCache, - new IsolatedCaches(type => NoAppCache.Instance)); + NoAppCache.Instance, + requestCache, + new IsolatedCaches(type => NoAppCache.Instance)); builder.Services.AddUnique(appCaches); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/PropertyValueLevelDetectionTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/PropertyValueLevelDetectionTests.cs new file mode 100644 index 0000000000..118f3758dc --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.PublishedCache.HybridCache/PropertyValueLevelDetectionTests.cs @@ -0,0 +1,156 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Infrastructure.HybridCache; +using Umbraco.Cms.Infrastructure.HybridCache.Factories; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.PublishedCache.HybridCache; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +internal sealed class PropertyValueLevelDetectionTests : UmbracoIntegrationTestWithContent +{ + private IPublishedContentFactory PublishedContentFactory => GetRequiredService(); + + private IPublishedValueFallback PublishedValueFallback => GetRequiredService(); + + private IPublishedContentTypeFactory PublishedContentTypeFactory => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + var requestCache = new DictionaryAppCache(); + var appCaches = new AppCaches( + NoAppCache.Instance, + requestCache, + new IsolatedCaches(type => NoAppCache.Instance)); + builder.Services.AddUnique(appCaches); + + builder.PropertyValueConverters() + .Remove() + .Append(); + } + + [TestCase("validSourceLevel", true)] + [TestCase("validInterLevel", true)] + [TestCase("validObjectLevel", true)] + [TestCase("invalidSourceLevel", false)] + [TestCase("invalidInterLevel", false)] + [TestCase("invalidObjectLevel", false)] + [TestCase("nullSourceLevel", false)] + [TestCase("nullInterLevel", false)] + [TestCase("nullObjectLevel", false)] + [TestCase("somethingElse", false)] + public void Can_Detect_Property_Value_At_All_Levels_For_Document(string titleValue, bool expectHasValue) + { + var contentCacheNode = new ContentCacheNode + { + Id = Textpage.Id, + Key = Textpage.Key, + ContentTypeId = Textpage.ContentType.Id, + CreateDate = Textpage.CreateDate, + CreatorId = Textpage.CreatorId, + SortOrder = Textpage.SortOrder, + Data = new ContentData( + Textpage.Name, + "text-page", + Textpage.VersionId, + Textpage.UpdateDate, + Textpage.WriterId, + Textpage.TemplateId, + true, + new Dictionary + { + { + "title", new[] + { + new PropertyData + { + Value = titleValue, + Culture = string.Empty, + Segment = string.Empty, + }, + } + }, + }, + null), + }; + var result = PublishedContentFactory.ToIPublishedContent(contentCacheNode, false); + Assert.IsNotNull(result); + Assert.AreEqual(expectHasValue, result.HasValue("title")); + + // NOTE: the .Value() extensions always end up returning the source value, no matter if the property value + // converter returns false from .HasValue()... this is the case both at property and at content level + var value = result.Value(PublishedValueFallback, "title"); + Assert.AreEqual(titleValue, value); + } + + [TestCase("validSourceLevel", true)] + [TestCase("validInterLevel", true)] + [TestCase("validObjectLevel", true)] + [TestCase("invalidSourceLevel", false)] + [TestCase("invalidInterLevel", false)] + [TestCase("invalidObjectLevel", false)] + [TestCase("nullSourceLevel", false)] + [TestCase("nullInterLevel", false)] + [TestCase("nullObjectLevel", false)] + [TestCase("somethingElse", false)] + public async Task Can_Detect_Property_Value_At_All_Levels_For_Element(string titleValue, bool expectHasValue) + { + var elementType = ContentTypeBuilder.CreateSimpleContentType("umbElement"); + elementType.IsElement = true; + await ContentTypeService.UpdateAsync(elementType, Constants.Security.SuperUserKey); + var publishedElementType = PublishedContentTypeFactory.CreateContentType(elementType); + + var element = new PublishedElement( + publishedElementType, + Guid.NewGuid(), + new Dictionary { { "title", titleValue } }, + false, + new VariationContext()); + + Assert.AreEqual(expectHasValue, element.HasValue("title")); + + // NOTE: the .Value() extensions always end up returning the source value, no matter if the property value + // converter returns false from .HasValue()... this is the case both at property and at content level + var value = element.Value(PublishedValueFallback, "title"); + Assert.AreEqual(titleValue, value); + } + + [HideFromTypeFinder] + public class PropertyValueLevelDetectionTestsConverter : PropertyValueConverterBase + { + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TextBox; + + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + => typeof(string); + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.None; + + public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) + => source as string; + + public override bool? IsValue(object? value, PropertyValueLevel level) + => level switch + { + PropertyValueLevel.Source when value?.ToString() is "validSourceLevel" => true, + PropertyValueLevel.Source when value?.ToString() is "invalidSourceLevel" => false, + PropertyValueLevel.Source when value?.ToString() is "nullSourceLevel" => null, + PropertyValueLevel.Inter when value?.ToString() is "validInterLevel" => true, + PropertyValueLevel.Inter when value?.ToString() is "invalidInterLevel" => false, + PropertyValueLevel.Source when value?.ToString() is "nullInternalLevel" => null, + PropertyValueLevel.Object when value?.ToString() is "validObjectLevel" => true, + PropertyValueLevel.Object when value?.ToString() is "invalidObjectLevel" => false, + PropertyValueLevel.Source when value?.ToString() is "nullObjectlLevel" => null, + _ => null, + }; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs index fddae4a60c..1a20ee76b2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Deploy/ArtifactBaseTests.cs @@ -12,25 +12,37 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Deploy; [TestFixture] public class ArtifactBaseTests { - [Test] - public void CanSerialize() - { - var udi = new GuidUdi("test", Guid.Parse("3382d5433b5749d08919bc9961422a1f")); - var artifact = new TestArtifact(udi, new List()) { Name = "Test Name", Alias = "testAlias" }; - - var serialized = JsonSerializer.Serialize(artifact, new JsonSerializerOptions() + private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { Converters = { new JsonUdiConverter(), } - }); + }; - var expected = - "{\"Udi\":\"umb://test/3382d5433b5749d08919bc9961422a1f\",\"Dependencies\":[],\"Checksum\":\"test checksum value\",\"Name\":\"Test Name\",\"Alias\":\"testAlias\"}"; + [Test] + public void Can_Serialize() + { + var udi = new GuidUdi("document", Guid.Parse("3382d5433b5749d08919bc9961422a1f")); + var artifact = new TestArtifact(udi, []) { Name = "Test Name", Alias = "testAlias" }; + + string serialized = JsonSerializer.Serialize(artifact, _jsonSerializerOptions); + + var expected = "{\"Udi\":\"umb://document/3382d5433b5749d08919bc9961422a1f\",\"Dependencies\":[],\"Checksum\":\"test checksum value\",\"Name\":\"Test Name\",\"Alias\":\"testAlias\"}"; Assert.AreEqual(expected, serialized); } + [Test] + public void Can_Deserialize() + { + var serialized = "{\"Udi\":\"umb://document/3382d5433b5749d08919bc9961422a1f\",\"Dependencies\":[],\"Checksum\":\"test checksum value\",\"Name\":\"Test Name\",\"Alias\":\"testAlias\"}"; + + TestArtifact? deserialized = JsonSerializer.Deserialize(serialized, _jsonSerializerOptions); + Assert.IsNotNull(deserialized); + Assert.AreEqual("Test Name", deserialized.Name); + Assert.AreEqual("testAlias", deserialized.Alias); + } + [Test] public void Dependencies_Are_Correctly_Ordered() { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonUdiConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonUdiConverterTests.cs new file mode 100644 index 0000000000..7462c8335a --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonUdiConverterTests.cs @@ -0,0 +1,67 @@ +using System.Text.Json; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Serialization; + +[TestFixture] +public class JsonUdiConverterTests +{ + private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions + { + Converters = + { + new JsonUdiConverter(), + }, + }; + + [Test] + public void Can_Serialize() + { + var udi = new GuidUdi("document", Guid.Parse("3382d5433b5749d08919bc9961422a1f")); + var artifact = new Test { Udi = udi }; + + string serialized = JsonSerializer.Serialize(artifact, _jsonSerializerOptions); + + var expected = "{\"Udi\":\"umb://document/3382d5433b5749d08919bc9961422a1f\"}"; + Assert.AreEqual(expected, serialized); + } + + [Test] + public void Can_Deserialize() + { + var serialized = "{\"Udi\":\"umb://document/3382d5433b5749d08919bc9961422a1f\"}"; + + Test? deserialized = JsonSerializer.Deserialize(serialized, _jsonSerializerOptions); + Assert.IsNotNull(deserialized); + Assert.AreEqual(Guid.Parse("3382d5433b5749d08919bc9961422a1f"), deserialized.Udi.Guid); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void Will_Deserialize_To_Null_With_Null_Or_Whitepsace_Udi(string? serializedUdi) + { + var serializedUdiPart = serializedUdi is null ? "null" : $"\"{serializedUdi}\""; + var serialized = "{\"Udi\":" + serializedUdiPart + "}"; + + Test? deserialized = JsonSerializer.Deserialize(serialized, _jsonSerializerOptions); + Assert.IsNotNull(deserialized); + Assert.IsNull(deserialized.Udi); + } + + [Test] + public void Throws_On_Invalid_Udi() + { + var serialized = "{\"Udi\":\"invalid-udi\"}"; + + Assert.Throws(() => + JsonSerializer.Deserialize(serialized, _jsonSerializerOptions)); + } + + private class Test + { + public GuidUdi? Udi { get; set; } + } +}