diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5a8717576b..131133fee2 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -86,7 +86,7 @@
-
+
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/ItemDocumentTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/ItemDocumentTypeItemController.cs
index b27efaff01..55211c7555 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/ItemDocumentTypeItemController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/ItemDocumentTypeItemController.cs
@@ -32,7 +32,7 @@ public class ItemDocumentTypeItemController : DocumentTypeItemControllerBase
return Ok(Enumerable.Empty());
}
- IEnumerable contentTypes = _contentTypeService.GetAll(ids);
+ IEnumerable contentTypes = _contentTypeService.GetMany(ids);
List responseModels = _mapper.MapEnumerable(contentTypes);
return await Task.FromResult(Ok(responseModels));
}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs
index 76121a555a..5069df0d80 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Item/SearchDocumentTypeItemController.cs
@@ -35,7 +35,7 @@ public class SearchDocumentTypeItemController : DocumentTypeItemControllerBase
return await Task.FromResult(Ok(new PagedModel { Total = searchResult.Total }));
}
- IEnumerable contentTypes = _contentTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
+ IEnumerable contentTypes = _contentTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
var result = new PagedModel
{
Items = _mapper.MapEnumerable(contentTypes),
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs
index 91c7af1c2e..0a54417200 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs
@@ -29,7 +29,7 @@ public class DocumentTypeTreeControllerBase : FolderTreeControllerBase entity.Id).ToArray())
+ .GetMany(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/ItemMediaTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/ItemMediaTypeItemController.cs
index d38ed7539d..14bcff0e51 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/ItemMediaTypeItemController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/ItemMediaTypeItemController.cs
@@ -32,7 +32,7 @@ public class ItemMediaTypeItemController : MediaTypeItemControllerBase
return Ok(Enumerable.Empty());
}
- IEnumerable mediaTypes = _mediaTypeService.GetAll(ids);
+ IEnumerable mediaTypes = _mediaTypeService.GetMany(ids);
List responseModels = _mapper.MapEnumerable(mediaTypes);
return await Task.FromResult(Ok(responseModels));
}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/SearchMediaTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/SearchMediaTypeItemController.cs
index 80ed9f55d1..475087766f 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/SearchMediaTypeItemController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Item/SearchMediaTypeItemController.cs
@@ -35,7 +35,7 @@ public class SearchMediaTypeItemController : MediaTypeItemControllerBase
return await Task.FromResult(Ok(new PagedModel { Total = searchResult.Total }));
}
- IEnumerable mediaTypes = _mediaTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
+ IEnumerable mediaTypes = _mediaTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray().EmptyNull());
var result = new PagedModel
{
Items = _mapper.MapEnumerable(mediaTypes),
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs
index 641ca09245..a3cc202b28 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs
@@ -30,7 +30,7 @@ public class MediaTypeTreeControllerBase : FolderTreeControllerBase entity.Id).ToArray())
+ .GetMany(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/ItemMemberTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/ItemMemberTypeItemController.cs
index 379f2eea7f..c110e9ef6b 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/ItemMemberTypeItemController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/ItemMemberTypeItemController.cs
@@ -32,7 +32,7 @@ public class ItemMemberTypeItemController : MemberTypeItemControllerBase
return Ok(Enumerable.Empty());
}
- IEnumerable memberTypes = _memberTypeService.GetAll(ids);
+ IEnumerable memberTypes = _memberTypeService.GetMany(ids);
List responseModels = _mapper.MapEnumerable(memberTypes);
return Ok(responseModels);
}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/SearchMemberTypeItemController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/SearchMemberTypeItemController.cs
index e60283834d..6e8151d8a2 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/SearchMemberTypeItemController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Item/SearchMemberTypeItemController.cs
@@ -34,7 +34,7 @@ public class SearchMemberTypeItemController : MemberTypeItemControllerBase
return await Task.FromResult(Ok(new PagedModel { Total = searchResult.Total }));
}
- IEnumerable memberTypes = _memberTypeService.GetAll(searchResult.Items.Select(item => item.Key).ToArray());
+ IEnumerable memberTypes = _memberTypeService.GetMany(searchResult.Items.Select(item => item.Key).ToArray());
var result = new PagedModel
{
Items = _mapper.MapEnumerable(memberTypes),
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs
index d2ef0cf0e4..4a9211de2c 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs
@@ -27,7 +27,7 @@ public class MemberTypeTreeControllerBase : NamedEntityTreeControllerBase entity.Id).ToArray())
+ .GetMany(entities.Select(entity => entity.Id).ToArray())
.ToDictionary(contentType => contentType.Id);
return entities.Select(entity =>
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs
index 316dbdf51d..c7aa2189c1 100644
--- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs
@@ -60,7 +60,7 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase
timer.Stop();
var contentTypeIconsByKey = _contentTypeService
- .GetAll(results.Select(content => content.ContentType.Key).Distinct())
+ .GetMany(results.Select(content => content.ContentType.Key).Distinct())
.ToDictionary(contentType => contentType.Key, contentType => contentType.Icon);
return await Task.FromResult(Ok(new TemplateQueryResultResponseModel
diff --git a/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs
index 25382c7209..79f1c22b3d 100644
--- a/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs
+++ b/src/Umbraco.Cms.Api.Management/Factories/DataTypeReferencePresentationFactory.cs
@@ -26,9 +26,9 @@ public class DataTypeReferencePresentationFactory : IDataTypeReferencePresentati
{
var getContentTypesByObjectType = new Dictionary, IEnumerable>>
{
- { UmbracoObjectTypes.DocumentType.GetUdiType(), keys => _contentTypeService.GetAll(keys) },
- { UmbracoObjectTypes.MediaType.GetUdiType(), keys => _mediaTypeService.GetAll(keys) },
- { UmbracoObjectTypes.MemberType.GetUdiType(), keys => _memberTypeService.GetAll(keys) }
+ { UmbracoObjectTypes.DocumentType.GetUdiType(), keys => _contentTypeService.GetMany(keys) },
+ { UmbracoObjectTypes.MediaType.GetUdiType(), keys => _mediaTypeService.GetMany(keys) },
+ { UmbracoObjectTypes.MemberType.GetUdiType(), keys => _memberTypeService.GetMany(keys) }
};
foreach (IGrouping>> usagesByEntityType in dataTypeUsages.GroupBy(u => u.Key.EntityType))
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
index 3b3eb8560e..1ff1b21277 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs
@@ -1,5 +1,3 @@
-using Microsoft.Extensions.DependencyInjection;
-using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
@@ -22,6 +20,7 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase _documentNavigationQueryService.TryGetParentKeyInBin(contentKey, out _);
+ private async Task HandlePublishedAsync(JsonPayload payload, CancellationToken cancellationToken)
+ {
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll))
+ {
+ await _publishStatusManagementService.InitializeAsync(cancellationToken);
+ }
+
+ if (payload.Key.HasValue is false)
+ {
+ return;
+ }
+
+ if (payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
+ {
+ await _publishStatusManagementService.RemoveAsync(payload.Key.Value, cancellationToken);
+ }
+ else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
+ {
+ await _publishStatusManagementService.AddOrUpdateStatusAsync(payload.Key.Value, cancellationToken);
+ }
+ else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshBranch))
+ {
+ await _publishStatusManagementService.AddOrUpdateStatusWithDescendantsAsync(payload.Key.Value, cancellationToken);
+ }
+ }
private void HandleRouting(JsonPayload payload)
{
if(payload.ChangeTypes.HasType(TreeChangeTypes.Remove))
diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
index f891443a9f..581e77c52a 100644
--- a/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/Implement/MediaCacheRefresher.cs
@@ -2,7 +2,6 @@ using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Persistence.Repositories;
-using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.Changes;
@@ -169,7 +168,7 @@ public sealed class MediaCacheRefresher : PayloadCacheRefresherBase(x => x.GetRequiredService());
Services.AddUnique(x => x.GetRequiredService());
+ Services.AddUnique();
+ Services.AddUnique(x => x.GetRequiredService());
+ Services.AddUnique(x => x.GetRequiredService());
+
// Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced
Services.AddUnique();
Services.AddUnique();
diff --git a/src/Umbraco.Core/Handlers/WarnDocumentTypeElementSwitchNotificationHandler.cs b/src/Umbraco.Core/Handlers/WarnDocumentTypeElementSwitchNotificationHandler.cs
index b4c13f8da3..d7de162315 100644
--- a/src/Umbraco.Core/Handlers/WarnDocumentTypeElementSwitchNotificationHandler.cs
+++ b/src/Umbraco.Core/Handlers/WarnDocumentTypeElementSwitchNotificationHandler.cs
@@ -35,7 +35,7 @@ public class WarnDocumentTypeElementSwitchNotificationHandler :
.Where(e => e.HasIdentity)
.Select(e => e.Key);
- IEnumerable persistedItems = _contentTypeService.GetAll(updatedKeys);
+ IEnumerable persistedItems = _contentTypeService.GetMany(updatedKeys);
var stateInformation = persistedItems
.ToDictionary(
diff --git a/src/Umbraco.Core/Models/INavigationModel.cs b/src/Umbraco.Core/Models/INavigationModel.cs
index bc33e22f0f..9663419627 100644
--- a/src/Umbraco.Core/Models/INavigationModel.cs
+++ b/src/Umbraco.Core/Models/INavigationModel.cs
@@ -17,6 +17,11 @@ public interface INavigationModel
///
int ParentId { get; set; }
+ ///
+ /// Gets or sets the sort order of the entity.
+ ///
+ int SortOrder { get; set; }
+
///
/// Gets or sets a value indicating whether this entity is in the recycle bin.
///
diff --git a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs
index 9edf00d6fb..5e8e412116 100644
--- a/src/Umbraco.Core/Models/Navigation/NavigationNode.cs
+++ b/src/Umbraco.Core/Models/Navigation/NavigationNode.cs
@@ -1,30 +1,57 @@
+using System.Collections.Concurrent;
+
namespace Umbraco.Cms.Core.Models.Navigation;
public sealed class NavigationNode
{
- private List _children;
+ private HashSet _children;
public Guid Key { get; private set; }
- public NavigationNode? Parent { get; private set; }
+ public int SortOrder { get; private set; }
- public IEnumerable Children => _children.AsEnumerable();
+ public Guid? Parent { get; private set; }
- public NavigationNode(Guid key)
+ public ISet Children => _children;
+
+ public NavigationNode(Guid key, int sortOrder = 0)
{
Key = key;
- _children = new List();
+ SortOrder = sortOrder;
+ _children = new HashSet();
}
- public void AddChild(NavigationNode child)
+ public void UpdateSortOrder(int newSortOrder) => SortOrder = newSortOrder;
+
+ public void AddChild(ConcurrentDictionary navigationStructure, Guid childKey)
{
- child.Parent = this;
- _children.Add(child);
+ if (navigationStructure.TryGetValue(childKey, out NavigationNode? child) is false)
+ {
+ throw new KeyNotFoundException($"Item with key '{childKey}' was not found in the navigation structure.");
+ }
+
+ child.Parent = Key;
+
+ // Add it as the last item
+ child.SortOrder = _children.Count;
+
+ _children.Add(childKey);
+
+ // Update the navigation structure
+ navigationStructure[childKey] = child;
}
- public void RemoveChild(NavigationNode child)
+ public void RemoveChild(ConcurrentDictionary navigationStructure, Guid childKey)
{
- _children.Remove(child);
+ if (navigationStructure.TryGetValue(childKey, out NavigationNode? child) is false)
+ {
+ throw new KeyNotFoundException($"Item with key '{childKey}' was not found in the navigation structure.");
+ }
+
+ _children.Remove(childKey);
child.Parent = null;
+
+ // Update the navigation structure
+ navigationStructure[childKey] = child;
}
}
diff --git a/src/Umbraco.Core/MonitorLock.cs b/src/Umbraco.Core/MonitorLock.cs
index 45dbdbbd10..251f8a2812 100644
--- a/src/Umbraco.Core/MonitorLock.cs
+++ b/src/Umbraco.Core/MonitorLock.cs
@@ -4,6 +4,7 @@ namespace Umbraco.Cms.Core;
/// Provides an equivalent to the c# lock statement, to be used in a using block.
///
/// Ie replace lock (o) {...} by using (new MonitorLock(o)) { ... }
+[Obsolete("Use System.Threading.Lock instead. This will be removed in Umbraco 16")]
public class MonitorLock : IDisposable
{
private readonly bool _entered;
diff --git a/src/Umbraco.Core/Persistence/Repositories/IPublishStatusRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IPublishStatusRepository.cs
new file mode 100644
index 0000000000..3176b02a36
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Repositories/IPublishStatusRepository.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Cms.Core.Persistence.Repositories;
+
+public interface IPublishStatusRepository
+{
+ Task>> GetAllPublishStatusAsync(CancellationToken cancellationToken);
+ Task> GetPublishStatusAsync(Guid documentKey, CancellationToken cancellationToken);
+ Task>> GetDescendantsOrSelfPublishStatusAsync(Guid rootDocumentKey, CancellationToken cancellationToken);
+}
diff --git a/src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs b/src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs
index 4eac53dc03..f6fd19eafc 100644
--- a/src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs
+++ b/src/Umbraco.Core/PublishedCache/IDatabaseCacheRebuilder.cs
@@ -3,4 +3,6 @@
public interface IDatabaseCacheRebuilder
{
void Rebuild();
+
+ void RebuildDatabaseCacheIfSerializerChanged();
}
diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
index eeaaeef9b9..76211530aa 100644
--- a/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
+++ b/src/Umbraco.Core/Routing/ContentFinderByUrlNew.cs
@@ -96,6 +96,7 @@ public class ContentFinderByUrlNew : IContentFinder
umbracoContext.InPreviewMode
);
+
IPublishedContent? node = null;
if (documentKey.HasValue)
{
diff --git a/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs b/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs
index cc597b587f..bf02fa6759 100644
--- a/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs
+++ b/src/Umbraco.Core/Services/ContentTypeEditing/ElementSwitchValidator.cs
@@ -31,7 +31,7 @@ public class ElementSwitchValidator : IElementSwitchValidator
}
// if there are any ancestors where IsElement is different from the contentType, the validation fails
- return await Task.FromResult(_contentTypeService.GetAll(ancestorIds)
+ return await Task.FromResult(_contentTypeService.GetMany(ancestorIds)
.Any(ancestor => ancestor.IsElement != contentType.IsElement) is false);
}
diff --git a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
index 5d91c29b27..9e17a372a9 100644
--- a/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Core/Services/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -339,16 +339,28 @@ public abstract class ContentTypeServiceBase : ContentTypeSe
///
public Task GetAsync(Guid guid) => Task.FromResult(Get(guid));
- public IEnumerable GetAll(params int[] ids)
+ public IEnumerable GetAll()
{
+ using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
+ scope.ReadLock(ReadLockIds);
+ return Repository.GetMany(Array.Empty());
+ }
+
+ public IEnumerable GetMany(params int[] ids)
+ {
+ if (ids.Any() is false)
+ {
+ return Enumerable.Empty();
+ }
+
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
scope.ReadLock(ReadLockIds);
return Repository.GetMany(ids);
}
- public IEnumerable GetAll(IEnumerable? ids)
+ public IEnumerable GetMany(IEnumerable? ids)
{
- if (ids is null)
+ if (ids is null || ids.Any() is false)
{
return Enumerable.Empty();
}
@@ -471,7 +483,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe
{
// GetAll is cheap, repository has a full dataset cache policy
// TODO: still, because it uses the cache, race conditions!
- IEnumerable allContentTypes = GetAll(Array.Empty());
+ IEnumerable allContentTypes = GetAll();
return GetComposedOf(id, allContentTypes);
}
@@ -1149,7 +1161,7 @@ public abstract class ContentTypeServiceBase : ContentTypeSe
}
else
{
- TItem[] allowedChildren = GetAll(parent.AllowedContentTypes.Select(x => x.Key)).ToArray();
+ TItem[] allowedChildren = GetMany(parent.AllowedContentTypes.Select(x => x.Key)).ToArray();
result = new PagedModel
{
Items = allowedChildren.Take(take).Skip(skip),
diff --git a/src/Umbraco.Core/Services/DocumentUrlService.cs b/src/Umbraco.Core/Services/DocumentUrlService.cs
index a80a169a68..16df42f894 100644
--- a/src/Umbraco.Core/Services/DocumentUrlService.cs
+++ b/src/Umbraco.Core/Services/DocumentUrlService.cs
@@ -1,13 +1,11 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.Runtime.CompilerServices;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
-using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.Navigation;
@@ -33,9 +31,10 @@ public class DocumentUrlService : IDocumentUrlService
private readonly IIdKeyMap _idKeyMap;
private readonly IDocumentNavigationQueryService _documentNavigationQueryService;
private readonly IDomainService _domainService;
+ private readonly IPublishStatusQueryService _publishStatusQueryService;
private readonly ConcurrentDictionary _cache = new();
- private bool _isInitialized = false;
+ private bool _isInitialized;
public DocumentUrlService(
ILogger logger,
@@ -50,7 +49,8 @@ public class DocumentUrlService : IDocumentUrlService
IKeyValueService keyValueService,
IIdKeyMap idKeyMap,
IDocumentNavigationQueryService documentNavigationQueryService,
- IDomainService domainService)
+ IDomainService domainService,
+ IPublishStatusQueryService publishStatusQueryService)
{
_logger = logger;
_documentUrlRepository = documentUrlRepository;
@@ -65,6 +65,7 @@ public class DocumentUrlService : IDocumentUrlService
_idKeyMap = idKeyMap;
_documentNavigationQueryService = documentNavigationQueryService;
_domainService = domainService;
+ _publishStatusQueryService = publishStatusQueryService;
}
public async Task InitAsync(bool forceEmpty, CancellationToken cancellationToken)
@@ -109,8 +110,7 @@ public class DocumentUrlService : IDocumentUrlService
scopeContext.Enlist("UpdateCache_" + cacheKey, () =>
{
- PublishedDocumentUrlSegment? existingValue = null;
- _cache.TryGetValue(cacheKey, out existingValue);
+ _cache.TryGetValue(cacheKey, out PublishedDocumentUrlSegment? existingValue);
if (existingValue is null)
{
@@ -135,13 +135,13 @@ public class DocumentUrlService : IDocumentUrlService
}
- private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode)
+ private void RemoveFromCache(IScopeContext scopeContext, Guid documentKey, string isoCode, bool isDraft)
{
- var cacheKeyDraft = CreateCacheKey(documentKey, isoCode, true);
+ var cacheKey = CreateCacheKey(documentKey, isoCode, isDraft);
- scopeContext.Enlist("RemoveFromCache_" + cacheKeyDraft, () =>
+ scopeContext.Enlist("RemoveFromCache_" + cacheKey, () =>
{
- if (_cache.TryRemove(cacheKeyDraft, out _) is false)
+ if (_cache.TryRemove(cacheKey, out _) is false)
{
_logger.LogDebug("Could not remove the document url cache. But the important thing is that it is not there.");
return false;
@@ -149,20 +149,6 @@ public class DocumentUrlService : IDocumentUrlService
return true;
});
-
- var cacheKeyPublished = CreateCacheKey(documentKey, isoCode, false);
-
- scopeContext.Enlist("RemoveFromCache_" + cacheKeyPublished, () =>
- {
- if (_cache.TryRemove(cacheKeyPublished, out _) is false)
- {
- _logger.LogDebug("Could not remove the document url cache. But the important thing is that it is not there.");
- return false;
- }
-
- return true;
- });
-
}
public async Task RebuildAllUrlsAsync()
@@ -179,7 +165,7 @@ public class DocumentUrlService : IDocumentUrlService
scope.Complete();
}
- public Task ShouldRebuildUrlsAsync()
+ private Task ShouldRebuildUrlsAsync()
{
var persistedValue = GetPersistedRebuildValue();
var currentValue = GetCurrentRebuildValue();
@@ -187,10 +173,7 @@ public class DocumentUrlService : IDocumentUrlService
return Task.FromResult(string.Equals(persistedValue, currentValue) is false);
}
- private string GetCurrentRebuildValue()
- {
- return string.Join("|", _urlSegmentProviderCollection.Select(x => x.GetType().Name));
- }
+ private string GetCurrentRebuildValue() => string.Join("|", _urlSegmentProviderCollection.Select(x => x.GetType().Name));
private string? GetPersistedRebuildValue() => _keyValueService.GetValue(RebuildKey);
@@ -212,8 +195,9 @@ public class DocumentUrlService : IDocumentUrlService
}
}
- public async Task CreateOrUpdateUrlSegmentsAsync(IEnumerable documents)
+ public async Task CreateOrUpdateUrlSegmentsAsync(IEnumerable documentsEnumerable)
{
+ IEnumerable documents = documentsEnumerable as IContent[] ?? documentsEnumerable.ToArray();
if(documents.Any() is false)
{
return;
@@ -222,10 +206,8 @@ public class DocumentUrlService : IDocumentUrlService
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
var toSave = new List();
- var toDelete = new List();
- var allCultures = documents.SelectMany(x => x.AvailableCultures ).Distinct();
- var languages = await _languageService.GetAllAsync();
+ IEnumerable languages = await _languageService.GetAllAsync();
var languageDictionary = languages.ToDictionary(x=>x.IsoCode);
foreach (IContent document in documents)
@@ -237,7 +219,7 @@ public class DocumentUrlService : IDocumentUrlService
foreach ((string culture, ILanguage language) in languageDictionary)
{
- HandleCaching(_coreScopeProvider.Context!, document, document.ContentType.VariesByCulture() ? culture : null, language, toDelete, toSave);
+ HandleCaching(_coreScopeProvider.Context!, document, document.ContentType.VariesByCulture() ? culture : null, language, toSave);
}
}
@@ -246,29 +228,18 @@ public class DocumentUrlService : IDocumentUrlService
_documentUrlRepository.Save(toSave);
}
- if(toDelete.Any())
- {
- _documentUrlRepository.DeleteByDocumentKey(toDelete);
- }
-
scope.Complete();
}
- private void HandleCaching(IScopeContext scopeContext, IContent document, string? culture, ILanguage language, List toDelete, List toSave)
+ private void HandleCaching(IScopeContext scopeContext, IContent document, string? culture, ILanguage language, List toSave)
{
- var models = GenerateModels(document, culture, language);
+ IEnumerable<(PublishedDocumentUrlSegment model, bool shouldCache)> modelsAndStatus = GenerateModels(document, culture, language);
- foreach (PublishedDocumentUrlSegment model in models)
+ foreach ((PublishedDocumentUrlSegment model, bool shouldCache) in modelsAndStatus)
{
- if (document.Published is false && model.IsDraft is false)
+ if (shouldCache is false)
{
- continue;
- }
-
- if (document.Trashed)
- {
- toDelete.Add(model.DocumentKey);
- RemoveFromCache(scopeContext, model.DocumentKey, language.IsoCode);
+ RemoveFromCache(scopeContext, model.DocumentKey, language.IsoCode, model.IsDraft);
}
else
{
@@ -278,27 +249,38 @@ public class DocumentUrlService : IDocumentUrlService
}
}
- private IEnumerable GenerateModels(IContent document, string? culture, ILanguage language)
+ private IEnumerable<(PublishedDocumentUrlSegment model, bool shouldCache)> GenerateModels(IContent document, string? culture, ILanguage language)
{
- if (document.ContentType.VariesByCulture() is false || document.PublishCultureInfos != null && document.PublishCultureInfos.Values.Any(x => x.Culture == culture))
+ if (document.Trashed is false
+ && (IsInvariantAndPublished(document) || IsVariantAndPublishedForCulture(document, culture)))
{
-
- var publishedUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, true);
+ var publishedUrlSegment =
+ document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture);
if (publishedUrlSegment.IsNullOrWhiteSpace())
{
_logger.LogWarning("No published url segment found for document {DocumentKey} in culture {Culture}", document.Key, culture ?? "{null}");
}
else
{
- yield return new PublishedDocumentUrlSegment()
+ yield return (new PublishedDocumentUrlSegment()
{
DocumentKey = document.Key,
LanguageId = language.Id,
UrlSegment = publishedUrlSegment,
IsDraft = false
- };
+ }, true);
}
}
+ else
+ {
+ yield return (new PublishedDocumentUrlSegment()
+ {
+ DocumentKey = document.Key,
+ LanguageId = language.Id,
+ UrlSegment = string.Empty,
+ IsDraft = false
+ }, false);
+ }
var draftUrlSegment = document.GetUrlSegment(_shortStringHelper, _urlSegmentProviderCollection, culture, false);
@@ -308,24 +290,36 @@ public class DocumentUrlService : IDocumentUrlService
}
else
{
- yield return new PublishedDocumentUrlSegment()
+ yield return (new PublishedDocumentUrlSegment()
{
DocumentKey = document.Key, LanguageId = language.Id, UrlSegment = draftUrlSegment, IsDraft = true
- };
+ }, document.Trashed is false);
}
}
- public async Task DeleteUrlsFromCacheAsync(IEnumerable documentKeys)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsVariantAndPublishedForCulture(IContent document, string? culture) =>
+ document.PublishCultureInfos?.Values.Any(x => x.Culture == culture) ?? false;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static bool IsInvariantAndPublished(IContent document)
+ => document.ContentType.VariesByCulture() is false // Is Invariant
+ && document.Published; // Is Published
+
+ public async Task DeleteUrlsFromCacheAsync(IEnumerable documentKeysEnumerable)
{
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
IEnumerable languages = await _languageService.GetAllAsync();
+ IEnumerable documentKeys = documentKeysEnumerable as Guid[] ?? documentKeysEnumerable.ToArray();
+
foreach (ILanguage language in languages)
{
foreach (Guid documentKey in documentKeys)
{
- RemoveFromCache(_coreScopeProvider.Context!, documentKey, language.IsoCode);
+ RemoveFromCache(_coreScopeProvider.Context!, documentKey, language.IsoCode, true);
+ RemoveFromCache(_coreScopeProvider.Context!, documentKey, language.IsoCode, false);
}
}
@@ -351,6 +345,12 @@ public class DocumentUrlService : IDocumentUrlService
// If a domain is assigned to this route, we need to follow the url segments
if (runnerKey.HasValue)
{
+ // if the domain node is unpublished, we need to return null.
+ if (isDraft is false && IsContentPublished(runnerKey.Value, culture) is false)
+ {
+ return null;
+ }
+
// If there is no url segments it means the domain root has been requested
if (urlSegments.Length == 0)
{
@@ -361,14 +361,20 @@ public class DocumentUrlService : IDocumentUrlService
foreach (var urlSegment in urlSegments)
{
//Get the children of the runnerKey and find the child (if any) with the correct url segment
- var childKeys = GetChildKeys(runnerKey.Value);
+ IEnumerable childKeys = GetChildKeys(runnerKey.Value);
runnerKey = GetChildWithUrlSegment(childKeys, urlSegment, culture, isDraft);
+
if (runnerKey is null)
{
break;
}
+ //if part of the path is unpublished, we need to break
+ if (isDraft is false && IsContentPublished(runnerKey.Value, culture) is false)
+ {
+ return null;
+ }
}
return runnerKey;
@@ -376,14 +382,14 @@ public class DocumentUrlService : IDocumentUrlService
// If there is no parts, it means it is a root (and no assigned domain)
if(urlSegments.Length == 0)
{
- // // if we do not hide the top level and no domain was found, it maens there is no content.
+ // // if we do not hide the top level and no domain was found, it mean there is no content.
// // TODO we can remove this to keep consistency with the old routing, but it seems incorrect to allow that.
// if (hideTopLevelNodeFromPath is false)
// {
// return null;
// }
- return GetTopMostRootKey();
+ return GetTopMostRootKey(isDraft, culture);
}
// Otherwise we have to find the root items (or child of the first root when hideTopLevelNodeFromPath is true) and follow the url segments in them to get to correct document key
@@ -393,7 +399,7 @@ public class DocumentUrlService : IDocumentUrlService
IEnumerable runnerKeys;
if (index == 0)
{
- runnerKeys = GetKeysInRoot(hideTopLevelNodeFromPath);
+ runnerKeys = GetKeysInRoot(hideTopLevelNodeFromPath, isDraft, culture);
}
else
{
@@ -408,12 +414,19 @@ public class DocumentUrlService : IDocumentUrlService
runnerKey = GetChildWithUrlSegment(runnerKeys, urlSegment, culture, isDraft);
}
+ if (isDraft is false && runnerKey.HasValue && IsContentPublished(runnerKey.Value, culture) is false)
+ {
+ return null;
+ }
+
return runnerKey;
}
+ private bool IsContentPublished(Guid contentKey, string culture) => _publishStatusQueryService.IsDocumentPublished(contentKey, culture);
+
public string GetLegacyRouteFormat(Guid docuemntKey, string? culture, bool isDraft)
{
- var documentIdAttempt = _idKeyMap.GetIdForKey(docuemntKey, UmbracoObjectTypes.Document);
+ Attempt documentIdAttempt = _idKeyMap.GetIdForKey(docuemntKey, UmbracoObjectTypes.Document);
if(documentIdAttempt.Success is false)
{
@@ -466,7 +479,7 @@ public class DocumentUrlService : IDocumentUrlService
return foundDomain.RootContentId + "/" + string.Join("/", urlSegments);
}
- var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last();
+ var isRootFirstItem = GetTopMostRootKey(isDraft, cultureOrDefault) == ancestorsOrSelfKeysArray.Last();
return GetFullUrl(isRootFirstItem, urlSegments, null);
}
@@ -481,7 +494,7 @@ public class DocumentUrlService : IDocumentUrlService
{
var result = new List();
- var documentIdAttempt = _idKeyMap.GetIdForKey(contentKey, UmbracoObjectTypes.Document);
+ Attempt documentIdAttempt = _idKeyMap.GetIdForKey(contentKey, UmbracoObjectTypes.Document);
if(documentIdAttempt.Success is false)
{
@@ -511,7 +524,7 @@ public class DocumentUrlService : IDocumentUrlService
{
if (ancestorOrSelfKeyToDomains.TryGetValue(ancestorOrSelfKey, out Task>? domainDictionaryTask))
{
- var domainDictionary = await domainDictionaryTask;
+ Dictionary domainDictionary = await domainDictionaryTask;
if (domainDictionary.TryGetValue(culture, out IDomain? domain))
{
foundDomain = domain;
@@ -527,7 +540,7 @@ public class DocumentUrlService : IDocumentUrlService
{
hasUrlInCulture = false;
}
- }
+ }
//If we did not find a domain and this is not the default language, then the content is not routable
if (foundDomain is null && language.IsDefault is false)
@@ -535,12 +548,12 @@ public class DocumentUrlService : IDocumentUrlService
continue;
}
- var isRootFirstItem = GetTopMostRootKey() == ancestorsOrSelfKeysArray.Last();
- result.Add(new UrlInfo(
- text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain),
- isUrl: hasUrlInCulture,
- culture: culture
- ));
+ var isRootFirstItem = GetTopMostRootKey(false, culture) == ancestorsOrSelfKeysArray.Last();
+ result.Add(new UrlInfo(
+ text: GetFullUrl(isRootFirstItem, urlSegments, foundDomain),
+ isUrl: hasUrlInCulture,
+ culture: culture
+ ));
}
@@ -582,24 +595,14 @@ public class DocumentUrlService : IDocumentUrlService
}
}
-
- //TODO test cases:
- // - Find the root, when a domain is set
- // - Find a nested child, when a domain is set
-
- // - Find the root when no domain is set and hideTopLevelNodeFromPath is true
- // - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is true
- // - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is true
- // - Find the root when no domain is set and hideTopLevelNodeFromPath is false
- // - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is false
- // - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is false
-
- // - All of the above when having Constants.Conventions.Content.UrlName set to a value
-
- private IEnumerable GetKeysInRoot(bool addFirstLevelChildren)
+ private IEnumerable GetKeysInRoot(bool addFirstLevelChildren, bool isDraft, string culture)
{
- //TODO replace with something more performand - Should be possible with navigationservice..
- IEnumerable rootKeys = _contentService.GetRootContent().Select(x=>x.Key).ToArray();
+ if (_documentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeysEnumerable) is false)
+ {
+ yield break;
+ }
+
+ IEnumerable rootKeys = rootKeysEnumerable as Guid[] ?? rootKeysEnumerable.ToArray();
foreach (Guid rootKey in rootKeys)
{
@@ -610,6 +613,11 @@ public class DocumentUrlService : IDocumentUrlService
{
foreach (Guid rootKey in rootKeys)
{
+ if (isDraft is false && IsContentPublished(rootKey, culture) is false)
+ {
+ continue;
+ }
+
IEnumerable childKeys = GetChildKeys(rootKey);
foreach (Guid childKey in childKeys)
@@ -655,17 +663,23 @@ public class DocumentUrlService : IDocumentUrlService
/// Gets the top most root key.
///
/// The top most root key.
- private Guid? GetTopMostRootKey()
+ private Guid? GetTopMostRootKey(bool isDraft, string culture)
{
if (_documentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys))
{
- return rootKeys.FirstOrDefault();
+ foreach (Guid rootKey in rootKeys)
+ {
+ if (isDraft || IsContentPublished(rootKey, culture))
+ {
+ return rootKey;
+ }
+ }
}
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static string CreateCacheKey(Guid documentKey, string culture, bool isDraft) => $"{documentKey}|{culture}|{isDraft}";
+ private static string CreateCacheKey(Guid documentKey, string culture, bool isDraft) => $"{documentKey}|{culture}|{isDraft}".ToLowerInvariant();
private Guid? GetStartNodeKey(int? documentStartNodeId)
{
diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
index bee084fbf9..e8b2f044ce 100644
--- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
+++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs
@@ -51,9 +51,10 @@ public interface IContentTypeBaseService : IContentTypeBaseService, IServ
///
bool HasContentNodes(int id);
- IEnumerable GetAll(params int[] ids);
+ IEnumerable GetAll();
+ IEnumerable GetMany(params int[] ids);
- IEnumerable GetAll(IEnumerable? ids);
+ IEnumerable GetMany(IEnumerable? ids);
IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis
diff --git a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
index a2cd8ea354..10b2b6ba1c 100644
--- a/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
+++ b/src/Umbraco.Core/Services/Navigation/ContentNavigationServiceBase.cs
@@ -1,5 +1,4 @@
using System.Collections.Concurrent;
-using Umbraco.Cms.Core.Factories;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Navigation;
using Umbraco.Cms.Core.Persistence.Repositories;
@@ -97,11 +96,13 @@ internal abstract class ContentNavigationServiceBase
// Recursively remove all descendants and add them to recycle bin
AddDescendantsToRecycleBinRecursively(nodeToRemove);
+ // Reset the SortOrder based on its new position in the bin
+ nodeToRemove.UpdateSortOrder(_recycleBinNavigationStructure.Count);
return _recycleBinNavigationStructure.TryAdd(nodeToRemove.Key, nodeToRemove) &&
_navigationStructure.TryRemove(key, out _);
}
- public bool Add(Guid key, Guid? parentKey = null)
+ public bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null)
{
NavigationNode? parentNode = null;
if (parentKey.HasValue)
@@ -116,13 +117,14 @@ internal abstract class ContentNavigationServiceBase
_roots.Add(key);
}
- var newNode = new NavigationNode(key);
+ // Note: sortOrder can't be automatically determined for items at root level, so it needs to be passed in
+ var newNode = new NavigationNode(key, sortOrder ?? 0);
if (_navigationStructure.TryAdd(key, newNode) is false)
{
return false; // Node with this key already exists
}
- parentNode?.AddChild(newNode);
+ parentNode?.AddChild(_navigationStructure, key);
return true;
}
@@ -155,13 +157,25 @@ internal abstract class ContentNavigationServiceBase
}
// Remove the node from its current parent's children list
- if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Key, out var currentParentNode))
+ if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Value, out NavigationNode? currentParentNode))
{
- currentParentNode.RemoveChild(nodeToMove);
+ currentParentNode.RemoveChild(_navigationStructure, key);
}
// Set the new parent for the node (if parent node is null - the node is moved to root)
- targetParentNode?.AddChild(nodeToMove);
+ targetParentNode?.AddChild(_navigationStructure, key);
+
+ return true;
+ }
+
+ public bool UpdateSortOrder(Guid key, int newSortOrder)
+ {
+ if (_navigationStructure.TryGetValue(key, out NavigationNode? node) is false)
+ {
+ return false; // Node doesn't exist
+ }
+
+ node.UpdateSortOrder(newSortOrder);
return true;
}
@@ -195,8 +209,7 @@ internal abstract class ContentNavigationServiceBase
}
// Set the new parent for the node (if parent node is null - the node is moved to root)
- targetParentNode?.AddChild(nodeToRestore);
-
+ targetParentNode?.AddChild(_recycleBinNavigationStructure, key);
// Restore the node and its descendants from the recycle bin to the main structure
RestoreNodeAndDescendantsRecursively(nodeToRestore);
@@ -240,7 +253,7 @@ internal abstract class ContentNavigationServiceBase
{
if (structure.TryGetValue(childKey, out NavigationNode? childNode))
{
- parentKey = childNode.Parent?.Key;
+ parentKey = childNode.Parent;
return true;
}
@@ -252,7 +265,10 @@ internal abstract class ContentNavigationServiceBase
private bool TryGetRootKeysFromStructure(IList input, out IEnumerable rootKeys)
{
// TODO can we make this more efficient?
- rootKeys = input.ToArray();
+ // Sort by SortOrder
+ rootKeys = input
+ .OrderBy(key => _navigationStructure[key].SortOrder)
+ .ToList();
return true;
}
@@ -266,7 +282,9 @@ internal abstract class ContentNavigationServiceBase
return false;
}
- childrenKeys = parentNode.Children.Select(child => child.Key);
+ // Keep children keys ordered based on their SortOrder
+ childrenKeys = GetOrderedChildren(parentNode, structure).ToList();
+
return true;
}
@@ -281,7 +299,7 @@ internal abstract class ContentNavigationServiceBase
return false;
}
- GetDescendantsRecursively(parentNode, descendants);
+ GetDescendantsRecursively(structure, parentNode, descendants);
descendantsKeys = descendants;
return true;
@@ -291,17 +309,16 @@ internal abstract class ContentNavigationServiceBase
{
var ancestors = new List();
- if (structure.TryGetValue(childKey, out NavigationNode? childNode) is false)
+ if (structure.TryGetValue(childKey, out NavigationNode? node) is false)
{
// Child doesn't exist
ancestorsKeys = [];
return false;
}
- while (childNode?.Parent is not null)
+ while (node.Parent is not null && structure.TryGetValue(node.Parent.Value, out node))
{
- ancestors.Add(childNode.Parent.Key);
- childNode = childNode.Parent;
+ ancestors.Add(node.Key);
}
ancestorsKeys = ancestors;
@@ -322,12 +339,13 @@ internal abstract class ContentNavigationServiceBase
// To find siblings of a node at root level, we need to iterate over all items and add those with null Parent
siblingsKeys = structure
.Where(kv => kv.Value.Parent is null && kv.Key != key)
+ .OrderBy(kv => kv.Value.SortOrder)
.Select(kv => kv.Key)
.ToList();
return true;
}
- if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable childrenKeys) is false)
+ if (TryGetChildrenKeys(node.Parent.Value, out IEnumerable childrenKeys) is false)
{
return false; // Couldn't retrieve children keys
}
@@ -337,12 +355,18 @@ internal abstract class ContentNavigationServiceBase
return true;
}
- private void GetDescendantsRecursively(NavigationNode node, List descendants)
+ private void GetDescendantsRecursively(ConcurrentDictionary structure, NavigationNode node, List descendants)
{
- foreach (NavigationNode child in node.Children)
+ var childrenKeys = GetOrderedChildren(node, structure).ToList();
+ foreach (Guid childKey in childrenKeys)
{
- descendants.Add(child.Key);
- GetDescendantsRecursively(child, descendants);
+ descendants.Add(childKey);
+
+ // Retrieve the child node and its descendants
+ if (structure.TryGetValue(childKey, out NavigationNode? childNode))
+ {
+ GetDescendantsRecursively(structure, childNode, descendants);
+ }
}
}
@@ -354,9 +378,9 @@ internal abstract class ContentNavigationServiceBase
}
// Remove the node from its parent's children list
- if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode))
+ if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Value, out NavigationNode? parentNode))
{
- parentNode.RemoveChild(nodeToRemove);
+ parentNode.RemoveChild(structure, key);
}
return true;
@@ -366,25 +390,39 @@ internal abstract class ContentNavigationServiceBase
{
_recycleBinRoots.Add(node.Key);
_roots.Remove(node.Key);
+ var childrenKeys = GetOrderedChildren(node, _navigationStructure).ToList();
- foreach (NavigationNode child in node.Children)
+ foreach (Guid childKey in childrenKeys)
{
- AddDescendantsToRecycleBinRecursively(child);
+ if (_navigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false)
+ {
+ continue;
+ }
+
+ // Reset the SortOrder based on its new position in the bin
+ childNode.UpdateSortOrder(_recycleBinNavigationStructure.Count);
+ AddDescendantsToRecycleBinRecursively(childNode);
// Only remove the child from the main structure if it was successfully added to the recycle bin
- if (_recycleBinNavigationStructure.TryAdd(child.Key, child))
+ if (_recycleBinNavigationStructure.TryAdd(childKey, childNode))
{
- _navigationStructure.TryRemove(child.Key, out _);
+ _navigationStructure.TryRemove(childKey, out _);
}
}
}
private void RemoveDescendantsRecursively(NavigationNode node)
{
- foreach (NavigationNode child in node.Children)
+ var childrenKeys = GetOrderedChildren(node, _recycleBinNavigationStructure).ToList();
+ foreach (Guid childKey in childrenKeys)
{
- RemoveDescendantsRecursively(child);
- _recycleBinNavigationStructure.TryRemove(child.Key, out _);
+ if (_recycleBinNavigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false)
+ {
+ continue;
+ }
+
+ RemoveDescendantsRecursively(childNode);
+ _recycleBinNavigationStructure.TryRemove(childKey, out _);
}
}
@@ -394,28 +432,41 @@ internal abstract class ContentNavigationServiceBase
{
_roots.Add(node.Key);
}
- _recycleBinRoots.Remove(node.Key);
- foreach (NavigationNode child in node.Children)
+ _recycleBinRoots.Remove(node.Key);
+ var childrenKeys = GetOrderedChildren(node, _recycleBinNavigationStructure).ToList();
+
+ foreach (Guid childKey in childrenKeys)
{
- RestoreNodeAndDescendantsRecursively(child);
+ if (_recycleBinNavigationStructure.TryGetValue(childKey, out NavigationNode? childNode) is false)
+ {
+ continue;
+ }
+
+ RestoreNodeAndDescendantsRecursively(childNode);
// Only remove the child from the recycle bin structure if it was successfully added to the main one
- if (_navigationStructure.TryAdd(child.Key, child))
+ if (_navigationStructure.TryAdd(childKey, childNode))
{
- _recycleBinNavigationStructure.TryRemove(child.Key, out _);
+ _recycleBinNavigationStructure.TryRemove(childKey, out _);
}
}
}
+ private IEnumerable GetOrderedChildren(NavigationNode node, ConcurrentDictionary structure)
+ => node.Children
+ .Where(structure.ContainsKey)
+ .OrderBy(childKey => structure[childKey].SortOrder)
+ .ToList();
+
private static void BuildNavigationDictionary(ConcurrentDictionary nodesStructure, IList roots, IEnumerable entities)
{
var entityList = entities.ToList();
- IDictionary idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key);
+ var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key);
foreach (INavigationModel entity in entityList)
{
- var node = new NavigationNode(entity.Key);
+ var node = new NavigationNode(entity.Key, entity.SortOrder);
nodesStructure[entity.Key] = node;
// We don't set the parent for items under root, it will stay null
@@ -433,7 +484,7 @@ internal abstract class ContentNavigationServiceBase
// If the parent node exists in the nodesStructure, add the node to the parent's children (parent is set as well)
if (nodesStructure.TryGetValue(parentKey, out NavigationNode? parentNode))
{
- parentNode.AddChild(node);
+ parentNode.AddChild(nodesStructure, entity.Key);
}
}
}
diff --git a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs
index 4ab8458f18..80dce527e1 100644
--- a/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs
+++ b/src/Umbraco.Core/Services/Navigation/INavigationManagementService.cs
@@ -33,11 +33,20 @@ public interface INavigationManagementService
/// The unique identifier of the parent node. If null, the new node will be added to
/// the root level.
///
+ ///
+ /// Optional value to define the node's position among its siblings when
+ /// adding node at root level.
///
/// true if the node was successfully added to the main navigation structure;
/// otherwise, false.
///
- bool Add(Guid key, Guid? parentKey = null);
+ ///
+ /// The sort order is particularly important when adding nodes at the root level. For child nodes,
+ /// it can usually be determined by the number of existing children under the parent. However,
+ /// when adding nodes directly to the root (where parentKey is null), a sort order must be provided
+ /// to ensure the item appears in the correct position among other root-level items.
+ ///
+ bool Add(Guid key, Guid? parentKey = null, int? sortOrder = null);
///
/// Moves an existing node to a new parent in the main navigation structure. If a
@@ -54,4 +63,15 @@ public interface INavigationManagementService
/// in the main navigation structure; otherwise, false.
///
bool Move(Guid key, Guid? targetParentKey = null);
+
+ ///
+ /// Updates the sort order of a node in the main navigation structure.
+ /// The sort order of other nodes in the same level will be adjusted accordingly.
+ ///
+ /// The unique identifier of the node to update.
+ /// The new sort order for the node.
+ ///
+ /// true if the node's sort order was successfully updated; otherwise, false.
+ ///
+ bool UpdateSortOrder(Guid key, int newSortOrder);
}
diff --git a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs
index e440c30794..7977e0c5db 100644
--- a/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs
+++ b/src/Umbraco.Core/Services/Navigation/INavigationQueryService.cs
@@ -18,7 +18,7 @@ public interface INavigationQueryService
bool TryGetDescendantsKeysOrSelfKeys(Guid childKey, out IEnumerable descendantsOrSelfKeys)
{
- if(TryGetDescendantsKeys(childKey, out var descendantsKeys))
+ if (TryGetDescendantsKeys(childKey, out IEnumerable? descendantsKeys))
{
descendantsOrSelfKeys = childKey.Yield().Concat(descendantsKeys);
return true;
@@ -28,14 +28,13 @@ public interface INavigationQueryService
return false;
}
-
bool TryGetAncestorsKeys(Guid childKey, out IEnumerable ancestorsKeys);
bool TryGetAncestorsOrSelfKeys(Guid childKey, out IEnumerable ancestorsOrSelfKeys)
{
- if(TryGetAncestorsKeys(childKey, out var ancestorsKeys))
+ if (TryGetAncestorsKeys(childKey, out IEnumerable? ancestorsKeys))
{
- ancestorsOrSelfKeys = childKey.Yield().Concat(ancestorsKeys);
+ ancestorsOrSelfKeys = childKey.Yield().Concat(ancestorsKeys);
return true;
}
diff --git a/src/Umbraco.Core/Services/PublishStatus/IPublishStatusManagementService.cs b/src/Umbraco.Core/Services/PublishStatus/IPublishStatusManagementService.cs
new file mode 100644
index 0000000000..215564fe27
--- /dev/null
+++ b/src/Umbraco.Core/Services/PublishStatus/IPublishStatusManagementService.cs
@@ -0,0 +1,9 @@
+namespace Umbraco.Cms.Core.Services.Navigation;
+
+public interface IPublishStatusManagementService
+{
+ Task InitializeAsync(CancellationToken cancellationToken);
+ Task AddOrUpdateStatusAsync(Guid documentKey, CancellationToken cancellationToken);
+ Task RemoveAsync(Guid documentKey, CancellationToken cancellationToken);
+ Task AddOrUpdateStatusWithDescendantsAsync(Guid rootDocumentKey, CancellationToken cancellationToken);
+}
diff --git a/src/Umbraco.Core/Services/PublishStatus/IPublishStatusQueryService.cs b/src/Umbraco.Core/Services/PublishStatus/IPublishStatusQueryService.cs
new file mode 100644
index 0000000000..523bcdbfb1
--- /dev/null
+++ b/src/Umbraco.Core/Services/PublishStatus/IPublishStatusQueryService.cs
@@ -0,0 +1,9 @@
+namespace Umbraco.Cms.Core.Services.Navigation;
+
+///
+///
+///
+public interface IPublishStatusQueryService
+{
+ bool IsDocumentPublished(Guid documentKey, string culture);
+}
diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishStatusInitializationHostedService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishStatusInitializationHostedService.cs
new file mode 100644
index 0000000000..b0d3583a60
--- /dev/null
+++ b/src/Umbraco.Core/Services/PublishStatus/PublishStatusInitializationHostedService.cs
@@ -0,0 +1,42 @@
+using Microsoft.Extensions.Hosting;
+
+namespace Umbraco.Cms.Core.Services.Navigation;
+
+///
+/// Responsible for seeding the in-memory publish status cache at application's startup
+/// by loading all data from the database.
+///
+public sealed class PublishStatusInitializationHostedService : IHostedLifecycleService
+{
+ private readonly IRuntimeState _runtimeState;
+ private readonly IPublishStatusManagementService _publishStatusManagementService;
+
+ public PublishStatusInitializationHostedService(
+ IRuntimeState runtimeState,
+ IPublishStatusManagementService publishStatusManagementService
+ )
+ {
+ _runtimeState = runtimeState;
+ _publishStatusManagementService = publishStatusManagementService;
+ }
+
+ public async Task StartingAsync(CancellationToken cancellationToken)
+ {
+ if(_runtimeState.Level < RuntimeLevel.Upgrade)
+ {
+ return;
+ }
+
+ await _publishStatusManagementService.InitializeAsync(cancellationToken);
+ }
+
+ public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+}
diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
new file mode 100644
index 0000000000..17694873c5
--- /dev/null
+++ b/src/Umbraco.Core/Services/PublishStatus/PublishStatusService.cs
@@ -0,0 +1,84 @@
+using System.Collections.Concurrent;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+
+namespace Umbraco.Cms.Core.Services.Navigation;
+
+public class PublishStatusService : IPublishStatusManagementService, IPublishStatusQueryService
+{
+ private readonly ILogger _logger;
+ private readonly IPublishStatusRepository _publishStatusRepository;
+ private readonly ICoreScopeProvider _coreScopeProvider;
+ private readonly IDictionary> _publishedCultures = new Dictionary>();
+ public PublishStatusService(
+ ILogger logger,
+ IPublishStatusRepository publishStatusRepository,
+ ICoreScopeProvider coreScopeProvider)
+ {
+ _logger = logger;
+ _publishStatusRepository = publishStatusRepository;
+ _coreScopeProvider = coreScopeProvider;
+ }
+
+ public async Task InitializeAsync(CancellationToken cancellationToken)
+ {
+ _publishedCultures.Clear();
+ IDictionary> publishStatus;
+ using (ICoreScope scope = _coreScopeProvider.CreateCoreScope())
+ {
+ publishStatus = await _publishStatusRepository.GetAllPublishStatusAsync(cancellationToken);
+ scope.Complete();
+ }
+
+ foreach ((Guid documentKey, ISet publishedCultures) in publishStatus)
+ {
+ if (_publishedCultures.TryAdd(documentKey, publishedCultures) is false)
+ {
+ _logger.LogWarning("Failed to add published cultures for document {DocumentKey}", documentKey);
+ }
+ }
+
+
+ }
+
+ public bool IsDocumentPublished(Guid documentKey, string culture)
+ {
+ if (_publishedCultures.TryGetValue(documentKey, out ISet? publishedCultures))
+ {
+ return publishedCultures.Contains(culture, StringComparer.InvariantCultureIgnoreCase);
+ }
+
+ _logger.LogDebug("Document {DocumentKey} not found in the publish status cache", documentKey);
+ return false;
+ }
+
+ public async Task AddOrUpdateStatusAsync(Guid documentKey, CancellationToken cancellationToken)
+ {
+ using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
+ ISet publishedCultures = await _publishStatusRepository.GetPublishStatusAsync(documentKey, cancellationToken);
+ _publishedCultures[documentKey] = publishedCultures;
+ scope.Complete();
+ }
+
+ public Task RemoveAsync(Guid documentKey, CancellationToken cancellationToken)
+ {
+ _publishedCultures.Remove(documentKey);
+ return Task.CompletedTask;
+ }
+
+ public async Task AddOrUpdateStatusWithDescendantsAsync(Guid rootDocumentKey, CancellationToken cancellationToken)
+ {
+ IDictionary> publishStatus;
+ using (ICoreScope scope = _coreScopeProvider.CreateCoreScope())
+ {
+ publishStatus = await _publishStatusRepository.GetDescendantsOrSelfPublishStatusAsync(rootDocumentKey, cancellationToken);
+ scope.Complete();
+ }
+
+ foreach ((Guid documentKey, ISet publishedCultures) in publishStatus)
+ {
+ _publishedCultures[documentKey] = publishedCultures;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs
index dc0fbf281d..ba5d1a844c 100644
--- a/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs
+++ b/src/Umbraco.Core/Templates/HtmlLocalLinkParser.cs
@@ -14,11 +14,15 @@ public sealed class HtmlLocalLinkParser
// media
// other page
internal static readonly Regex LocalLinkTagPattern = new(
- @"document|media)['""].*?(?href=[""']/{localLink:(?[a-fA-F0-9-]+)})[""'])|((?href=[""']/{localLink:(?[a-fA-F0-9-]+)})[""'].*?type=(['""])(?document|media)(?:['""])))|(?:(?:type=['""](?document|media)['""])|(?:(?href=[""']/{localLink:[a-fA-F0-9-]+})[""'])))[^>]*>",
+ @"\/?{localLink:(?[a-fA-F0-9-]+)})[^>]*?>",
+ RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
+
+ internal static readonly Regex TypePattern = new(
+ """type=['"](?(?:media|document))['"]""",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
internal static readonly Regex LocalLinkPattern = new(
- @"href=""[/]?(?:\{|\%7B)localLink:([a-zA-Z0-9-://]+)(?:\}|\%7D)",
+ @"href=['""](?\/?(?:\{|\%7B)localLink:(?[a-zA-Z0-9-://]+)(?:\}|\%7D))",
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
private readonly IPublishedUrlProvider _publishedUrlProvider;
@@ -58,24 +62,20 @@ public sealed class HtmlLocalLinkParser
{
if (tagData.Udi is not null)
{
- var newLink = "#";
- if (tagData.Udi?.EntityType == Constants.UdiEntityType.Document)
+ var newLink = tagData.Udi?.EntityType switch
{
- newLink = _publishedUrlProvider.GetUrl(tagData.Udi.Guid);
- }
- else if (tagData.Udi?.EntityType == Constants.UdiEntityType.Media)
- {
- newLink = _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid);
- }
-
+ Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(tagData.Udi.Guid),
+ Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid),
+ _ => ""
+ };
text = StripTypeAttributeFromTag(text, tagData.Udi!.EntityType);
- text = text.Replace(tagData.TagHref, "href=\"" + newLink);
+ text = text.Replace(tagData.TagHref, newLink);
}
else if (tagData.IntId.HasValue)
{
var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value);
- text = text.Replace(tagData.TagHref, "href=\"" + newLink);
+ text = text.Replace(tagData.TagHref, newLink);
}
}
@@ -83,7 +83,7 @@ public sealed class HtmlLocalLinkParser
}
// under normal circumstances, the type attribute is preceded by a space
- // to cover the rare occasion where it isn't, we first replace with a a space and then without.
+ // to cover the rare occasion where it isn't, we first replace with a space and then without.
private string StripTypeAttributeFromTag(string tag, string type) =>
tag.Replace($" type=\"{type}\"", string.Empty)
.Replace($"type=\"{type}\"", string.Empty);
@@ -93,21 +93,22 @@ public sealed class HtmlLocalLinkParser
MatchCollection localLinkTagMatches = LocalLinkTagPattern.Matches(text);
foreach (Match linkTag in localLinkTagMatches)
{
- if (linkTag.Groups.Count < 1)
+ if (Guid.TryParse(linkTag.Groups["guid"].Value, out Guid guid) is false)
{
continue;
}
- if (Guid.TryParse(linkTag.Groups["guid"].Value, out Guid guid) is false)
+ // Find the type attribute
+ Match typeMatch = TypePattern.Match(linkTag.Value);
+ if (typeMatch.Success is false)
{
continue;
}
yield return new LocalLinkTag(
null,
- new GuidUdi(linkTag.Groups["type"].Value, guid),
- linkTag.Groups["locallink"].Value,
- linkTag.Value);
+ new GuidUdi(typeMatch.Groups["type"].Value, guid),
+ linkTag.Groups["locallink"].Value);
}
// also return legacy results for values that have not been migrated
@@ -124,25 +125,26 @@ public sealed class HtmlLocalLinkParser
MatchCollection tags = LocalLinkPattern.Matches(text);
foreach (Match tag in tags)
{
- if (tag.Groups.Count > 0)
+ if (tag.Groups.Count <= 0)
{
- var id = tag.Groups[1].Value; // .Remove(tag.Groups[1].Value.Length - 1, 1);
+ continue;
+ }
- // The id could be an int or a UDI
- if (UdiParser.TryParse(id, out Udi? udi))
- {
- var guidUdi = udi as GuidUdi;
- if (guidUdi is not null)
- {
- yield return new LocalLinkTag(null, guidUdi, tag.Value, null);
- }
- }
+ var id = tag.Groups["guid"].Value;
- if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId))
+ // The id could be an int or a UDI
+ if (UdiParser.TryParse(id, out Udi? udi))
+ {
+ if (udi is GuidUdi guidUdi)
{
- yield return new LocalLinkTag (intId, null, tag.Value, null);
+ yield return new LocalLinkTag(null, guidUdi, tag.Groups["locallink"].Value);
}
}
+
+ if (int.TryParse(id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intId))
+ {
+ yield return new LocalLinkTag (intId, null, tag.Groups["locallink"].Value);
+ }
}
}
@@ -155,20 +157,10 @@ public sealed class HtmlLocalLinkParser
TagHref = tagHref;
}
- public LocalLinkTag(int? intId, GuidUdi? udi, string tagHref, string? fullTag)
- {
- IntId = intId;
- Udi = udi;
- TagHref = tagHref;
- FullTag = fullTag;
- }
-
public int? IntId { get; }
public GuidUdi? Udi { get; }
public string TagHref { get; }
-
- public string? FullTag { get; }
}
}
diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs
index 5cbf0e6dc2..14dc4db632 100644
--- a/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs
+++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/BlockEditorElementTypeCache.cs
@@ -15,7 +15,9 @@ internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache
_appCaches = appCaches;
}
- public IEnumerable GetAll(IEnumerable keys)
+ public IEnumerable GetMany(IEnumerable keys) => GetAll().Where(elementType => keys.Contains(elementType.Key));
+
+ public IEnumerable GetAll()
{
// TODO: make this less dumb; don't fetch all elements, only fetch the items that aren't yet in the cache and amend the cache as more elements are loaded
@@ -27,6 +29,6 @@ internal sealed class BlockEditorElementTypeCache : IBlockEditorElementTypeCache
_appCaches.RequestCache.Set(cacheKey, cachedElements);
}
- return cachedElements.Where(elementType => keys.Contains(elementType.Key));
+ return cachedElements;
}
}
diff --git a/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs
index 5ab1dc49af..f99d2ff875 100644
--- a/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs
+++ b/src/Umbraco.Infrastructure/Cache/PropertyEditors/IBlockEditorElementTypeCache.cs
@@ -4,5 +4,6 @@ namespace Umbraco.Cms.Core.Cache.PropertyEditors;
public interface IBlockEditorElementTypeCache
{
- IEnumerable GetAll(IEnumerable keys);
+ IEnumerable GetMany(IEnumerable keys);
+ IEnumerable GetAll();
}
diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
index 37e3c6063c..73ee0d263a 100644
--- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
+++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs
@@ -81,6 +81,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique();
builder.Services.AddUnique();
builder.Services.AddUnique();
+ builder.Services.AddUnique();
return builder;
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs
index ba2530d40d..6abef6eeba 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ContentTypeDto.cs
@@ -12,6 +12,9 @@ internal class ContentTypeDto
public const string TableName = Constants.DatabaseSchema.Tables.ContentType;
private string? _alias;
+ // Public constants to bind properties between DTOs
+ public const string VariationsColumnName = "variations";
+
[Column("pk")]
[PrimaryKeyColumn(IdentitySeed = 700)]
public int PrimaryKey { get; set; }
@@ -51,7 +54,7 @@ internal class ContentTypeDto
[Constraint(Default = "0")]
public bool AllowAtRoot { get; set; }
- [Column("variations")]
+ [Column(VariationsColumnName)]
[Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)]
public byte Variations { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs
index 2bd9f559ec..e89d9cae63 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentCultureVariationDto.cs
@@ -11,6 +11,9 @@ internal class DocumentCultureVariationDto
{
public const string TableName = Constants.DatabaseSchema.Tables.DocumentCultureVariation;
+ // Public constants to bind properties between DTOs
+ public const string PublishedColumnName = "published";
+
[Column("id")]
[PrimaryKeyColumn]
public int Id { get; set; }
@@ -40,7 +43,7 @@ internal class DocumentCultureVariationDto
// de-normalized for perfs
// (means there is a published content version culture variation for the language)
- [Column("published")]
+ [Column(PublishedColumnName)]
public bool Published { get; set; }
// de-normalized for perfs
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs
index 715d588ff4..e50ed28de6 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DocumentDto.cs
@@ -11,12 +11,16 @@ public class DocumentDto
{
private const string TableName = Constants.DatabaseSchema.Tables.Document;
+
+ // Public constants to bind properties between DTOs
+ public const string PublishedColumnName = "published";
+
[Column("nodeId")]
[PrimaryKeyColumn(AutoIncrement = false)]
[ForeignKey(typeof(ContentDto))]
public int NodeId { get; set; }
- [Column("published")]
+ [Column(PublishedColumnName)]
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Published")]
public bool Published { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs
index bcf8403b73..3fe65f8322 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/LanguageDto.cs
@@ -11,6 +11,9 @@ internal class LanguageDto
{
public const string TableName = Constants.DatabaseSchema.Tables.Language;
+ // Public constants to bind properties between DTOs
+ public const string IsoCodeColumnName = "languageISOCode";
+
///
/// Gets or sets the identifier of the language.
///
@@ -21,7 +24,7 @@ internal class LanguageDto
///
/// Gets or sets the ISO code of the language.
///
- [Column("languageISOCode")]
+ [Column(IsoCodeColumnName)]
[Index(IndexTypes.UniqueNonClustered)]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(14)]
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs
index 156a85b19c..6b452b0c8a 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NavigationDto.cs
@@ -19,6 +19,10 @@ internal class NavigationDto : INavigationModel
[Column(NodeDto.ParentIdColumnName)]
public int ParentId { get; set; }
+ ///
+ [Column(NodeDto.SortOrderColumnName)]
+ public int SortOrder { get; set; }
+
///
[Column(NodeDto.TrashedColumnName)]
public bool Trashed { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs
index 2ac62429ba..c136f45fd4 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/NodeDto.cs
@@ -17,6 +17,7 @@ public class NodeDto
public const string IdColumnName = "id";
public const string KeyColumnName = "uniqueId";
public const string ParentIdColumnName = "parentId";
+ public const string SortOrderColumnName = "sortOrder";
public const string TrashedColumnName = "trashed";
private int? _userId;
@@ -46,7 +47,7 @@ public class NodeDto
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Path")]
public string Path { get; set; } = null!;
- [Column("sortOrder")]
+ [Column(SortOrderColumnName)]
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType_trashed_sorted", ForColumns = "nodeObjectType,trashed,sortOrder,id", IncludeColumns = "uniqueID,parentID,level,path,nodeUser,text,createDate")]
public int SortOrder { get; set; }
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublishStatusRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublishStatusRepository.cs
new file mode 100644
index 0000000000..c4889df880
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/PublishStatusRepository.cs
@@ -0,0 +1,137 @@
+using NPoco;
+using Umbraco.Cms.Core.DeliveryApi;
+using Umbraco.Cms.Core.Media.EmbedProviders;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Cms.Infrastructure.Scoping;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
+
+public class PublishStatusRepository: IPublishStatusRepository
+{
+ private readonly IScopeAccessor _scopeAccessor;
+
+ public PublishStatusRepository(IScopeAccessor scopeAccessor)
+ => _scopeAccessor = scopeAccessor;
+
+ private IUmbracoDatabase Database
+ {
+ get
+ {
+ if (_scopeAccessor.AmbientScope is null)
+ {
+ throw new NotSupportedException("Need to be executed in a scope");
+ }
+
+ return _scopeAccessor.AmbientScope.Database;
+ }
+ }
+
+ private Sql GetBaseQuery()
+ {
+ Sql sql = Database.SqlContext.Sql()
+ .Select(
+ $"n.{NodeDto.KeyColumnName}",
+ $"l.{LanguageDto.IsoCodeColumnName}",
+ $"ct.{ContentTypeDto.VariationsColumnName}",
+ $"d.{DocumentDto.PublishedColumnName}",
+ $"COALESCE(dcv.{DocumentCultureVariationDto.PublishedColumnName}, 0) as {PublishStatusDto.DocumentVariantPublishStatusColumnName}")
+ .From("d")
+ .InnerJoin("c").On((d, c) => d.NodeId == c.NodeId, "c", "d")
+ .InnerJoin("ct").On((c, ct) => c.ContentTypeId == ct.NodeId, "c", "ct")
+ .CrossJoin("l")
+ .LeftJoin("dcv").On((l, dcv, d) => l.Id == dcv.LanguageId && d.NodeId == dcv.NodeId , "l", "dcv", "d")
+ .InnerJoin("n").On((d, n) => n.NodeId == d.NodeId, "d", "n")
+ ;
+
+ return sql;
+ }
+
+
+ public async Task>> GetAllPublishStatusAsync(CancellationToken cancellationToken)
+ {
+ Sql sql = GetBaseQuery();
+
+ List? databaseRecords = await Database.FetchAsync(sql);
+
+ return Map(databaseRecords);
+ }
+
+ public async Task> GetPublishStatusAsync(Guid documentKey, CancellationToken cancellationToken)
+ {
+ Sql sql = GetBaseQuery();
+ sql = sql.Where(n => n.UniqueId == documentKey, "n");
+
+ List? databaseRecords = await Database.FetchAsync(sql);
+
+ IDictionary> result = Map(databaseRecords);
+ return result.ContainsKey(documentKey) ? result[documentKey] : new HashSet();
+ }
+
+ public async Task>> GetDescendantsOrSelfPublishStatusAsync(Guid rootDocumentKey, CancellationToken cancellationToken)
+ {
+ var pathSql = Database.SqlContext.Sql()
+ .Select(x => x.Path)
+ .From()
+ .Where(x => x.UniqueId == rootDocumentKey);
+ var rootPath = await Database.ExecuteScalarAsync(pathSql);
+
+ Sql sql = GetBaseQuery()
+ .InnerJoin("rn").On((n, rn) => n.Path.StartsWith(rootPath), "n", "rn") //rn = root node
+ .Where(rn => rn.UniqueId == rootDocumentKey, "rn");
+
+ List? databaseRecords = await Database.FetchAsync(sql);
+
+ IDictionary> result = Map(databaseRecords);
+
+ return result;
+ }
+
+ private IDictionary> Map(List databaseRecords)
+ {
+ return databaseRecords
+ .GroupBy(x => x.Key)
+ .ToDictionary(
+ x=>x.Key,
+ x=> (ISet) x.Where(x=> IsPublished(x)).Select(y=>y.IsoCode).ToHashSet());
+ }
+
+ private bool IsPublished(PublishStatusDto publishStatusDto)
+ {
+ switch ((ContentVariation)publishStatusDto.ContentTypeVariation)
+ {
+ case ContentVariation.Culture:
+ case ContentVariation.CultureAndSegment:
+ return publishStatusDto.DocumentVariantPublishStatus;
+ case ContentVariation.Nothing:
+ case ContentVariation.Segment:
+ default:
+ return publishStatusDto.DocumentInvariantPublished;
+ }
+ }
+
+ private class PublishStatusDto
+ {
+
+ public const string DocumentVariantPublishStatusColumnName = "variantPublished";
+
+
+ [Column(NodeDto.KeyColumnName)]
+ public Guid Key { get; set; }
+
+ [Column(LanguageDto.IsoCodeColumnName)]
+ public string IsoCode { get; set; } = "";
+
+ [Column(ContentTypeDto.VariationsColumnName)]
+ public byte ContentTypeVariation { get; set; }
+
+ [Column(DocumentDto.PublishedColumnName)]
+ public bool DocumentInvariantPublished { get; set; }
+
+ [Column(DocumentVariantPublishStatusColumnName)]
+ public bool DocumentVariantPublishStatus { get; set; }
+ }
+
+}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
index 97fcd17aa9..798cdf1d7d 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
@@ -1,8 +1,10 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
@@ -20,6 +22,7 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal
{
private readonly IJsonSerializer _jsonSerializer;
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
protected BlockEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
@@ -29,9 +32,22 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal
ILogger> logger,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
- IIOHelper ioHelper,
- BlockEditorVarianceHandler blockEditorVarianceHandler)
- : base(attribute, propertyEditors, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories, blockEditorVarianceHandler) =>
+ IIOHelper ioHelper)
+ : this(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ protected BlockEditorPropertyValueEditor(
+ PropertyEditorCollection propertyEditors,
+ DataValueReferenceFactoryCollection dataValueReferenceFactories,
+ IDataTypeConfigurationCache dataTypeConfigurationCache,
+ IShortStringHelper shortStringHelper,
+ IJsonSerializer jsonSerializer,
+ BlockEditorVarianceHandler blockEditorVarianceHandler,
+ ILanguageService languageService)
+ : base(propertyEditors, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, dataValueReferenceFactories, blockEditorVarianceHandler, languageService) =>
_jsonSerializer = jsonSerializer;
///
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs
index e77ff3d9c0..281b5151ed 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs
@@ -32,7 +32,7 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV
foreach (var group in itemDataGroups)
{
- var allElementTypes = _elementTypeCache.GetAll(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key);
+ var allElementTypes = _elementTypeCache.GetMany(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key);
for (var i = 0; i < group.Items.Count; i++)
{
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs
index 5fcb8d4dd9..1beddc2a61 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs
@@ -59,7 +59,7 @@ public class BlockEditorValues
// filter out any content that isn't referenced in the layout references
IEnumerable contentTypeKeys = blockEditorData.BlockValue.ContentData.Select(x => x.ContentTypeKey)
.Union(blockEditorData.BlockValue.SettingsData.Select(x => x.ContentTypeKey)).Distinct();
- IDictionary contentTypesDictionary = _elementTypeCache.GetAll(contentTypeKeys).ToDictionary(x=>x.Key);
+ IDictionary contentTypesDictionary = _elementTypeCache.GetMany(contentTypeKeys).ToDictionary(x=>x.Key);
foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x =>
blockEditorData.References.Any(r => r.ContentKey == x.Key)))
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
index 2b7364b1aa..b0e8ac46fc 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs
@@ -5,7 +5,6 @@ using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
-using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
@@ -37,12 +36,11 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
#region Value Editor
protected override IDataValueEditor CreateValueEditor() =>
- DataValueEditorFactory.Create(Attribute!);
+ DataValueEditorFactory.Create();
internal class BlockGridEditorPropertyValueEditor : BlockEditorPropertyValueEditor
{
public BlockGridEditorPropertyValueEditor(
- DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeConfigurationCache dataTypeConfigurationCache,
@@ -50,11 +48,11 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
ILogger logger,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
- IIOHelper ioHelper,
IBlockEditorElementTypeCache elementTypeCache,
IPropertyValidationService propertyValidationService,
- BlockEditorVarianceHandler blockEditorVarianceHandler)
- : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, blockEditorVarianceHandler)
+ BlockEditorVarianceHandler blockEditorVarianceHandler,
+ ILanguageService languageService)
+ : base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService)
{
BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache));
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
index 59fa980d2b..2c194ba2c2 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs
@@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
-using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
@@ -47,12 +46,11 @@ public abstract class BlockListPropertyEditorBase : DataEditor
protected virtual BlockEditorDataConverter CreateBlockEditorDataConverter() => new BlockListEditorDataConverter(_jsonSerializer);
protected override IDataValueEditor CreateValueEditor() =>
- DataValueEditorFactory.Create(Attribute!, CreateBlockEditorDataConverter());
+ DataValueEditorFactory.Create(CreateBlockEditorDataConverter());
internal class BlockListEditorPropertyValueEditor : BlockEditorPropertyValueEditor
{
public BlockListEditorPropertyValueEditor(
- DataEditorAttribute attribute,
BlockEditorDataConverter blockEditorDataConverter,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
@@ -62,10 +60,10 @@ public abstract class BlockListPropertyEditorBase : DataEditor
ILogger logger,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
- IIOHelper ioHelper,
IPropertyValidationService propertyValidationService,
- BlockEditorVarianceHandler blockEditorVarianceHandler)
- : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, blockEditorVarianceHandler)
+ BlockEditorVarianceHandler blockEditorVarianceHandler,
+ ILanguageService languageService)
+ : base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService)
{
BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, elementTypeCache, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache));
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
index 82d66a3bd0..8a774a5cfd 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs
@@ -1,5 +1,7 @@
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
@@ -8,7 +10,6 @@ using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
-using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -18,12 +19,13 @@ public abstract class BlockValuePropertyValueEditorBase : DataV
{
private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache;
private readonly PropertyEditorCollection _propertyEditors;
- private readonly ILogger _logger;
private readonly IJsonSerializer _jsonSerializer;
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactoryCollection;
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
private BlockEditorValues? _blockEditorValues;
+ private readonly ILanguageService _languageService;
+ [Obsolete("Please use the non-obsolete constructor. Will be removed in V16.")]
protected BlockValuePropertyValueEditorBase(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
@@ -33,16 +35,33 @@ public abstract class BlockValuePropertyValueEditorBase : DataV
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
+ DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection)
+ : this(propertyEditors,
+ dataTypeConfigurationCache,
+ shortStringHelper,
+ jsonSerializer,
+ dataValueReferenceFactoryCollection,
+ StaticServiceProvider.Instance.GetRequiredService(),
+ StaticServiceProvider.Instance.GetRequiredService())
+ {
+ }
+
+ protected BlockValuePropertyValueEditorBase(
+ PropertyEditorCollection propertyEditors,
+ IDataTypeConfigurationCache dataTypeConfigurationCache,
+ IShortStringHelper shortStringHelper,
+ IJsonSerializer jsonSerializer,
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection,
- BlockEditorVarianceHandler blockEditorVarianceHandler)
- : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute)
+ BlockEditorVarianceHandler blockEditorVarianceHandler,
+ ILanguageService languageService)
+ : base(shortStringHelper, jsonSerializer)
{
_propertyEditors = propertyEditors;
_dataTypeConfigurationCache = dataTypeConfigurationCache;
- _logger = logger;
_jsonSerializer = jsonSerializer;
_dataValueReferenceFactoryCollection = dataValueReferenceFactoryCollection;
_blockEditorVarianceHandler = blockEditorVarianceHandler;
+ _languageService = languageService;
}
///
@@ -121,7 +140,10 @@ public abstract class BlockValuePropertyValueEditorBase : DataV
object? configuration = _dataTypeConfigurationCache.GetConfiguration(blockPropertyValue.PropertyType.DataTypeKey);
- result.AddRange(tagsProvider.GetTags(blockPropertyValue.Value, configuration, languageId));
+ var tagLanguageId = blockPropertyValue.Culture is not null
+ ? _languageService.GetAsync(blockPropertyValue.Culture).GetAwaiter().GetResult()?.Id
+ : languageId;
+ result.AddRange(tagsProvider.GetTags(blockPropertyValue.Value, configuration, tagLanguageId));
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
index 31cabac3bf..f1281747c2 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs
@@ -2,7 +2,6 @@
// See LICENSE for more details.
using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Cache.PropertyEditors;
@@ -66,7 +65,7 @@ public class RichTextPropertyEditor : DataEditor
///
///
protected override IDataValueEditor CreateValueEditor() =>
- DataValueEditorFactory.Create(Attribute!);
+ DataValueEditorFactory.Create();
protected override IConfigurationEditor CreateConfigurationEditor() =>
new RichTextConfigurationEditor(_ioHelper);
@@ -78,7 +77,6 @@ public class RichTextPropertyEditor : DataEditor
internal class RichTextPropertyValueEditor : BlockValuePropertyValueEditorBase
{
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
- private readonly ILocalizedTextService _localizedTextService;
private readonly IHtmlSanitizer _htmlSanitizer;
private readonly HtmlImageSourceParser _imageSourceParser;
private readonly HtmlLocalLinkParser _localLinkParser;
@@ -89,28 +87,25 @@ public class RichTextPropertyEditor : DataEditor
private readonly ILogger _logger;
public RichTextPropertyValueEditor(
- DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
IDataTypeConfigurationCache dataTypeReadCache,
ILogger logger,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
- ILocalizedTextService localizedTextService,
IShortStringHelper shortStringHelper,
HtmlImageSourceParser imageSourceParser,
HtmlLocalLinkParser localLinkParser,
RichTextEditorPastedImages pastedImages,
IJsonSerializer jsonSerializer,
- IIOHelper ioHelper,
IHtmlSanitizer htmlSanitizer,
IBlockEditorElementTypeCache elementTypeCache,
IPropertyValidationService propertyValidationService,
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection,
IRichTextRequiredValidator richTextRequiredValidator,
- BlockEditorVarianceHandler blockEditorVarianceHandler)
- : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection, blockEditorVarianceHandler)
+ BlockEditorVarianceHandler blockEditorVarianceHandler,
+ ILanguageService languageService)
+ : base(propertyEditors, dataTypeReadCache, shortStringHelper, jsonSerializer, dataValueReferenceFactoryCollection, blockEditorVarianceHandler, languageService)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
- _localizedTextService = localizedTextService;
_imageSourceParser = imageSourceParser;
_localLinkParser = localLinkParser;
_pastedImages = pastedImages;
diff --git a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs
index 17f7350a0d..4955f17554 100644
--- a/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs
+++ b/src/Umbraco.Infrastructure/Search/IndexingNotificationHandler.ContentType.cs
@@ -109,7 +109,7 @@ public sealed class ContentTypeIndexingNotificationHandler : INotificationHandle
{
const int pageSize = 500;
- IEnumerable memberTypes = _memberTypeService.GetAll(memberTypeIds);
+ IEnumerable memberTypes = _memberTypeService.GetMany(memberTypeIds);
foreach (IMemberType memberType in memberTypes)
{
var page = 0;
diff --git a/src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs b/src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs
index 38a3858904..429c923a8e 100644
--- a/src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/DatabaseCacheRebuilder.cs
@@ -1,18 +1,37 @@
-using Umbraco.Cms.Core.PublishedCache;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Logging;
+using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
namespace Umbraco.Cms.Infrastructure.HybridCache;
internal class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
{
+ private const string NuCacheSerializerKey = "Umbraco.Web.PublishedCache.NuCache.Serializer";
private readonly IDatabaseCacheRepository _databaseCacheRepository;
private readonly ICoreScopeProvider _coreScopeProvider;
+ private readonly IOptions _nucacheSettings;
+ private readonly IKeyValueService _keyValueService;
+ private readonly ILogger _logger;
+ private readonly IProfilingLogger _profilingLogger;
- public DatabaseCacheRebuilder(IDatabaseCacheRepository databaseCacheRepository, ICoreScopeProvider coreScopeProvider)
+ public DatabaseCacheRebuilder(
+ IDatabaseCacheRepository databaseCacheRepository,
+ ICoreScopeProvider coreScopeProvider,
+ IOptions nucacheSettings,
+ IKeyValueService keyValueService,
+ ILogger logger, IProfilingLogger profilingLogger)
{
_databaseCacheRepository = databaseCacheRepository;
_coreScopeProvider = coreScopeProvider;
+ _nucacheSettings = nucacheSettings;
+ _keyValueService = keyValueService;
+ _logger = logger;
+ _profilingLogger = profilingLogger;
}
public void Rebuild()
@@ -21,4 +40,28 @@ internal class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
_databaseCacheRepository.Rebuild();
scope.Complete();
}
+
+ public void RebuildDatabaseCacheIfSerializerChanged()
+ {
+ using var scope = _coreScopeProvider.CreateCoreScope();
+ NuCacheSerializerType serializer = _nucacheSettings.Value.NuCacheSerializerType;
+ var currentSerializerValue = _keyValueService.GetValue(NuCacheSerializerKey);
+
+ if (Enum.TryParse(currentSerializerValue, out NuCacheSerializerType currentSerializer) && serializer == currentSerializer)
+ {
+ return;
+ }
+
+ _logger.LogWarning(
+ "Database cache was serialized using {CurrentSerializer}. Currently configured cache serializer {Serializer}. Rebuilding database cache.",
+ currentSerializer, serializer);
+
+ using (_profilingLogger.TraceDuration($"Rebuilding database cache with {serializer} serializer"))
+ {
+ Rebuild();
+ _keyValueService.SetValue(NuCacheSerializerKey, serializer.ToString());
+ }
+
+ scope.Complete();
+ }
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
index 191927ba37..c33db4c8e1 100644
--- a/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -60,6 +60,7 @@ public static class UmbracoBuilderExtensions
throw new IndexOutOfRangeException();
}
});
+ builder.AddNotificationAsyncHandler(); // Need to happen before notification handlers use the cache. Eg. seeding
builder.Services.AddSingleton();
builder.AddNotificationAsyncHandler();
builder.AddNotificationAsyncHandler();
diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/HybridCacheStartupNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/HybridCacheStartupNotificationHandler.cs
new file mode 100644
index 0000000000..19d524f791
--- /dev/null
+++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/HybridCacheStartupNotificationHandler.cs
@@ -0,0 +1,32 @@
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Services;
+
+namespace Umbraco.Cms.Infrastructure.HybridCache.NotificationHandlers;
+
+///
+/// Rebuilds the database cache if required when the serializer changes
+///
+public class HybridCacheStartupNotificationHandler : INotificationAsyncHandler
+{
+ private readonly IDatabaseCacheRebuilder _databaseCacheRebuilder;
+ private readonly IRuntimeState _runtimeState;
+
+ public HybridCacheStartupNotificationHandler(IDatabaseCacheRebuilder databaseCacheRebuilder, IRuntimeState runtimeState)
+ {
+ _databaseCacheRebuilder = databaseCacheRebuilder;
+ _runtimeState = runtimeState;
+ }
+
+ public Task HandleAsync(UmbracoApplicationStartingNotification notification, CancellationToken cancellationToken)
+ {
+ if (_runtimeState.Level > RuntimeLevel.Install)
+ {
+ _databaseCacheRebuilder.RebuildDatabaseCacheIfSerializerChanged();
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
index 1cea1a2360..72d9668306 100644
--- a/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/NotificationHandlers/SeedingNotificationHandler.cs
@@ -1,4 +1,6 @@
-using Umbraco.Cms.Core;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
@@ -11,19 +13,21 @@ internal class SeedingNotificationHandler : INotificationAsyncHandler globalSettings)
{
_documentCacheService = documentCacheService;
_mediaCacheService = mediaCacheService;
_runtimeState = runtimeState;
+ _globalSettings = globalSettings.Value;
}
public async Task HandleAsync(UmbracoApplicationStartedNotification notification,
CancellationToken cancellationToken)
{
- if (_runtimeState.Level <= RuntimeLevel.Install)
+ if (_runtimeState.Level <= RuntimeLevel.Install || (_runtimeState.Level == RuntimeLevel.Upgrade && _globalSettings.ShowMaintenancePageWhenInUpgradeState))
{
return;
}
diff --git a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
index 4efc2fbebe..30d1358d64 100644
--- a/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
+++ b/src/Umbraco.PublishedCache.HybridCache/Persistence/DatabaseCacheRepository.cs
@@ -886,7 +886,7 @@ WHERE cmsContentNu.nodeId IN (
dto.VersionId,
dto.PubVersionDate,
dto.CreatorId,
- dto.EditTemplateId == 0 ? null : dto.EditTemplateId,
+ dto.PubTemplateId == 0 ? null : dto.PubTemplateId,
true,
deserializedContent?.PropertyData,
deserializedContent?.CultureData);
diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
index ea404d9703..4ccaccfba5 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -195,6 +195,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddHostedService();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
+ builder.Services.AddHostedService();
return builder;
}
diff --git a/src/Umbraco.Web.UI.Client b/src/Umbraco.Web.UI.Client
index 29583d3d34..8fc0d29019 160000
--- a/src/Umbraco.Web.UI.Client
+++ b/src/Umbraco.Web.UI.Client
@@ -1 +1 @@
-Subproject commit 29583d3d34f57e98052450128435fcb06a0c1984
+Subproject commit 8fc0d29019e8e35ce1663fd40b9fc71d51b50ad3
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs
index 95659b38be..08327d200a 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Copy.cs
@@ -106,4 +106,34 @@ public partial class DocumentNavigationServiceTests
Assert.AreNotEqual(GreatGrandchild1.Key, copiedGreatGrandChild1.Key);
});
}
+
+ [Test]
+ [TestCase(null)] // Content root
+ [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root
+ [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1
+ [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1
+ [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2
+ [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2
+ [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3
+ [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1
+ [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3
+ [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4
+ public async Task Copying_Content_Adds_It_Last(Guid? parentKey)
+ {
+ // Act
+ var copyAttempt = await ContentEditingService.CopyAsync(Grandchild1.Key, parentKey, false, true, Constants.Security.SuperUserKey);
+ Guid copiedItemKey = copyAttempt.Result.Key;
+
+ // Assert
+ if (parentKey is null)
+ {
+ DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys);
+ Assert.AreEqual(copiedItemKey, rootKeys.Last());
+ }
+ else
+ {
+ DocumentNavigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable childrenKeys);
+ Assert.AreEqual(copiedItemKey, childrenKeys.Last());
+ }
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs
index 4138f80b07..3e0d3e85f5 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Create.cs
@@ -1,6 +1,5 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
-using Umbraco.Cms.Core.Models.ContentEditing;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
@@ -37,7 +36,7 @@ public partial class DocumentNavigationServiceTests
// Arrange
DocumentNavigationQueryService.TryGetChildrenKeys(Child1.Key, out IEnumerable initialChildrenKeys);
var initialChild1ChildrenCount = initialChildrenKeys.Count();
- var createModel = CreateContentCreateModel("Child1Child", Guid.NewGuid(), Child1.Key);
+ var createModel = CreateContentCreateModel("Grandchild 3", Guid.NewGuid(), Child1.Key);
// Act
var createAttempt = await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
@@ -55,4 +54,37 @@ public partial class DocumentNavigationServiceTests
Assert.IsTrue(childrenList.Contains(createdItemKey));
});
}
+
+ [Test]
+ [TestCase(null)] // Content root
+ [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root
+ [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1
+ [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1
+ [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2
+ [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2
+ [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3
+ [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1
+ [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3
+ [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4
+ public async Task Creating_Child_Content_Adds_It_As_The_Last_Child(Guid? parentKey)
+ {
+ // Arrange
+ Guid newNodeKey = Guid.NewGuid();
+ var createModel = CreateContentCreateModel("Child", newNodeKey, parentKey);
+
+ // Act
+ await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ // Assert
+ if (parentKey is null)
+ {
+ DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys);
+ Assert.AreEqual(newNodeKey, rootKeys.Last());
+ }
+ else
+ {
+ DocumentNavigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable childrenKeys);
+ Assert.AreEqual(newNodeKey, childrenKeys.Last());
+ }
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs
index 8a60eaecc8..32ba7934b1 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Delete.cs
@@ -39,4 +39,42 @@ public partial class DocumentNavigationServiceTests
}
});
}
+
+ // TODO: Add more test cases
+ [Test]
+ public async Task Sort_Order_Of_Siblings_Updates_When_Deleting_Content_And_Adding_New_One()
+ {
+ // Arrange
+ Guid nodeToDelete = Child3.Key;
+ Guid node = Child1.Key;
+
+ // Act
+ await ContentEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion);
+ var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count);
+ Assert.AreEqual(Child2.Key, siblingsKeysAfterDeletionList[0]);
+ });
+
+ // Create a new sibling under the same parent
+ var key = Guid.NewGuid();
+ var createModel = CreateContentCreateModel("Child 4", key, Root.Key);
+ await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation);
+ var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList();
+
+ // Verify sibling order after creating the new content
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, siblingsKeysAfterCreationList.Count);
+ Assert.AreEqual(Child2.Key, siblingsKeysAfterCreationList[0]);
+ Assert.AreEqual(key, siblingsKeysAfterCreationList[1]);
+ });
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs
index f31f4f7907..0c8dc8c502 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Move.cs
@@ -94,4 +94,72 @@ public partial class DocumentNavigationServiceTests
Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants);
});
}
+
+ [Test]
+ [TestCase(null)] // Content root
+ [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root
+ [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2
+ [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2
+ [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3
+ [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1
+ [TestCase("B606E3FF-E070-4D46-8CB9-D31352029FDF")] // Child 3
+ [TestCase("F381906C-223C-4466-80F7-B63B4EE073F8")] // Grandchild 4
+ public async Task Moving_Content_Adds_It_Last(Guid? targetParentKey)
+ {
+ // Arrange
+ Guid nodeToMove = Grandchild1.Key;
+
+ // Act
+ await ContentEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey);
+
+ // Assert
+ if (targetParentKey is null)
+ {
+ DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys);
+ Assert.AreEqual(nodeToMove, rootKeys.Last());
+ }
+ else
+ {
+ DocumentNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys);
+ Assert.AreEqual(nodeToMove, childrenKeys.Last());
+ }
+ }
+
+ // TODO: Add more test cases
+ [Test]
+ public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Content_And_Adding_New_One()
+ {
+ // Arrange
+ Guid nodeToMove = Child3.Key;
+ Guid node = Child1.Key;
+
+ // Act
+ await ContentEditingService.MoveAsync(nodeToMove, null, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterMoving);
+ var siblingsKeysAfterMovingList = siblingsKeysAfterMoving.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(1, siblingsKeysAfterMovingList.Count);
+ Assert.AreEqual(Child2.Key, siblingsKeysAfterMovingList[0]);
+ });
+
+ // Create a new sibling under the same parent
+ var key = Guid.NewGuid();
+ var createModel = CreateContentCreateModel("Child 4", key, Root.Key);
+ await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation);
+ var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList();
+
+ // Verify sibling order after creating the new content
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, siblingsKeysAfterCreationList.Count);
+ Assert.AreEqual(Child2.Key, siblingsKeysAfterCreationList[0]);
+ Assert.AreEqual(key, siblingsKeysAfterCreationList[1]);
+ });
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs
index 9e5e7dcc69..f14c83d8fc 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.MoveToRecycleBin.cs
@@ -6,7 +6,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
public partial class DocumentNavigationServiceTests
{
[Test]
- public async Task Structure_Updates_When_Moving_Content_To_Recycle_Bin()
+ public async Task Parent_And_Descendants_Are_Updated_When_Content_Is_Moved_To_Recycle_Bin()
{
// Arrange
Guid nodeToMoveToRecycleBin = Child3.Key;
@@ -19,7 +19,7 @@ public partial class DocumentNavigationServiceTests
DocumentNavigationQueryService.TryGetDescendantsKeys(nodeToMoveToRecycleBin, out IEnumerable initialDescendantsKeys);
var beforeMoveDescendants = initialDescendantsKeys.ToList();
DocumentNavigationQueryService.TryGetChildrenKeys(originalParentKey.Value, out IEnumerable initialParentChildrenKeys);
- var beforeMoveParentSiblingsCount = initialParentChildrenKeys.Count();
+ var beforeMoveParentChildrenCount = initialParentChildrenKeys.Count();
// Act
await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey);
@@ -30,7 +30,7 @@ public partial class DocumentNavigationServiceTests
DocumentNavigationQueryService.TryGetDescendantsKeysInBin(nodeToMoveToRecycleBin, out IEnumerable afterMoveDescendantsKeys);
var afterMoveDescendants = afterMoveDescendantsKeys.ToList();
DocumentNavigationQueryService.TryGetChildrenKeys((Guid)originalParentKey, out IEnumerable afterMoveParentChildrenKeys);
- var afterMoveParentSiblingsCount = afterMoveParentChildrenKeys.Count();
+ var afterMoveParentChildrenCount = afterMoveParentChildrenKeys.Count();
DocumentNavigationQueryService.TryGetSiblingsKeysInBin(nodeInRecycleBin, out IEnumerable afterMoveRecycleBinSiblingsKeys);
var afterMoveRecycleBinSiblingsCount = afterMoveRecycleBinSiblingsKeys.Count();
@@ -42,8 +42,78 @@ public partial class DocumentNavigationServiceTests
Assert.IsNull(updatedParentKeyInRecycleBin); // Verify the node's parent is now located at the root of the recycle bin (null)
Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants);
- Assert.AreEqual(beforeMoveParentSiblingsCount - 1, afterMoveParentSiblingsCount);
+ Assert.AreEqual(beforeMoveParentChildrenCount - 1, afterMoveParentChildrenCount);
Assert.AreEqual(beforeMoveRecycleBinSiblingsCount + 1, afterMoveRecycleBinSiblingsCount);
});
}
+
+ // TODO: Add more test cases
+ [Test]
+ public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Content_To_Recycle_Bin_And_Adding_New_One()
+ {
+ // Arrange
+ Guid nodeToMoveToRecycleBin = Child3.Key;
+ Guid node = Child1.Key;
+
+ // Act
+ await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion);
+ var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count);
+ Assert.AreEqual(Child2.Key, siblingsKeysAfterDeletionList[0]);
+ });
+
+ // Create a new sibling under the same parent
+ var key = Guid.NewGuid();
+ var createModel = CreateContentCreateModel("Child 4", key, Root.Key);
+ await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation);
+ var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList();
+
+ // Verify sibling order after creating the new content
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, siblingsKeysAfterCreationList.Count);
+ Assert.AreEqual(Child2.Key, siblingsKeysAfterCreationList[0]);
+ Assert.AreEqual(key, siblingsKeysAfterCreationList[1]);
+ });
+ }
+
+ [Test]
+ public async Task Sort_Order_Of_Chilldren_Is_Maintained_When_Moving_Content_To_Recycle_Bin()
+ {
+ // Arrange
+ Guid nodeToMoveToRecycleBin = Child1.Key;
+
+ // Create a new grandchild under Child1
+ var key = Guid.NewGuid();
+ var createModel = CreateContentCreateModel("Grandchild 3", key, nodeToMoveToRecycleBin);
+ await ContentEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ DocumentNavigationQueryService.TryGetChildrenKeys(nodeToMoveToRecycleBin, out IEnumerable childrenKeysBeforeDeletion);
+ var childrenKeysBeforeDeletionList = childrenKeysBeforeDeletion.ToList();
+
+ // Act
+ await ContentEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetChildrenKeysInBin(nodeToMoveToRecycleBin, out IEnumerable childrenKeysAfterDeletion);
+ var childrenKeysAfterDeletionList = childrenKeysAfterDeletion.ToList();
+
+ // Verify children order in the bin
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(3, childrenKeysAfterDeletionList.Count);
+ Assert.AreEqual(Grandchild1.Key, childrenKeysAfterDeletionList[0]);
+ Assert.AreEqual(Grandchild2.Key, childrenKeysAfterDeletionList[1]);
+ Assert.AreEqual(key, childrenKeysAfterDeletionList[2]);
+ Assert.IsTrue(childrenKeysBeforeDeletionList.SequenceEqual(childrenKeysAfterDeletionList));
+ });
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs
index e57f0c652c..c4a6a2a3de 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Restore.cs
@@ -50,4 +50,37 @@ public partial class DocumentNavigationServiceTests
Assert.AreEqual(targetParentKey, restoredItemParentKey);
});
}
+
+ [Test]
+ [TestCase(null)] // Content root
+ [TestCase("E48DD82A-7059-418E-9B82-CDD5205796CF")] // Root
+ [TestCase("C6173927-0C59-4778-825D-D7B9F45D8DDE")] // Child 1
+ [TestCase("E856AC03-C23E-4F63-9AA9-681B42A58573")] // Grandchild 1
+ [TestCase("A1B1B217-B02F-4307-862C-A5E22DB729EB")] // Grandchild 2
+ [TestCase("60E0E5C4-084E-4144-A560-7393BEAD2E96")] // Child 2
+ [TestCase("D63C1621-C74A-4106-8587-817DEE5FB732")] // Grandchild 3
+ [TestCase("56E29EA9-E224-4210-A59F-7C2C5C0C5CC7")] // Great-grandchild 1
+ public async Task Restoring_Content_Adds_It_Last(Guid? targetParentKey)
+ {
+ // Arrange
+ Guid nodeToRestore = Child3.Key;
+
+ // Move node to recycle bin
+ await ContentEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey);
+
+ // Act
+ await ContentEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey);
+
+ // Assert
+ if (targetParentKey is null)
+ {
+ DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys);
+ Assert.AreEqual(nodeToRestore, rootKeys.Last());
+ }
+ else
+ {
+ DocumentNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys);
+ Assert.AreEqual(nodeToRestore, childrenKeys.Last());
+ }
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs
new file mode 100644
index 0000000000..7d1e113974
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.Sort.cs
@@ -0,0 +1,199 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.ContentEditing;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
+
+public partial class DocumentNavigationServiceTests
+{
+ [Test]
+ public async Task Structure_Updates_When_Reversing_Children_Sort_Order()
+ {
+ // Arrange
+ Guid nodeToSortItsChildren = Root.Key;
+ DocumentNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable initialChildrenKeys);
+ List initialChildrenKeysList = initialChildrenKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(3, initialChildrenKeysList.Count);
+
+ // Assert initial order
+ Assert.AreEqual(Child1.Key, initialChildrenKeysList[0]);
+ Assert.AreEqual(Child2.Key, initialChildrenKeysList[1]);
+ Assert.AreEqual(Child3.Key, initialChildrenKeysList[2]);
+ });
+
+ IEnumerable sortingModels = initialChildrenKeys
+ .Reverse()
+ .Select((key, index) => new SortingModel { Key = key, SortOrder = index });
+
+ // Act
+ await ContentEditingService.SortAsync(nodeToSortItsChildren, sortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable sortedChildrenKeys);
+ List sortedChildrenKeysList = sortedChildrenKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(Child3.Key, sortedChildrenKeysList[0]);
+ Assert.AreEqual(Child2.Key, sortedChildrenKeysList[1]);
+ Assert.AreEqual(Child1.Key, sortedChildrenKeysList[2]);
+ });
+
+ var expectedChildrenKeysList = initialChildrenKeys.Reverse().ToList();
+
+ // Check that the order matches what is expected
+ Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList));
+ }
+
+ [Test]
+ public async Task Structure_Updates_When_Children_Have_Custom_Sort_Order()
+ {
+ // Arrange
+ Guid node = Root.Key;
+ var customSortingModels = new List
+ {
+ new() { Key = Child2.Key, SortOrder = 0 }, // Move Child 2 to the position 1
+ new() { Key = Child3.Key, SortOrder = 1 }, // Move Child 3 to the position 2
+ new() { Key = Child1.Key, SortOrder = 2 }, // Move Child 1 to the position 3
+ };
+
+ // Act
+ await ContentEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetChildrenKeys(node, out IEnumerable sortedChildrenKeys);
+ List sortedChildrenKeysList = sortedChildrenKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(Child2.Key, sortedChildrenKeysList[0]);
+ Assert.AreEqual(Child3.Key, sortedChildrenKeysList[1]);
+ Assert.AreEqual(Child1.Key, sortedChildrenKeysList[2]);
+ });
+
+ var expectedChildrenKeysList = customSortingModels
+ .OrderBy(x => x.SortOrder)
+ .Select(x => x.Key)
+ .ToList();
+
+ // Check that the order matches what is expected
+ Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList));
+ }
+
+ [Test]
+ public async Task Structure_Updates_When_Sorting_Items_At_Root()
+ {
+ // Arrange
+ var anotherRootCreateModel = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey);
+ await ContentEditingService.CreateAsync(anotherRootCreateModel, Constants.Security.SuperUserKey);
+ DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys);
+
+ var sortingModels = initialRootKeys
+ .Reverse()
+ .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index });
+
+ // Act
+ await ContentEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable sortedRootKeys);
+
+ var expectedRootKeysList = initialRootKeys.Reverse().ToList();
+
+ // Check that the order matches what is expected
+ Assert.IsTrue(expectedRootKeysList.SequenceEqual(sortedRootKeys));
+ }
+
+ [Test]
+ public async Task Descendants_Are_Returned_In_Correct_Order_After_Children_Are_Reordered()
+ {
+ // Arrange
+ Guid node = Root.Key;
+ DocumentNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable initialDescendantsKeys);
+
+ var customSortingModels = new List
+ {
+ new() { Key = Child3.Key, SortOrder = 0 }, // Move Child 3 to the position 1
+ new() { Key = Child1.Key, SortOrder = 1 }, // Move Child 1 to the position 2
+ new() { Key = Child2.Key, SortOrder = 2 }, // Move Child 2 to the position 3
+ };
+
+ var expectedDescendantsOrder = new List
+ {
+ Child3.Key, Grandchild4.Key, // Child 3 and its descendants
+ Child1.Key, Grandchild1.Key, Grandchild2.Key, // Child 1 and its descendants
+ Child2.Key, Grandchild3.Key, GreatGrandchild1.Key, // Child 2 and its descendants
+ };
+
+ // Act
+ await ContentEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable updatedDescendantsKeys);
+ List updatedDescendantsKeysList = updatedDescendantsKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(initialDescendantsKeys.SequenceEqual(updatedDescendantsKeysList));
+ Assert.IsTrue(expectedDescendantsOrder.SequenceEqual(updatedDescendantsKeysList));
+ });
+ }
+
+ [Test]
+ [TestCase(1, 2, 0, new[] { "B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE" })] // Custom sort order: Child 3, Child 1, Child 2; Expected order: Child 3, Child 1
+ [TestCase(0, 1, 2, new[] { "C6173927-0C59-4778-825D-D7B9F45D8DDE", "B606E3FF-E070-4D46-8CB9-D31352029FDF" })] // Custom sort order: Child 1, Child 2, Child 3; Expected order: Child 1, Child 3
+ [TestCase(2, 0, 1, new[] { "B606E3FF-E070-4D46-8CB9-D31352029FDF", "C6173927-0C59-4778-825D-D7B9F45D8DDE" })] // Custom sort order: Child 2, Child 3, Child 1; Expected order: Child 3, Child 1
+ public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting(int sortOrder1, int sortOrder2, int sortOrder3, string[] expectedSiblings)
+ {
+ // Arrange
+ Guid node = Child2.Key;
+
+ var customSortingModels = new List
+ {
+ new() { Key = Child1.Key, SortOrder = sortOrder1 }, // Move Child 1 to the position sortOrder1
+ new() { Key = Child2.Key, SortOrder = sortOrder2 }, // Move Child 2 to the position sortOrder2
+ new() { Key = Child3.Key, SortOrder = sortOrder3 }, // Move Child 3 to the position sortOrder3
+ };
+
+ Guid[] expectedSiblingsOrder = Array.ConvertAll(expectedSiblings, Guid.Parse);
+
+ // Act
+ await ContentEditingService.SortAsync(Root.Key, customSortingModels, Constants.Security.SuperUserKey); // Using the parent key here
+
+ // Assert
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys);
+ var sortedSiblingsKeysList = sortedSiblingsKeys.ToList();
+
+ Assert.IsTrue(expectedSiblingsOrder.SequenceEqual(sortedSiblingsKeysList));
+ }
+
+ [Test]
+ public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting_At_Root()
+ {
+ // Arrange
+ Guid node = Root.Key;
+ var anotherRootCreateModel1 = CreateContentCreateModel("Root 2", Guid.NewGuid(), Constants.System.RootKey);
+ await ContentEditingService.CreateAsync(anotherRootCreateModel1, Constants.Security.SuperUserKey);
+ var anotherRootCreateModel2 = CreateContentCreateModel("Root 3", Guid.NewGuid(), Constants.System.RootKey);
+ await ContentEditingService.CreateAsync(anotherRootCreateModel2, Constants.Security.SuperUserKey);
+ DocumentNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys);
+
+ var sortingModels = initialRootKeys
+ .Reverse()
+ .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index });
+
+ var expectedSiblingsKeysList = initialRootKeys.Reverse().Where(k => k != node).ToList();
+
+ // Act
+ await ContentEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ DocumentNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys);
+ var sortedSiblingsKeysList = sortedSiblingsKeys.ToList();
+
+ Assert.IsTrue(expectedSiblingsKeysList.SequenceEqual(sortedSiblingsKeysList));
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs
index d4b02a8755..5ef6de9a92 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentUrlServiceTest.cs
@@ -63,9 +63,9 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent
{
var isoCode = (await LanguageService.GetDefaultLanguageAsync()).IsoCode;
- var actual = DocumentUrlService.GetUrlSegment(Trashed.Key, isoCode, true);
+ Assert.IsNull(DocumentUrlService.GetUrlSegment(Trashed.Key, isoCode, true));
+ Assert.IsNull(DocumentUrlService.GetUrlSegment(Trashed.Key, isoCode, false));
- Assert.IsNull(actual);
}
//TODO test with the urlsegment property value!
@@ -120,9 +120,40 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent
[Test]
public void No_Published_Route_when_not_published()
{
+ Assert.IsNotNull(DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, true));
Assert.IsNull(DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, false));
}
+ [Test]
+ public void Unpublished_Pages_Are_not_available()
+ {
+ //Arrange
+ ContentService.PublishBranch(Textpage, true, new[] { "*" });
+
+ Assert.Multiple(() =>
+ {
+ Assert.IsNotNull(DocumentUrlService.GetDocumentKeyByRoute("/", "en-US", null, true));
+ Assert.IsNotNull(DocumentUrlService.GetDocumentKeyByRoute("/", "en-US", null, false));
+ Assert.IsNotNull(DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, true));
+ Assert.IsNotNull(DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, false));
+ });
+
+ //Act
+ ContentService.Unpublish(Textpage );
+
+ Assert.Multiple(() =>
+ {
+ //The unpublished page self
+ Assert.IsNotNull(DocumentUrlService.GetDocumentKeyByRoute("/", "en-US", null, true));
+ Assert.IsNull(DocumentUrlService.GetDocumentKeyByRoute("/", "en-US", null, false));
+
+ //A descendant of the unpublished page
+ Assert.IsNotNull(DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, true));
+ Assert.IsNull(DocumentUrlService.GetDocumentKeyByRoute("/text-page-1", "en-US", null, false));
+ });
+
+ }
+
[Test]
[TestCase("/text-page-1/sub-page-1", "en-US", true, ExpectedResult = "DF49F477-12F2-4E33-8563-91A7CC1DCDBB")]
@@ -181,10 +212,24 @@ public class DocumentUrlServiceTest : UmbracoIntegrationTestWithContent
// Publish both the main root and the second root with descendants
if (loadDraft is false)
{
+
ContentService.PublishBranch(Textpage, true, new[] { "*" });
ContentService.PublishBranch(secondRoot, true, new[] { "*" });
}
return DocumentUrlService.GetDocumentKeyByRoute(route, isoCode, null, loadDraft)?.ToString()?.ToUpper();
}
+
+ //TODO test cases:
+ // - Find the root, when a domain is set
+ // - Find a nested child, when a domain is set
+
+ // - Find the root when no domain is set and hideTopLevelNodeFromPath is true
+ // - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is true
+ // - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is true
+ // - Find the root when no domain is set and hideTopLevelNodeFromPath is false
+ // - Find a nested child of item in the root top when no domain is set and hideTopLevelNodeFromPath is false
+ // - Find a nested child of item in the root bottom when no domain is set and hideTopLevelNodeFromPath is false
+
+ // - All of the above when having Constants.Conventions.Content.UrlName set to a value
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs
index 3111e5c8e5..9b1baa3c71 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Create.cs
@@ -11,7 +11,7 @@ public partial class MediaNavigationServiceTests
// Arrange
MediaNavigationQueryService.TryGetSiblingsKeys(Album.Key, out IEnumerable initialSiblingsKeys);
var initialRootNodeSiblingsCount = initialSiblingsKeys.Count();
- var createModel = CreateMediaCreateModel("Root Image", Guid.NewGuid(), ImageMediaType.Key);
+ var createModel = CreateMediaCreateModel("Album 2", Guid.NewGuid(), FolderMediaType.Key);
// Act
var createAttempt = await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
@@ -54,4 +54,32 @@ public partial class MediaNavigationServiceTests
Assert.IsTrue(childrenList.Contains(createdItemKey));
});
}
+
+ [Test]
+ [TestCase(null)] // Media root
+ [TestCase("1CD97C02-8534-4B72-AE9E-AE52EC94CF31")] // Album
+ [TestCase("139DC977-E50F-4382-9728-B278C4B7AC6A")] // Sub-album 1
+ [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564")] // Sub-album 2
+ [TestCase("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB")] // Sub-sub-album 1
+ public async Task Creating_Child_Media_Adds_It_As_The_Last_Child(Guid? parentKey)
+ {
+ // Arrange
+ Guid newNodeKey = Guid.NewGuid();
+ var createModel = CreateMediaCreateModel("Child Image", newNodeKey, ImageMediaType.Key, parentKey);
+
+ // Act
+ await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ // Assert
+ if (parentKey is null)
+ {
+ MediaNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys);
+ Assert.AreEqual(newNodeKey, rootKeys.Last());
+ }
+ else
+ {
+ MediaNavigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable childrenKeys);
+ Assert.AreEqual(newNodeKey, childrenKeys.Last());
+ }
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs
index 5eeadb2484..c728d6a6d3 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Delete.cs
@@ -37,4 +37,42 @@ public partial class MediaNavigationServiceTests
}
});
}
+
+ // TODO: Add more test cases
+ [Test]
+ public async Task Sort_Order_Of_Siblings_Updates_When_Deleting_Media_And_Adding_New_One()
+ {
+ // Arrange
+ Guid nodeToDelete = SubAlbum2.Key;
+ Guid node = Image1.Key;
+
+ // Act
+ await MediaEditingService.DeleteAsync(nodeToDelete, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion);
+ var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count);
+ Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterDeletionList[0]);
+ });
+
+ // Create a new sibling under the same parent
+ var key = Guid.NewGuid();
+ var createModel = CreateMediaCreateModel("Child Image", key, ImageMediaType.Key, Album.Key);
+ await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation);
+ var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList();
+
+ // Verify sibling order after creating the new media
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, siblingsKeysAfterCreationList.Count);
+ Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterCreationList[0]);
+ Assert.AreEqual(key, siblingsKeysAfterCreationList[1]);
+ });
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs
index 1677dcec45..a54455f630 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Move.cs
@@ -94,4 +94,68 @@ public partial class MediaNavigationServiceTests
Assert.AreEqual(beforeMoveDescendants, afterMoveDescendants);
});
}
+
+ [Test]
+ [TestCase(null)] // Media root
+ [TestCase("1CD97C02-8534-4B72-AE9E-AE52EC94CF31")] // Album
+ [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564")] // Sub-album 2
+ [TestCase("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB")] // Sub-sub-album 1
+ public async Task Moving_Media_Adds_It_Last(Guid? targetParentKey)
+ {
+ // Arrange
+ Guid nodeToMove = Image2.Key;
+
+ // Act
+ await MediaEditingService.MoveAsync(nodeToMove, targetParentKey, Constants.Security.SuperUserKey);
+
+ // Assert
+ if (targetParentKey is null)
+ {
+ MediaNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys);
+ Assert.AreEqual(nodeToMove, rootKeys.Last());
+ }
+ else
+ {
+ MediaNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys);
+ Assert.AreEqual(nodeToMove, childrenKeys.Last());
+ }
+ }
+
+ // TODO: Add more test cases
+ [Test]
+ public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Media_And_Adding_New_One()
+ {
+ // Arrange
+ Guid nodeToMove = SubAlbum2.Key;
+ Guid node = Image1.Key;
+
+ // Act
+ await MediaEditingService.MoveAsync(nodeToMove, null, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterMoving);
+ var siblingsKeysAfterMovingList = siblingsKeysAfterMoving.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(1, siblingsKeysAfterMovingList.Count);
+ Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterMovingList[0]);
+ });
+
+ // Create a new sibling under the same parent
+ var key = Guid.NewGuid();
+ var createModel = CreateMediaCreateModel("Child Image", key, ImageMediaType.Key, Album.Key);
+ await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation);
+ var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList();
+
+ // Verify sibling order after creating the new media
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, siblingsKeysAfterCreationList.Count);
+ Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterMovingList[0]);
+ Assert.AreEqual(key, siblingsKeysAfterCreationList[1]);
+ });
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs
index f54eae0924..244e2be5dc 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.MoveToRecycleBin.cs
@@ -45,4 +45,74 @@ public partial class MediaNavigationServiceTests
Assert.AreEqual(beforeMoveRecycleBinSiblingsCount + 1, afterMoveRecycleBinSiblingsCount);
});
}
+
+ // TODO: Add more test cases
+ [Test]
+ public async Task Sort_Order_Of_Siblings_Updates_When_Moving_Media_To_Recycle_Bin_And_Adding_New_One()
+ {
+ // Arrange
+ Guid nodeToMoveToRecycleBin = SubAlbum2.Key;
+ Guid node = Image1.Key;
+
+ // Act
+ await MediaEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterDeletion);
+ var siblingsKeysAfterDeletionList = siblingsKeysAfterDeletion.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(1, siblingsKeysAfterDeletionList.Count);
+ Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterDeletionList[0]);
+ });
+
+ // Create a new sibling under the same parent
+ var key = Guid.NewGuid();
+ var createModel = CreateMediaCreateModel("Child Image", key, ImageMediaType.Key, Album.Key);
+ await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable siblingsKeysAfterCreation);
+ var siblingsKeysAfterCreationList = siblingsKeysAfterCreation.ToList();
+
+ // Verify sibling order after creating the new media
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(2, siblingsKeysAfterCreationList.Count);
+ Assert.AreEqual(SubAlbum1.Key, siblingsKeysAfterDeletionList[0]);
+ Assert.AreEqual(key, siblingsKeysAfterCreationList[1]);
+ });
+ }
+
+ [Test]
+ public async Task Sort_Order_Of_Chilldren_Is_Maintained_When_Moving_Media_To_Recycle_Bin()
+ {
+ // Arrange
+ Guid nodeToMoveToRecycleBin = SubAlbum1.Key;
+
+ // Create a new grandchild under Child1
+ var key = Guid.NewGuid();
+ var createModel = CreateMediaCreateModel("Image 5", key, ImageMediaType.Key, nodeToMoveToRecycleBin);
+ await MediaEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey);
+
+ MediaNavigationQueryService.TryGetChildrenKeys(nodeToMoveToRecycleBin, out IEnumerable childrenKeysBeforeDeletion);
+ var childrenKeysBeforeDeletionList = childrenKeysBeforeDeletion.ToList();
+
+ // Act
+ await MediaEditingService.MoveToRecycleBinAsync(nodeToMoveToRecycleBin, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetChildrenKeysInBin(nodeToMoveToRecycleBin, out IEnumerable childrenKeysAfterDeletion);
+ var childrenKeysAfterDeletionList = childrenKeysAfterDeletion.ToList();
+
+ // Verify children order in the bin
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(3, childrenKeysAfterDeletionList.Count);
+ Assert.AreEqual(Image2.Key, childrenKeysAfterDeletionList[0]);
+ Assert.AreEqual(Image3.Key, childrenKeysAfterDeletionList[1]);
+ Assert.AreEqual(key, childrenKeysAfterDeletionList[2]);
+ Assert.IsTrue(childrenKeysBeforeDeletionList.SequenceEqual(childrenKeysAfterDeletionList));
+ });
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs
index 22e5e3d799..c0ff408125 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Restore.cs
@@ -50,4 +50,33 @@ public partial class MediaNavigationServiceTests
Assert.AreEqual(targetParentKey, restoredItemParentKey);
});
}
+
+ [Test]
+ [TestCase(null)] // Media root
+ [TestCase("139DC977-E50F-4382-9728-B278C4B7AC6A")] // Sub-album 1
+ [TestCase("DBCAFF2F-BFA4-4744-A948-C290C432D564")] // Sub-album 2
+ [TestCase("E0B23D56-9A0E-4FC4-BD42-834B73B4C7AB")] // Sub-sub-album 1
+ public async Task Restoring_Content_Adds_It_Last(Guid? targetParentKey)
+ {
+ // Arrange
+ Guid nodeToRestore = Image1.Key;
+
+ // Move node to recycle bin
+ await MediaEditingService.MoveToRecycleBinAsync(nodeToRestore, Constants.Security.SuperUserKey);
+
+ // Act
+ await MediaEditingService.RestoreAsync(nodeToRestore, targetParentKey, Constants.Security.SuperUserKey);
+
+ // Assert
+ if (targetParentKey is null)
+ {
+ MediaNavigationQueryService.TryGetRootKeys(out IEnumerable rootKeys);
+ Assert.AreEqual(nodeToRestore, rootKeys.Last());
+ }
+ else
+ {
+ MediaNavigationQueryService.TryGetChildrenKeys(targetParentKey.Value, out IEnumerable childrenKeys);
+ Assert.AreEqual(nodeToRestore, childrenKeys.Last());
+ }
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Sort.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Sort.cs
new file mode 100644
index 0000000000..99f6e8f258
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MediaNavigationServiceTests.Sort.cs
@@ -0,0 +1,199 @@
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models.ContentEditing;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
+
+public partial class MediaNavigationServiceTests
+{
+ [Test]
+ public async Task Structure_Updates_When_Reversing_Children_Sort_Order()
+ {
+ // Arrange
+ Guid nodeToSortItsChildren = Album.Key;
+ MediaNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable initialChildrenKeys);
+ List initialChildrenKeysList = initialChildrenKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(3, initialChildrenKeysList.Count);
+
+ // Assert initial order
+ Assert.AreEqual(Image1.Key, initialChildrenKeysList[0]);
+ Assert.AreEqual(SubAlbum1.Key, initialChildrenKeysList[1]);
+ Assert.AreEqual(SubAlbum2.Key, initialChildrenKeysList[2]);
+ });
+
+ IEnumerable sortingModels = initialChildrenKeys
+ .Reverse()
+ .Select((key, index) => new SortingModel { Key = key, SortOrder = index });
+
+ // Act
+ await MediaEditingService.SortAsync(nodeToSortItsChildren, sortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetChildrenKeys(nodeToSortItsChildren, out IEnumerable sortedChildrenKeys);
+ List sortedChildrenKeysList = sortedChildrenKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(SubAlbum2.Key, sortedChildrenKeysList[0]);
+ Assert.AreEqual(SubAlbum1.Key, sortedChildrenKeysList[1]);
+ Assert.AreEqual(Image1.Key, sortedChildrenKeysList[2]);
+ });
+
+ var expectedChildrenKeysList = initialChildrenKeys.Reverse().ToList();
+
+ // Check that the order matches what is expected
+ Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList));
+ }
+
+ [Test]
+ public async Task Structure_Updates_When_Children_Have_Custom_Sort_Order()
+ {
+ // Arrange
+ Guid node = Album.Key;
+ var customSortingModels = new List
+ {
+ new() { Key = SubAlbum1.Key, SortOrder = 0 }, // Move Sub-album 1 to the position 1
+ new() { Key = SubAlbum2.Key, SortOrder = 1 }, // Move Sub-album 2 to the position 2
+ new() { Key = Image1.Key, SortOrder = 2 }, // Move Image 1 to the position 3
+ };
+
+ // Act
+ await MediaEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetChildrenKeys(node, out IEnumerable sortedChildrenKeys);
+ List sortedChildrenKeysList = sortedChildrenKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(SubAlbum1.Key, sortedChildrenKeysList[0]);
+ Assert.AreEqual(SubAlbum2.Key, sortedChildrenKeysList[1]);
+ Assert.AreEqual(Image1.Key, sortedChildrenKeysList[2]);
+ });
+
+ var expectedChildrenKeysList = customSortingModels
+ .OrderBy(x => x.SortOrder)
+ .Select(x => x.Key)
+ .ToList();
+
+ // Check that the order matches what is expected
+ Assert.IsTrue(expectedChildrenKeysList.SequenceEqual(sortedChildrenKeysList));
+ }
+
+ [Test]
+ public async Task Structure_Updates_When_Sorting_Items_At_Root()
+ {
+ // Arrange
+ var anotherRootCreateModel = CreateMediaCreateModel("Album 2", Guid.NewGuid(), FolderMediaType.Key, Constants.System.RootKey);
+ await MediaEditingService.CreateAsync(anotherRootCreateModel, Constants.Security.SuperUserKey);
+ MediaNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys);
+
+ var sortingModels = initialRootKeys
+ .Reverse()
+ .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index });
+
+ // Act
+ await MediaEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetRootKeys(out IEnumerable sortedRootKeys);
+
+ var expectedRootKeysList = initialRootKeys.Reverse().ToList();
+
+ // Check that the order matches what is expected
+ Assert.IsTrue(expectedRootKeysList.SequenceEqual(sortedRootKeys));
+ }
+
+ [Test]
+ public async Task Descendants_Are_Returned_In_Correct_Order_After_Children_Are_Reordered()
+ {
+ // Arrange
+ Guid node = Album.Key;
+ MediaNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable initialDescendantsKeys);
+
+ var customSortingModels = new List
+ {
+ new() { Key = SubAlbum2.Key, SortOrder = 0 }, // Move Sub-album 2 to the position 1
+ new() { Key = Image1.Key, SortOrder = 1 }, // Move Image 1 to the position 2
+ new() { Key = SubAlbum1.Key, SortOrder = 2 }, // Move Sub-album 1 to the position 3
+ };
+
+ var expectedDescendantsOrder = new List
+ {
+ SubAlbum2.Key, SubSubAlbum1.Key, Image4.Key, // Sub-album 2 and its descendants
+ Image1.Key, // Image 1
+ SubAlbum1.Key, Image2.Key, Image3.Key, // Sub-album 1 and its descendants
+ };
+
+ // Act
+ await MediaEditingService.SortAsync(node, customSortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetDescendantsKeys(node, out IEnumerable updatedDescendantsKeys);
+ List updatedDescendantsKeysList = updatedDescendantsKeys.ToList();
+
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(initialDescendantsKeys.SequenceEqual(updatedDescendantsKeysList));
+ Assert.IsTrue(expectedDescendantsOrder.SequenceEqual(updatedDescendantsKeysList));
+ });
+ }
+
+ [Test]
+ [TestCase(1, 2, 0, new[] { "DBCAFF2F-BFA4-4744-A948-C290C432D564", "03976EBE-A942-4F24-9885-9186E99AEF7C" })] // Custom sort order: Sub-album 2, Image 1, Sub-album 1; Expected order: Sub-album 2, Image 1
+ [TestCase(0, 1, 2, new[] { "03976EBE-A942-4F24-9885-9186E99AEF7C", "DBCAFF2F-BFA4-4744-A948-C290C432D564" })] // Custom sort order: Image 1, Sub-album 1, Sub-album 2; Expected order: Image 1, Sub-album 2
+ [TestCase(2, 0, 1, new[] { "DBCAFF2F-BFA4-4744-A948-C290C432D564", "03976EBE-A942-4F24-9885-9186E99AEF7C" })] // Custom sort order: Sub-album 1, Sub-album 2, Image 1; Expected order: Sub-album 2, Image 1
+ public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting(int sortOrder1, int sortOrder2, int sortOrder3, string[] expectedSiblings)
+ {
+ // Arrange
+ Guid node = SubAlbum1.Key;
+
+ var customSortingModels = new List
+ {
+ new() { Key = Image1.Key, SortOrder = sortOrder1 }, // Move Image 1 to the position sortOrder1
+ new() { Key = SubAlbum1.Key, SortOrder = sortOrder2 }, // Move Sub-album 1 to the position sortOrder2
+ new() { Key = SubAlbum2.Key, SortOrder = sortOrder3 }, // Move Sub-album 2 to the position sortOrder3
+ };
+
+ Guid[] expectedSiblingsOrder = Array.ConvertAll(expectedSiblings, Guid.Parse);
+
+ // Act
+ await MediaEditingService.SortAsync(Album.Key, customSortingModels, Constants.Security.SuperUserKey); // Using the parent key here
+
+ // Assert
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys);
+ var sortedSiblingsKeysList = sortedSiblingsKeys.ToList();
+
+ Assert.IsTrue(expectedSiblingsOrder.SequenceEqual(sortedSiblingsKeysList));
+ }
+
+ [Test]
+ public async Task Siblings_Are_Returned_In_Correct_Order_After_Sorting_At_Root()
+ {
+ // Arrange
+ Guid node = Album.Key;
+ var anotherRootCreateModel1 = CreateMediaCreateModel("Album 2", Guid.NewGuid(), FolderMediaType.Key, Constants.System.RootKey);
+ await MediaEditingService.CreateAsync(anotherRootCreateModel1, Constants.Security.SuperUserKey);
+ var anotherRootCreateModel2 = CreateMediaCreateModel("Album 3", Guid.NewGuid(), FolderMediaType.Key, Constants.System.RootKey);
+ await MediaEditingService.CreateAsync(anotherRootCreateModel2, Constants.Security.SuperUserKey);
+ MediaNavigationQueryService.TryGetRootKeys(out IEnumerable initialRootKeys);
+
+ var sortingModels = initialRootKeys
+ .Reverse()
+ .Select((rootKey, index) => new SortingModel { Key = rootKey, SortOrder = index });
+
+ var expectedSiblingsKeysList = initialRootKeys.Reverse().Where(k => k != node).ToList();
+
+ // Act
+ await MediaEditingService.SortAsync(Constants.System.RootKey, sortingModels, Constants.Security.SuperUserKey);
+
+ // Assert
+ MediaNavigationQueryService.TryGetSiblingsKeys(node, out IEnumerable sortedSiblingsKeys);
+ var sortedSiblingsKeysList = sortedSiblingsKeys.ToList();
+
+ Assert.IsTrue(expectedSiblingsKeysList.SequenceEqual(sortedSiblingsKeysList));
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs
new file mode 100644
index 0000000000..f4d4d0f48d
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/PublishStatusServiceTest.cs
@@ -0,0 +1,216 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NUnit.Framework;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Handlers;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Persistence.Repositories;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Services.Navigation;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Infrastructure.DependencyInjection;
+using Umbraco.Cms.Infrastructure.Examine;
+using Umbraco.Cms.Infrastructure.Examine.DependencyInjection;
+using Umbraco.Cms.Infrastructure.HostedServices;
+using Umbraco.Cms.Infrastructure.Search;
+using Umbraco.Cms.Tests.Common.Attributes;
+using Umbraco.Cms.Tests.Common.Builders;
+using Umbraco.Cms.Tests.Common.Testing;
+using Umbraco.Cms.Tests.Integration.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Scoping;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services;
+
+[TestFixture]
+[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Mock)]
+public class PublishStatusServiceTest : UmbracoIntegrationTestWithContent
+{
+ protected IPublishStatusQueryService PublishStatusQueryService => GetRequiredService();
+
+ private const string DefaultCulture = "en-US";
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.Services.AddUnique();
+ builder.AddNotificationHandler();
+ }
+
+ [Test]
+ public async Task InitializeAsync_loads_from_db()
+ {
+ var randomCulture = "da-DK";
+ var sut = new PublishStatusService(
+ GetRequiredService>(),
+ GetRequiredService(),
+ GetRequiredService()
+ );
+
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, DefaultCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, DefaultCulture));
+
+ Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, DefaultCulture));
+
+ Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, randomCulture));
+
+ Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, randomCulture));
+ });
+
+ // Act
+ var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" });
+ await sut.InitializeAsync(CancellationToken.None);
+
+ Assert.Multiple(() =>
+ {
+ Assert.IsTrue(publishResults.All(x=>x.Result == PublishResultType.SuccessPublish));
+ Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
+ Assert.IsTrue(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsTrue(sut.IsDocumentPublished(Subpage.Key, DefaultCulture));
+ Assert.IsTrue(sut.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsTrue(sut.IsDocumentPublished(Subpage3.Key, DefaultCulture));
+
+ Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, DefaultCulture));
+
+ Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage3.Key, randomCulture));
+
+ Assert.IsFalse(sut.IsDocumentPublished(Trashed.Key, randomCulture));
+ });
+ }
+
+ [Test]
+ public async Task AddOrUpdateStatusWithDescendantsAsync()
+ {
+ var randomCulture = "da-DK";
+ var sut = new PublishStatusService(
+ GetRequiredService>(),
+ GetRequiredService(),
+ GetRequiredService()
+ );
+
+ Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
+
+ // Act
+ var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" });
+ await sut.AddOrUpdateStatusWithDescendantsAsync(Textpage.Key, CancellationToken.None);
+ Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
+ Assert.IsTrue(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Updated due to being an descendant
+ Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture)); // Do not exist
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture)); // Do not exist
+ }
+
+ [Test]
+ public async Task AddOrUpdateStatusAsync()
+ {
+ var randomCulture = "da-DK";
+ var sut = new PublishStatusService(
+ GetRequiredService>(),
+ GetRequiredService(),
+ GetRequiredService()
+ );
+
+ Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
+
+ // Act
+ var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" });
+ await sut.AddOrUpdateStatusAsync(Textpage.Key, CancellationToken.None);
+ Assert.IsTrue(sut.IsDocumentPublished(Textpage.Key, DefaultCulture));
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, DefaultCulture)); // Not updated
+ Assert.IsFalse(sut.IsDocumentPublished(Textpage.Key, randomCulture)); // Do not exist
+ Assert.IsFalse(sut.IsDocumentPublished(Subpage.Key, randomCulture)); // Do not exist
+ }
+
+ [Test]
+ public void When_Nothing_is_publised_all_return_false()
+ {
+ var randomCulture = "da-DK";
+ Assert.Multiple(() =>
+ {
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, DefaultCulture));
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, DefaultCulture));
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, randomCulture));
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, randomCulture));
+ });
+
+ }
+
+ [Test]
+ public void Unpublish_leads_to_unpublised_in_this_service()
+ {
+ var grandchild = ContentBuilder.CreateSimpleContent(ContentType, "Grandchild", Subpage2.Id);
+
+ var contentSchedule = ContentScheduleCollection.CreateWithEntry(DateTime.Now.AddMinutes(-5), null);
+ ContentService.Save(grandchild, -1, contentSchedule);
+
+ var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" });
+ var randomCulture = "da-DK";
+
+ var subPage2FromDB = ContentService.GetById(Subpage2.Key);
+ var publishResult = ContentService.Unpublish(subPage2FromDB);
+ Assert.Multiple(() =>
+ {
+ Assert.IsTrue(publishResults.All(x=>x.Result == PublishResultType.SuccessPublish));
+ Assert.IsTrue(publishResult.Success);
+ Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(grandchild.Key, DefaultCulture)); // grandchild is still published, but it will not be routable
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(grandchild.Key, randomCulture));
+ });
+
+ }
+
+ [Test]
+ public void When_Branch_is_publised_default_language_return_true()
+ {
+ var publishResults = ContentService.PublishBranch(Textpage, true, new[] { "*" });
+ var randomCulture = "da-DK";
+ Assert.Multiple(() =>
+ {
+ Assert.IsTrue(publishResults.All(x=>x.Result == PublishResultType.SuccessPublish));
+ Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, DefaultCulture));
+ Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, DefaultCulture));
+ Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, DefaultCulture));
+ Assert.IsTrue(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, DefaultCulture));
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, DefaultCulture));
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Textpage.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage2.Key, randomCulture));
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Subpage3.Key, randomCulture));
+
+ Assert.IsFalse(PublishStatusQueryService.IsDocumentPublished(Trashed.Key, randomCulture));
+ });
+
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs
index 0b10332352..72f4502b1a 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs
@@ -28,6 +28,8 @@ public class BlockListPropertyEditorTests : UmbracoIntegrationTest
private PropertyEditorCollection PropertyEditorCollection => GetRequiredService();
+ private ILanguageService LanguageService => GetRequiredService();
+
[Test]
public async Task Can_Track_References()
{
@@ -147,9 +149,89 @@ public class BlockListPropertyEditorTests : UmbracoIntegrationTest
var tags = valueEditor.GetTags(content.GetValue("blocks"), null, null).ToArray();
Assert.AreEqual(3, tags.Length);
- Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One"));
- Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two"));
- Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three"));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One" && tag.LanguageId == null));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two" && tag.LanguageId == null));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three" && tag.LanguageId == null));
+ }
+
+ [Test]
+ public async Task Can_Track_Tags_For_Block_Level_Variance()
+ {
+ var result = await LanguageService.CreateAsync(
+ new Language("da-DK", "Danish"), Constants.Security.SuperUserKey);
+ Assert.IsTrue(result.Success);
+ var daDkId = result.Result.Id;
+
+ var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type");
+ elementType.IsElement = true;
+ elementType.Variations = ContentVariation.Culture;
+ elementType.PropertyTypes.First(p => p.Alias == "tags").Variations = ContentVariation.Culture;
+ ContentTypeService.Save(elementType);
+
+ var blockListContentType = await CreateBlockListContentType(elementType);
+ blockListContentType.Variations = ContentVariation.Culture;
+ ContentTypeService.Save(blockListContentType);
+
+ var contentElementKey = Guid.NewGuid();
+ var blockListValue = new BlockListValue
+ {
+ Layout = new Dictionary>
+ {
+ {
+ Constants.PropertyEditors.Aliases.BlockList,
+ new IBlockLayoutItem[]
+ {
+ new BlockListLayoutItem { ContentKey = contentElementKey }
+ }
+ }
+ },
+ ContentData =
+ [
+ new()
+ {
+ Key = contentElementKey,
+ ContentTypeAlias = elementType.Alias,
+ ContentTypeKey = elementType.Key,
+ Values =
+ [
+ new ()
+ {
+ Alias = "tags",
+ // this is a little skewed, but the tags editor expects a serialized array of strings
+ Value = JsonSerializer.Serialize(new[] { "Tag One EN", "Tag Two EN", "Tag Three EN" }),
+ Culture = "en-US"
+ },
+ new ()
+ {
+ Alias = "tags",
+ // this is a little skewed, but the tags editor expects a serialized array of strings
+ Value = JsonSerializer.Serialize(new[] { "Tag One DA", "Tag Two DA", "Tag Three DA" }),
+ Culture = "da-DK"
+ }
+ ]
+ }
+ ]
+ };
+ var blocksPropertyValue = JsonSerializer.Serialize(blockListValue);
+
+ var content = new ContentBuilder()
+ .WithContentType(blockListContentType)
+ .WithCultureName("en-US", "My Blocks EN")
+ .WithCultureName("da-DK", "My Blocks DA")
+ .WithPropertyValues(new { blocks = blocksPropertyValue })
+ .Build();
+ ContentService.Save(content);
+
+ var valueEditor = await GetValueEditor(blockListContentType);
+
+ var tags = valueEditor.GetTags(content.GetValue("blocks"), null, null).ToArray();
+ Assert.AreEqual(6, tags.Length);
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One EN" && tag.LanguageId == 1));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two EN" && tag.LanguageId == 1));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three EN" && tag.LanguageId == 1));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One DA" && tag.LanguageId == daDkId));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two DA" && tag.LanguageId == daDkId));
+ Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three DA" && tag.LanguageId == daDkId));
}
[Test]
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
index 92dbdc3715..24ff0983e2 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj
@@ -194,6 +194,9 @@
DocumentNavigationServiceTests.cs
+
+ DocumentNavigationServiceTests.cs
+
DocumentNavigationServiceTests.cs
@@ -215,9 +218,15 @@
MediaNavigationServiceTests.cs
+
+ DocumentNavigationServiceTests.cs
+
MediaNavigationServiceTests.cs
+
+ MediaNavigationServiceTests.cs
+
MediaNavigationServiceTests.cs
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs
index 48d5c3e0c6..4fe4c34b6e 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs
@@ -39,9 +39,8 @@ public class DataValueEditorReuseTests
var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of());
_dataValueEditorFactoryMock
.Setup(m =>
- m.Create(It.IsAny(), It.IsAny>()))
+ m.Create(It.IsAny>()))
.Returns(() => new BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor(
- new DataEditorAttribute("a"),
new BlockListEditorDataConverter(Mock.Of()),
_propertyEditorCollection,
_dataValueReferenceFactories,
@@ -51,9 +50,9 @@ public class DataValueEditorReuseTests
Mock.Of>(),
Mock.Of