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}>