Introduce INavigationService for in-memory navigation data (#16818)
* Tests * Remove props and use local vars * Adding preliminary navigation service and content implementation * Adding preliminary unit tests * Change from async methods * Refactor GetParentKey to TryGetParentKey * Refactor GetChildrenKeys to TryGetChildrenKeys * Refactor GetDescendantsKeys to TryGetDescendantsKeys * Refactor GetAncestorsKeys to TryGetAncestorsKeys * Refactor GetSiblingsKeys to TryGetSiblingsKeys * Refactor TryGetChildrenKeys * Initial integration tests * Use ContentEditingService instead of ContentService * Remove INavigationService.Copy implementation and unit tests * Rename var * Adding clarification * Initial ContentNavigationRepository * Initial NavigationFactory * Remove filtering from factory * NavigationRepository and implementation * InitializationService responsible for seeding the in-memory structure * Register repository and service * Adding NavigationDto and NavigationNode * Adding INavigationService dependency and Enlist updating navigation structure actions * Documentation * Adding tests for removing descendants as well * Changed to ConcurrentDictionary * Remove keys comments for tests * Adding documentation * Forgotten ConcurrentDictionary change * Isolating the operations on the model * Splitting the INavigationService to separate the querying from the managing functionality * Introducing specific navigation services for document, document recycle bin, media and media recycle bin * Making ContentNavigationService into a base as the functionality will be shared between the document, document recycle bin, media and media recycle bin services * Adding the implementations of document, document recycle bin, media and media recycle bin navigation services * Fixing comments * Initializing all 4 collections * Adapting the navigation unit tests to the base now * Adapting integration tests to specific navigation service * Adding test for rebuilding the structure * Adding implementation for Adding and Getting a node - needed for moving to and restoring from the recycle bin + tests * Updating the document navigation structure from the ContentService * Fix typo * Adding trashed items implementation in base - currently managing 2 structures * Removing no longer relevant GetNavigationNode and AddNavigationNode * Fix removing parent when child is removed supporting methods * Added restoring functionality * Adding Bin functionality to DocumentNavigationService * Removing Move signature from IDocumentNavigationService * Adding RecycleBin query and management services * Re-adding Move and removing GetNavigationNode and AddNavigationNode signatures from interface * Rebuilding bin structure using _documentNavigationService, instead of _documentRecycleBinNavigationService * Fixing test name * Adding more tests for remove * Adding tests for restore and removing ones for GetNavigationNode and AddNavigationNode * Remove comments * Removing document and media RecycleBinNavigationService and their interfaces * Adding media rebuild bin * Fixing initialization with correct interfaces * Removing RecycleBinNavigationServices' registration * Remove IDocumentRecycleBinNavigationService dependency * Updating in-memory nav structure when content updates happen * Adding the rest of the integration tests * Clean up IMediaNavigationService * Fix comments * Remove CustomTestSetup in integration tests as the structure is updated when content updates happen * Adding and fixing comments * Making RebuildBinAsync abstract as well * Adding DocumentNavigationServiceTestsBase * Splitting DocumentNavigationServiceTests into partial test classes * Cleaning up DocumentNavigationServiceTests since tests have been moved to specific partial classes * Reuse a method for creating content in tests * Change type in test base * Adding navigation structure updates in media service * Adding MediaNavigationServiceTestsBase * Adding integration tests for media nav str * Remove services as we will have more concrete ones * Add document and media IXNavigationQueryService and IXNavigationManagementService * Inject ManagementService in ContentService.cs and MediaService.cs * Change implementation to implement the new services + registration * Make classes sealed * Inject correct services in InitializationService * Using the right services in integration tests * Adding comments * Removing bin interfaces from main navigation ones * Rename Remove to MoveToBin * V14 QA added block list editor tests (#16862) * Added tests for blocklistEditor * Added more tets * Removed faker * Added blockTest * Updates * Added tests * Removed dependencies * Fixes * Clean up * Fixed naming * Cleaned up * Bumped version * Added missing semicolons * Added tags * Only runs the new tests * Updates * Bumped version * Fixed tests * Cleaned up * Updated version * Fixes, not done * Fixed tests * Bumped helpers * Bumped helpers * Fixed conflict * Fixed comment * Reverted to run smokeTests * Updated helpers * improve missingProperties data returned for missing propertie values (#16910) Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> * update backoffice submodule * Rename initialization service to initialization hosted service * Refactor repository to return a collection * Add interface for the NavigationDto * Add constants to bind property names between DTOs * Move factory and fix input type * Use constants for column names * Use factory from base --------- Co-authored-by: Andreas Zerbst <73799582+andr317c@users.noreply.github.com> Co-authored-by: Bjarke Berg <mail@bergmania.dk> Co-authored-by: Sven Geusens <sge@umbraco.dk> Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
eff520c7eb
commit
5a7d563b8a
@@ -39,6 +39,7 @@ using Umbraco.Cms.Core.Preview;
|
||||
using Umbraco.Cms.Core.Security.Authorization;
|
||||
using Umbraco.Cms.Core.Services.FileSystem;
|
||||
using Umbraco.Cms.Core.Services.ImportExport;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Core.Services.Querying.RecycleBin;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Core.Telemetry;
|
||||
@@ -338,8 +339,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
factory.GetRequiredService<ICoreScopeProvider>(),
|
||||
factory.GetRequiredService<ILoggerFactory>(),
|
||||
factory.GetRequiredService<IEventMessagesFactory>(),
|
||||
factory.GetRequiredService<IExternalLoginWithKeyRepository>()
|
||||
));
|
||||
factory.GetRequiredService<IExternalLoginWithKeyRepository>()));
|
||||
Services.AddUnique<ILogViewerService, LogViewerService>();
|
||||
Services.AddUnique<IExternalLoginWithKeyService>(factory => factory.GetRequiredService<ExternalLoginService>());
|
||||
Services.AddUnique<ILocalizedTextService>(factory => new LocalizedTextService(
|
||||
@@ -352,6 +352,12 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
Services.AddSingleton<CompiledPackageXmlParser>();
|
||||
Services.AddUnique<IPreviewTokenGenerator, NoopPreviewTokenGenerator>();
|
||||
Services.AddUnique<IPreviewService, PreviewService>();
|
||||
Services.AddUnique<DocumentNavigationService, DocumentNavigationService>();
|
||||
Services.AddUnique<IDocumentNavigationQueryService>(x => x.GetRequiredService<DocumentNavigationService>());
|
||||
Services.AddUnique<IDocumentNavigationManagementService>(x => x.GetRequiredService<DocumentNavigationService>());
|
||||
Services.AddUnique<MediaNavigationService, MediaNavigationService>();
|
||||
Services.AddUnique<IMediaNavigationQueryService>(x => x.GetRequiredService<MediaNavigationService>());
|
||||
Services.AddUnique<IMediaNavigationManagementService>(x => x.GetRequiredService<MediaNavigationService>());
|
||||
|
||||
// Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced
|
||||
Services.AddUnique<IHtmlSanitizer, NoopHtmlSanitizer>();
|
||||
|
||||
45
src/Umbraco.Core/Factories/NavigationFactory.cs
Normal file
45
src/Umbraco.Core/Factories/NavigationFactory.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Navigation;
|
||||
|
||||
namespace Umbraco.Cms.Core.Factories;
|
||||
|
||||
internal static class NavigationFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds a dictionary of NavigationNode objects from a given dataset.
|
||||
/// </summary>
|
||||
/// <param name="entities">The <see cref="INavigationModel" /> objects used to build the navigation nodes dictionary.</param>
|
||||
/// <returns>A dictionary of <see cref="NavigationNode" /> objects with key corresponding to their unique Guid.</returns>
|
||||
public static ConcurrentDictionary<Guid, NavigationNode> BuildNavigationDictionary(IEnumerable<INavigationModel> entities)
|
||||
{
|
||||
var nodesStructure = new ConcurrentDictionary<Guid, NavigationNode>();
|
||||
var entityList = entities.ToList();
|
||||
var idToKeyMap = entityList.ToDictionary(x => x.Id, x => x.Key);
|
||||
|
||||
foreach (INavigationModel entity in entityList)
|
||||
{
|
||||
var node = new NavigationNode(entity.Key);
|
||||
nodesStructure[entity.Key] = node;
|
||||
|
||||
// We don't set the parent for items under root, it will stay null
|
||||
if (entity.ParentId == -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (idToKeyMap.TryGetValue(entity.ParentId, out Guid parentKey) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
return nodesStructure;
|
||||
}
|
||||
}
|
||||
24
src/Umbraco.Core/Models/INavigationModel.cs
Normal file
24
src/Umbraco.Core/Models/INavigationModel.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace Umbraco.Cms.Core.Models;
|
||||
|
||||
public interface INavigationModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the integer identifier of the entity.
|
||||
/// </summary>
|
||||
int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Guid unique identifier of the entity.
|
||||
/// </summary>
|
||||
Guid Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the integer identifier of the parent entity.
|
||||
/// </summary>
|
||||
int ParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this entity is in the recycle bin.
|
||||
/// </summary>
|
||||
bool Trashed { get; set; }
|
||||
}
|
||||
30
src/Umbraco.Core/Models/Navigation/NavigationNode.cs
Normal file
30
src/Umbraco.Core/Models/Navigation/NavigationNode.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace Umbraco.Cms.Core.Models.Navigation;
|
||||
|
||||
public sealed class NavigationNode
|
||||
{
|
||||
private List<NavigationNode> _children;
|
||||
|
||||
public Guid Key { get; private set; }
|
||||
|
||||
public NavigationNode? Parent { get; private set; }
|
||||
|
||||
public IEnumerable<NavigationNode> Children => _children.AsEnumerable();
|
||||
|
||||
public NavigationNode(Guid key)
|
||||
{
|
||||
Key = key;
|
||||
_children = new List<NavigationNode>();
|
||||
}
|
||||
|
||||
public void AddChild(NavigationNode child)
|
||||
{
|
||||
child.Parent = this;
|
||||
_children.Add(child);
|
||||
}
|
||||
|
||||
public void RemoveChild(NavigationNode child)
|
||||
{
|
||||
_children.Remove(child);
|
||||
child.Parent = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
public interface INavigationRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a collection of content nodes as navigation models based on the object type key.
|
||||
/// </summary>
|
||||
/// <param name="objectTypeKey">The unique identifier for the object type.</param>
|
||||
/// <returns>A collection of navigation models.</returns>
|
||||
IEnumerable<INavigationModel> GetContentNodesByObjectType(Guid objectTypeKey);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a collection of trashed content nodes as navigation models based on the object type key.
|
||||
/// </summary>
|
||||
/// <param name="objectTypeKey">The unique identifier for the object type.</param>
|
||||
/// <returns>A collection of navigation models.</returns>
|
||||
IEnumerable<INavigationModel> GetTrashedContentNodesByObjectType(Guid objectTypeKey);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -33,25 +34,27 @@ public class ContentService : RepositoryService, IContentService
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly ICultureImpactFactory _cultureImpactFactory;
|
||||
private readonly IUserIdKeyResolver _userIdKeyResolver;
|
||||
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
|
||||
private IQuery<IContent>? _queryNotTrashed;
|
||||
|
||||
#region Constructors
|
||||
|
||||
public ContentService(
|
||||
ICoreScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IDocumentRepository documentRepository,
|
||||
IEntityRepository entityRepository,
|
||||
IAuditRepository auditRepository,
|
||||
IContentTypeRepository contentTypeRepository,
|
||||
IDocumentBlueprintRepository documentBlueprintRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
Lazy<IPropertyValidationService> propertyValidationService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
ICultureImpactFactory cultureImpactFactory,
|
||||
IUserIdKeyResolver userIdKeyResolver)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
ICoreScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IDocumentRepository documentRepository,
|
||||
IEntityRepository entityRepository,
|
||||
IAuditRepository auditRepository,
|
||||
IContentTypeRepository contentTypeRepository,
|
||||
IDocumentBlueprintRepository documentBlueprintRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
Lazy<IPropertyValidationService> propertyValidationService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
ICultureImpactFactory cultureImpactFactory,
|
||||
IUserIdKeyResolver userIdKeyResolver,
|
||||
IDocumentNavigationManagementService documentNavigationManagementService)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_documentRepository = documentRepository;
|
||||
_entityRepository = entityRepository;
|
||||
@@ -63,9 +66,43 @@ public class ContentService : RepositoryService, IContentService
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_cultureImpactFactory = cultureImpactFactory;
|
||||
_userIdKeyResolver = userIdKeyResolver;
|
||||
_documentNavigationManagementService = documentNavigationManagementService;
|
||||
_logger = loggerFactory.CreateLogger<ContentService>();
|
||||
}
|
||||
|
||||
[Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")]
|
||||
public ContentService(
|
||||
ICoreScopeProvider provider,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IDocumentRepository documentRepository,
|
||||
IEntityRepository entityRepository,
|
||||
IAuditRepository auditRepository,
|
||||
IContentTypeRepository contentTypeRepository,
|
||||
IDocumentBlueprintRepository documentBlueprintRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
Lazy<IPropertyValidationService> propertyValidationService,
|
||||
IShortStringHelper shortStringHelper,
|
||||
ICultureImpactFactory cultureImpactFactory,
|
||||
IUserIdKeyResolver userIdKeyResolver)
|
||||
: this(
|
||||
provider,
|
||||
loggerFactory,
|
||||
eventMessagesFactory,
|
||||
documentRepository,
|
||||
entityRepository,
|
||||
auditRepository,
|
||||
contentTypeRepository,
|
||||
documentBlueprintRepository,
|
||||
languageRepository,
|
||||
propertyValidationService,
|
||||
shortStringHelper,
|
||||
cultureImpactFactory,
|
||||
userIdKeyResolver,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationManagementService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")]
|
||||
public ContentService(
|
||||
ICoreScopeProvider provider,
|
||||
@@ -93,7 +130,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
propertyValidationService,
|
||||
shortStringHelper,
|
||||
cultureImpactFactory,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>())
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IDocumentNavigationManagementService>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1034,6 +1072,11 @@ public class ContentService : RepositoryService, IContentService
|
||||
// have always changed if it's been saved in the back office but that's not really fail safe.
|
||||
_documentRepository.Save(content);
|
||||
|
||||
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.Save-with-contentSchedule",
|
||||
() => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key));
|
||||
|
||||
if (contentSchedule != null)
|
||||
{
|
||||
_documentRepository.PersistContentSchedule(content, contentSchedule);
|
||||
@@ -1097,6 +1140,11 @@ public class ContentService : RepositoryService, IContentService
|
||||
content.WriterId = userId;
|
||||
|
||||
_documentRepository.Save(content);
|
||||
|
||||
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.Save",
|
||||
() => _documentNavigationManagementService.Add(content.Key, GetParent(content)?.Key));
|
||||
}
|
||||
|
||||
scope.Notifications.Publish(
|
||||
@@ -2288,6 +2336,26 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
|
||||
DoDelete(content);
|
||||
|
||||
if (content.Trashed)
|
||||
{
|
||||
// Updates in-memory navigation structure for recycle bin items
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.DeleteLocked-trashed",
|
||||
() => _documentNavigationManagementService.RemoveFromBin(content.Key));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Updates in-memory navigation structure for both documents and recycle bin items
|
||||
// as the item needs to be deleted whether it is in the recycle bin or not
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.DeleteLocked",
|
||||
() =>
|
||||
{
|
||||
_documentNavigationManagementService.MoveToBin(content.Key);
|
||||
_documentNavigationManagementService.RemoveFromBin(content.Key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
|
||||
@@ -2512,6 +2580,8 @@ public class ContentService : RepositoryService, IContentService
|
||||
// trash indicates whether we are trashing, un-trashing, or not changing anything
|
||||
private void PerformMoveLocked(IContent content, int parentId, IContent? parent, int userId, ICollection<(IContent, string)> moves, bool? trash)
|
||||
{
|
||||
// Needed to update the in-memory navigation structure
|
||||
var cameFromRecycleBin = content.ParentId == Constants.System.RecycleBinContent;
|
||||
content.WriterId = userId;
|
||||
content.ParentId = parentId;
|
||||
|
||||
@@ -2560,6 +2630,33 @@ public class ContentService : RepositoryService, IContentService
|
||||
}
|
||||
}
|
||||
while (total > pageSize);
|
||||
|
||||
if (parentId == Constants.System.RecycleBinContent)
|
||||
{
|
||||
// Updates in-memory navigation structure for both document items and recycle bin items
|
||||
// as we are moving to recycle bin
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-to-recycle-bin",
|
||||
() => _documentNavigationManagementService.MoveToBin(content.Key));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cameFromRecycleBin)
|
||||
{
|
||||
// Updates in-memory navigation structure for both document items and recycle bin items
|
||||
// as we are restoring from recycle bin
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked-restore",
|
||||
() => _documentNavigationManagementService.RestoreFromBin(content.Key, parent?.Key));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Updates in-memory navigation structure
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.PerformMoveLocked",
|
||||
() => _documentNavigationManagementService.Move(content.Key, parent?.Key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformMoveContentLocked(IContent content, int userId, bool? trash)
|
||||
@@ -2663,6 +2760,9 @@ public class ContentService : RepositoryService, IContentService
|
||||
{
|
||||
EventMessages eventMessages = EventMessagesFactory.Get();
|
||||
|
||||
// keep track of updates (copied item key and parent key) for the in-memory navigation structure
|
||||
var navigationUpdates = new List<Tuple<Guid, Guid?>>();
|
||||
|
||||
IContent copy = content.DeepCloneWithResetIdentities();
|
||||
copy.ParentId = parentId;
|
||||
|
||||
@@ -2699,6 +2799,9 @@ public class ContentService : RepositoryService, IContentService
|
||||
// save and flush because we need the ID for the recursive Copying events
|
||||
_documentRepository.Save(copy);
|
||||
|
||||
// store navigation update information for copied item
|
||||
navigationUpdates.Add(Tuple.Create(copy.Key, GetParent(copy)?.Key));
|
||||
|
||||
// add permissions
|
||||
if (currentPermissions.Count > 0)
|
||||
{
|
||||
@@ -2750,12 +2853,29 @@ public class ContentService : RepositoryService, IContentService
|
||||
// save and flush (see above)
|
||||
_documentRepository.Save(descendantCopy);
|
||||
|
||||
// store navigation update information for descendants
|
||||
navigationUpdates.Add(Tuple.Create(descendantCopy.Key, GetParent(descendantCopy)?.Key));
|
||||
|
||||
copies.Add(Tuple.Create(descendant, descendantCopy));
|
||||
idmap[descendant.Id] = descendantCopy.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (navigationUpdates.Count > 0)
|
||||
{
|
||||
// Updates in-memory navigation structure
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.Copy",
|
||||
() =>
|
||||
{
|
||||
foreach (Tuple<Guid, Guid?> update in navigationUpdates)
|
||||
{
|
||||
_documentNavigationManagementService.Add(update.Item1, update.Item2);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// not handling tags here, because
|
||||
// - tags should be handled by the content repository
|
||||
// - a copy is unpublished and therefore has no impact on tags in DB
|
||||
@@ -3697,4 +3817,29 @@ public class ContentService : RepositoryService, IContentService
|
||||
DeleteBlueprintsOfTypes(new[] { contentTypeId }, userId);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Enlists an action in the current scope context to update the in-memory navigation structure
|
||||
/// when the scope completes successfully.
|
||||
/// </summary>
|
||||
/// <param name="enlistingActionKey">The unique key identifying the action to be enlisted.</param>
|
||||
/// <param name="updateNavigation">The action to be performed for updating the in-memory navigation structure.</param>
|
||||
/// <exception cref="NullReferenceException">Thrown when the scope context is null and therefore cannot be used.</exception>
|
||||
private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation)
|
||||
{
|
||||
IScopeContext? scopeContext = ScopeProvider.Context;
|
||||
|
||||
if (scopeContext is null)
|
||||
{
|
||||
throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used.");
|
||||
}
|
||||
|
||||
scopeContext.Enlist(enlistingActionKey, completed =>
|
||||
{
|
||||
if (completed)
|
||||
{
|
||||
updateNavigation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services.Changes;
|
||||
using Umbraco.Cms.Core.Services.Navigation;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -28,6 +29,7 @@ namespace Umbraco.Cms.Core.Services
|
||||
private readonly IEntityRepository _entityRepository;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly IUserIdKeyResolver _userIdKeyResolver;
|
||||
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
|
||||
|
||||
private readonly MediaFileManager _mediaFileManager;
|
||||
|
||||
@@ -43,7 +45,8 @@ namespace Umbraco.Cms.Core.Services
|
||||
IMediaTypeRepository mediaTypeRepository,
|
||||
IEntityRepository entityRepository,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IUserIdKeyResolver userIdKeyResolver)
|
||||
IUserIdKeyResolver userIdKeyResolver,
|
||||
IMediaNavigationManagementService mediaNavigationManagementService)
|
||||
: base(provider, loggerFactory, eventMessagesFactory)
|
||||
{
|
||||
_mediaFileManager = mediaFileManager;
|
||||
@@ -53,6 +56,34 @@ namespace Umbraco.Cms.Core.Services
|
||||
_entityRepository = entityRepository;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
_userIdKeyResolver = userIdKeyResolver;
|
||||
_mediaNavigationManagementService = mediaNavigationManagementService;
|
||||
}
|
||||
|
||||
[Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")]
|
||||
public MediaService(
|
||||
ICoreScopeProvider provider,
|
||||
MediaFileManager mediaFileManager,
|
||||
ILoggerFactory loggerFactory,
|
||||
IEventMessagesFactory eventMessagesFactory,
|
||||
IMediaRepository mediaRepository,
|
||||
IAuditRepository auditRepository,
|
||||
IMediaTypeRepository mediaTypeRepository,
|
||||
IEntityRepository entityRepository,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IUserIdKeyResolver userIdKeyResolver)
|
||||
: this(
|
||||
provider,
|
||||
mediaFileManager,
|
||||
loggerFactory,
|
||||
eventMessagesFactory,
|
||||
mediaRepository,
|
||||
auditRepository,
|
||||
mediaTypeRepository,
|
||||
entityRepository,
|
||||
shortStringHelper,
|
||||
userIdKeyResolver,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMediaNavigationManagementService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")]
|
||||
@@ -76,8 +107,8 @@ namespace Umbraco.Cms.Core.Services
|
||||
mediaTypeRepository,
|
||||
entityRepository,
|
||||
shortStringHelper,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>()
|
||||
)
|
||||
StaticServiceProvider.Instance.GetRequiredService<IUserIdKeyResolver>(),
|
||||
StaticServiceProvider.Instance.GetRequiredService<IMediaNavigationManagementService>())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -769,6 +800,12 @@ namespace Umbraco.Cms.Core.Services
|
||||
media.WriterId = userId;
|
||||
|
||||
_mediaRepository.Save(media);
|
||||
|
||||
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.MediaService.Save",
|
||||
() => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key));
|
||||
|
||||
scope.Notifications.Publish(new MediaSavedNotification(media, eventMessages).WithStateFrom(savingNotification));
|
||||
// TODO: See note about suppressing events in content service
|
||||
scope.Notifications.Publish(new MediaTreeChangeNotification(media, TreeChangeTypes.RefreshNode, eventMessages));
|
||||
@@ -810,6 +847,11 @@ namespace Umbraco.Cms.Core.Services
|
||||
}
|
||||
|
||||
_mediaRepository.Save(media);
|
||||
|
||||
// Updates in-memory navigation structure - we only handle new items, other updates are not a concern
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.ContentService.Save-collection",
|
||||
() => _mediaNavigationManagementService.Add(media.Key, GetParent(media)?.Key));
|
||||
}
|
||||
|
||||
scope.Notifications.Publish(new MediaSavedNotification(mediasA, messages).WithStateFrom(savingNotification));
|
||||
@@ -881,6 +923,26 @@ namespace Umbraco.Cms.Core.Services
|
||||
}
|
||||
|
||||
DoDelete(media);
|
||||
|
||||
if (media.Trashed)
|
||||
{
|
||||
// Updates in-memory navigation structure for recycle bin items
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.MediaService.DeleteLocked-trashed",
|
||||
() => _mediaNavigationManagementService.RemoveFromBin(media.Key));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Updates in-memory navigation structure for both media and recycle bin items
|
||||
// as the item needs to be deleted whether it is in the recycle bin or not
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.MediaService.DeleteLocked",
|
||||
() =>
|
||||
{
|
||||
_mediaNavigationManagementService.MoveToBin(media.Key);
|
||||
_mediaNavigationManagementService.RemoveFromBin(media.Key);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: both DeleteVersions methods below have an issue. Sort of. They do NOT take care of files the way
|
||||
@@ -1069,6 +1131,8 @@ namespace Umbraco.Cms.Core.Services
|
||||
// trash indicates whether we are trashing, un-trashing, or not changing anything
|
||||
private void PerformMoveLocked(IMedia media, int parentId, IMedia? parent, int userId, ICollection<(IMedia, string)> moves, bool? trash)
|
||||
{
|
||||
// Needed to update the in-memory navigation structure
|
||||
var cameFromRecycleBin = media.ParentId == Constants.System.RecycleBinMedia;
|
||||
media.ParentId = parentId;
|
||||
|
||||
// get the level delta (old pos to new pos)
|
||||
@@ -1114,6 +1178,32 @@ namespace Umbraco.Cms.Core.Services
|
||||
}
|
||||
while (total > pageSize);
|
||||
|
||||
if (parentId == Constants.System.RecycleBinMedia)
|
||||
{
|
||||
// Updates in-memory navigation structure for both media items and recycle bin items
|
||||
// as we are moving to recycle bin
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-to-recycle-bin",
|
||||
() => _mediaNavigationManagementService.MoveToBin(media.Key));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (cameFromRecycleBin)
|
||||
{
|
||||
// Updates in-memory navigation structure for both media items and recycle bin items
|
||||
// as we are restoring from recycle bin
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked-restore",
|
||||
() => _mediaNavigationManagementService.RestoreFromBin(media.Key, parent?.Key));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Updates in-memory navigation structure
|
||||
UpdateInMemoryNavigationStructure(
|
||||
"Umbraco.Cms.Core.Services.MediaService.PerformMoveLocked",
|
||||
() => _mediaNavigationManagementService.Move(media.Key, parent?.Key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformMoveMediaLocked(IMedia media, bool? trash)
|
||||
@@ -1421,6 +1511,29 @@ namespace Umbraco.Cms.Core.Services
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Enlists an action in the current scope context to update the in-memory navigation structure
|
||||
/// when the scope completes successfully.
|
||||
/// </summary>
|
||||
/// <param name="enlistingActionKey">The unique key identifying the action to be enlisted.</param>
|
||||
/// <param name="updateNavigation">The action to be performed for updating the in-memory navigation structure.</param>
|
||||
/// <exception cref="NullReferenceException">Thrown when the scope context is null and therefore cannot be used.</exception>
|
||||
private void UpdateInMemoryNavigationStructure(string enlistingActionKey, Action updateNavigation)
|
||||
{
|
||||
IScopeContext? scopeContext = ScopeProvider.Context;
|
||||
|
||||
if (scopeContext is null)
|
||||
{
|
||||
throw new NullReferenceException($"The {nameof(scopeContext)} is null and cannot be used.");
|
||||
}
|
||||
|
||||
scopeContext.Enlist(enlistingActionKey, completed =>
|
||||
{
|
||||
if (completed)
|
||||
{
|
||||
updateNavigation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
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;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
internal abstract class ContentNavigationServiceBase
|
||||
{
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
private readonly INavigationRepository _navigationRepository;
|
||||
private ConcurrentDictionary<Guid, NavigationNode> _navigationStructure = new();
|
||||
private ConcurrentDictionary<Guid, NavigationNode> _recycleBinNavigationStructure = new();
|
||||
|
||||
protected ContentNavigationServiceBase(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository)
|
||||
{
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
_navigationRepository = navigationRepository;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the entire main navigation structure. Implementations should define how the structure is rebuilt.
|
||||
/// </summary>
|
||||
public abstract Task RebuildAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the recycle bin navigation structure. Implementations should define how the bin structure is rebuilt.
|
||||
/// </summary>
|
||||
public abstract Task RebuildBinAsync();
|
||||
|
||||
public bool TryGetParentKey(Guid childKey, out Guid? parentKey)
|
||||
=> TryGetParentKeyFromStructure(_navigationStructure, childKey, out parentKey);
|
||||
|
||||
public bool TryGetChildrenKeys(Guid parentKey, out IEnumerable<Guid> childrenKeys)
|
||||
=> TryGetChildrenKeysFromStructure(_navigationStructure, parentKey, out childrenKeys);
|
||||
|
||||
public bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable<Guid> descendantsKeys)
|
||||
=> TryGetDescendantsKeysFromStructure(_navigationStructure, parentKey, out descendantsKeys);
|
||||
|
||||
public bool TryGetAncestorsKeys(Guid childKey, out IEnumerable<Guid> ancestorsKeys)
|
||||
=> TryGetAncestorsKeysFromStructure(_navigationStructure, childKey, out ancestorsKeys);
|
||||
|
||||
public bool TryGetSiblingsKeys(Guid key, out IEnumerable<Guid> siblingsKeys)
|
||||
=> TryGetSiblingsKeysFromStructure(_navigationStructure, key, out siblingsKeys);
|
||||
|
||||
public bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey)
|
||||
=> TryGetParentKeyFromStructure(_recycleBinNavigationStructure, childKey, out parentKey);
|
||||
|
||||
public bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable<Guid> childrenKeys)
|
||||
=> TryGetChildrenKeysFromStructure(_recycleBinNavigationStructure, parentKey, out childrenKeys);
|
||||
|
||||
public bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable<Guid> descendantsKeys)
|
||||
=> TryGetDescendantsKeysFromStructure(_recycleBinNavigationStructure, parentKey, out descendantsKeys);
|
||||
|
||||
public bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable<Guid> ancestorsKeys)
|
||||
=> TryGetAncestorsKeysFromStructure(_recycleBinNavigationStructure, childKey, out ancestorsKeys);
|
||||
|
||||
public bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable<Guid> siblingsKeys)
|
||||
=> TryGetSiblingsKeysFromStructure(_recycleBinNavigationStructure, key, out siblingsKeys);
|
||||
|
||||
public bool MoveToBin(Guid key)
|
||||
{
|
||||
if (TryRemoveNodeFromParentInStructure(_navigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null)
|
||||
{
|
||||
return false; // Node doesn't exist
|
||||
}
|
||||
|
||||
// Recursively remove all descendants and add them to recycle bin
|
||||
AddDescendantsToRecycleBinRecursively(nodeToRemove);
|
||||
|
||||
return _recycleBinNavigationStructure.TryAdd(nodeToRemove.Key, nodeToRemove) &&
|
||||
_navigationStructure.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
public bool Add(Guid key, Guid? parentKey = null)
|
||||
{
|
||||
NavigationNode? parentNode = null;
|
||||
if (parentKey.HasValue)
|
||||
{
|
||||
if (_navigationStructure.TryGetValue(parentKey.Value, out parentNode) is false)
|
||||
{
|
||||
return false; // Parent node doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
var newNode = new NavigationNode(key);
|
||||
if (_navigationStructure.TryAdd(key, newNode) is false)
|
||||
{
|
||||
return false; // Node with this key already exists
|
||||
}
|
||||
|
||||
parentNode?.AddChild(newNode);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Move(Guid key, Guid? targetParentKey = null)
|
||||
{
|
||||
if (_navigationStructure.TryGetValue(key, out NavigationNode? nodeToMove) is false)
|
||||
{
|
||||
return false; // Node doesn't exist
|
||||
}
|
||||
|
||||
if (key == targetParentKey)
|
||||
{
|
||||
return false; // Cannot move a node to itself
|
||||
}
|
||||
|
||||
NavigationNode? targetParentNode = null;
|
||||
if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false)
|
||||
{
|
||||
return false; // Target parent doesn't exist
|
||||
}
|
||||
|
||||
// Remove the node from its current parent's children list
|
||||
if (nodeToMove.Parent is not null && _navigationStructure.TryGetValue(nodeToMove.Parent.Key, out var currentParentNode))
|
||||
{
|
||||
currentParentNode.RemoveChild(nodeToMove);
|
||||
}
|
||||
|
||||
// Set the new parent for the node (if parent node is null - the node is moved to root)
|
||||
targetParentNode?.AddChild(nodeToMove);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveFromBin(Guid key)
|
||||
{
|
||||
if (TryRemoveNodeFromParentInStructure(_recycleBinNavigationStructure, key, out NavigationNode? nodeToRemove) is false || nodeToRemove is null)
|
||||
{
|
||||
return false; // Node doesn't exist
|
||||
}
|
||||
|
||||
RemoveDescendantsRecursively(nodeToRemove);
|
||||
|
||||
return _recycleBinNavigationStructure.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
public bool RestoreFromBin(Guid key, Guid? targetParentKey = null)
|
||||
{
|
||||
if (_recycleBinNavigationStructure.TryGetValue(key, out NavigationNode? nodeToRestore) is false)
|
||||
{
|
||||
return false; // Node doesn't exist
|
||||
}
|
||||
|
||||
// If a target parent is specified, try to find it in the main structure
|
||||
NavigationNode? targetParentNode = null;
|
||||
if (targetParentKey.HasValue && _navigationStructure.TryGetValue(targetParentKey.Value, out targetParentNode) is false)
|
||||
{
|
||||
return false; // Target parent doesn't exist
|
||||
}
|
||||
|
||||
// Set the new parent for the node (if parent node is null - the node is moved to root)
|
||||
targetParentNode?.AddChild(nodeToRestore);
|
||||
|
||||
// Restore the node and its descendants from the recycle bin to the main structure
|
||||
RestoreNodeAndDescendantsRecursively(nodeToRestore);
|
||||
|
||||
return _navigationStructure.TryAdd(nodeToRestore.Key, nodeToRestore) &&
|
||||
_recycleBinNavigationStructure.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the navigation structure based on the specified object type key and whether the items are trashed.
|
||||
/// Only relevant for items in the content and media trees (which have readLock values of -333 or -334).
|
||||
/// </summary>
|
||||
/// <param name="readLock">The read lock value, should be -333 or -334 for content and media trees.</param>
|
||||
/// <param name="objectTypeKey">The key of the object type to rebuild.</param>
|
||||
/// <param name="trashed">Indicates whether the items are in the recycle bin.</param>
|
||||
protected async Task HandleRebuildAsync(int readLock, Guid objectTypeKey, bool trashed)
|
||||
{
|
||||
// This is only relevant for items in the content and media trees
|
||||
if (readLock != Constants.Locks.ContentTree && readLock != Constants.Locks.MediaTree)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true);
|
||||
scope.ReadLock(readLock);
|
||||
|
||||
IEnumerable<INavigationModel> navigationModels = trashed ?
|
||||
_navigationRepository.GetTrashedContentNodesByObjectType(objectTypeKey) :
|
||||
_navigationRepository.GetContentNodesByObjectType(objectTypeKey);
|
||||
|
||||
_navigationStructure = NavigationFactory.BuildNavigationDictionary(navigationModels);
|
||||
}
|
||||
|
||||
private bool TryGetParentKeyFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid childKey, out Guid? parentKey)
|
||||
{
|
||||
if (structure.TryGetValue(childKey, out NavigationNode? childNode))
|
||||
{
|
||||
parentKey = childNode.Parent?.Key;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Child doesn't exist
|
||||
parentKey = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryGetChildrenKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid parentKey, out IEnumerable<Guid> childrenKeys)
|
||||
{
|
||||
if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false)
|
||||
{
|
||||
// Parent doesn't exist
|
||||
childrenKeys = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
childrenKeys = parentNode.Children.Select(child => child.Key);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetDescendantsKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid parentKey, out IEnumerable<Guid> descendantsKeys)
|
||||
{
|
||||
var descendants = new List<Guid>();
|
||||
|
||||
if (structure.TryGetValue(parentKey, out NavigationNode? parentNode) is false)
|
||||
{
|
||||
// Parent doesn't exist
|
||||
descendantsKeys = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
GetDescendantsRecursively(parentNode, descendants);
|
||||
|
||||
descendantsKeys = descendants;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetAncestorsKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid childKey, out IEnumerable<Guid> ancestorsKeys)
|
||||
{
|
||||
var ancestors = new List<Guid>();
|
||||
|
||||
if (structure.TryGetValue(childKey, out NavigationNode? childNode) is false)
|
||||
{
|
||||
// Child doesn't exist
|
||||
ancestorsKeys = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
while (childNode?.Parent is not null)
|
||||
{
|
||||
ancestors.Add(childNode.Parent.Key);
|
||||
childNode = childNode.Parent;
|
||||
}
|
||||
|
||||
ancestorsKeys = ancestors;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetSiblingsKeysFromStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid key, out IEnumerable<Guid> siblingsKeys)
|
||||
{
|
||||
siblingsKeys = [];
|
||||
|
||||
if (structure.TryGetValue(key, out NavigationNode? node) is false)
|
||||
{
|
||||
return false; // Node doesn't exist
|
||||
}
|
||||
|
||||
if (node.Parent is null)
|
||||
{
|
||||
// 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)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetChildrenKeys(node.Parent.Key, out IEnumerable<Guid> childrenKeys) is false)
|
||||
{
|
||||
return false; // Couldn't retrieve children keys
|
||||
}
|
||||
|
||||
// Filter out the node itself to get its siblings
|
||||
siblingsKeys = childrenKeys.Where(childKey => childKey != key).ToList();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void GetDescendantsRecursively(NavigationNode node, List<Guid> descendants)
|
||||
{
|
||||
foreach (NavigationNode child in node.Children)
|
||||
{
|
||||
descendants.Add(child.Key);
|
||||
GetDescendantsRecursively(child, descendants);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryRemoveNodeFromParentInStructure(ConcurrentDictionary<Guid, NavigationNode> structure, Guid key, out NavigationNode? nodeToRemove)
|
||||
{
|
||||
if (structure.TryGetValue(key, out nodeToRemove) is false)
|
||||
{
|
||||
return false; // Node doesn't exist
|
||||
}
|
||||
|
||||
// Remove the node from its parent's children list
|
||||
if (nodeToRemove.Parent is not null && structure.TryGetValue(nodeToRemove.Parent.Key, out NavigationNode? parentNode))
|
||||
{
|
||||
parentNode.RemoveChild(nodeToRemove);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void AddDescendantsToRecycleBinRecursively(NavigationNode node)
|
||||
{
|
||||
foreach (NavigationNode child in node.Children)
|
||||
{
|
||||
AddDescendantsToRecycleBinRecursively(child);
|
||||
|
||||
// Only remove the child from the main structure if it was successfully added to the recycle bin
|
||||
if (_recycleBinNavigationStructure.TryAdd(child.Key, child))
|
||||
{
|
||||
_navigationStructure.TryRemove(child.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveDescendantsRecursively(NavigationNode node)
|
||||
{
|
||||
foreach (NavigationNode child in node.Children)
|
||||
{
|
||||
RemoveDescendantsRecursively(child);
|
||||
_recycleBinNavigationStructure.TryRemove(child.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
private void RestoreNodeAndDescendantsRecursively(NavigationNode node)
|
||||
{
|
||||
foreach (NavigationNode child in node.Children)
|
||||
{
|
||||
RestoreNodeAndDescendantsRecursively(child);
|
||||
|
||||
// Only remove the child from the recycle bin structure if it was successfully added to the main one
|
||||
if (_navigationStructure.TryAdd(child.Key, child))
|
||||
{
|
||||
_recycleBinNavigationStructure.TryRemove(child.Key, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
internal sealed class DocumentNavigationService : ContentNavigationServiceBase, IDocumentNavigationQueryService, IDocumentNavigationManagementService
|
||||
{
|
||||
public DocumentNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository)
|
||||
: base(coreScopeProvider, navigationRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task RebuildAsync()
|
||||
=> await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, false);
|
||||
|
||||
public override async Task RebuildBinAsync()
|
||||
=> await HandleRebuildAsync(Constants.Locks.ContentTree, Constants.ObjectTypes.Document, true);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
public interface IDocumentNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
public interface IDocumentNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
public interface IMediaNavigationManagementService : INavigationManagementService, IRecycleBinNavigationManagementService
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
public interface IMediaNavigationQueryService : INavigationQueryService, IRecycleBinNavigationQueryService
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for sharing logic between the document and media navigation services
|
||||
/// for managing the navigation structure.
|
||||
/// </summary>
|
||||
public interface INavigationManagementService
|
||||
{
|
||||
/// <summary>
|
||||
/// Rebuilds the entire navigation structure by refreshing the navigation tree based
|
||||
/// on the current state of the underlying repository.
|
||||
/// </summary>
|
||||
Task RebuildAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Removes a node from the main navigation structure and moves it, along with
|
||||
/// its descendants, to the root of the recycle bin structure.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier of the node to remove.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the node and its descendants were successfully removed from the
|
||||
/// main navigation structure and added to the recycle bin; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
bool MoveToBin(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new node to the main navigation structure. If a parent key is provided,
|
||||
/// the new node is added as a child of the specified parent. If no parent key is
|
||||
/// provided, the new node is added at the root level.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier of the new node to add.</param>
|
||||
/// <param name="parentKey">
|
||||
/// The unique identifier of the parent node. If <c>null</c>, the new node will be added to
|
||||
/// the root level.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the node was successfully added to the main navigation structure;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
bool Add(Guid key, Guid? parentKey = null);
|
||||
|
||||
/// <summary>
|
||||
/// Moves an existing node to a new parent in the main navigation structure. If a
|
||||
/// target parent key is provided, the node is moved under the specified parent.
|
||||
/// If no target parent key is provided, the node is moved to the root level.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier of the node to move.</param>
|
||||
/// <param name="targetParentKey">
|
||||
/// The unique identifier of the new parent node. If <c>null</c>, the node will be moved to
|
||||
/// the root level.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the node and its descendants were successfully moved to the new parent
|
||||
/// in the main navigation structure; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
bool Move(Guid key, Guid? targetParentKey = null);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for sharing logic between the document and media navigation services
|
||||
/// for querying the navigation structure.
|
||||
/// </summary>
|
||||
public interface INavigationQueryService
|
||||
{
|
||||
bool TryGetParentKey(Guid childKey, out Guid? parentKey);
|
||||
|
||||
bool TryGetChildrenKeys(Guid parentKey, out IEnumerable<Guid> childrenKeys);
|
||||
|
||||
bool TryGetDescendantsKeys(Guid parentKey, out IEnumerable<Guid> descendantsKeys);
|
||||
|
||||
bool TryGetAncestorsKeys(Guid childKey, out IEnumerable<Guid> ancestorsKeys);
|
||||
|
||||
bool TryGetSiblingsKeys(Guid key, out IEnumerable<Guid> siblingsKeys);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for sharing logic between the document and media navigation services
|
||||
/// for managing the recycle bin navigation structure.
|
||||
/// </summary>
|
||||
public interface IRecycleBinNavigationManagementService
|
||||
{
|
||||
/// <summary>
|
||||
/// Rebuilds the recycle bin navigation structure by fetching the latest trashed nodes
|
||||
/// from the underlying repository.
|
||||
/// </summary>
|
||||
Task RebuildBinAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Permanently removes a node and all of its descendants from the recycle bin navigation structure.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier of the node to remove.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the node and its descendants were successfully removed from the recycle bin;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
bool RemoveFromBin(Guid key);
|
||||
|
||||
/// <summary>
|
||||
/// Restores a node and all of its descendants from the recycle bin navigation structure and moves them back
|
||||
/// to the main navigation structure. The node can be restored to a specified target parent or to the root
|
||||
/// level if no parent is specified.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique identifier of the node to restore from the recycle bin navigation structure.</param>
|
||||
/// <param name="targetParentKey">
|
||||
/// The unique identifier of the target parent node in the main navigation structure to which the node
|
||||
/// should be restored. If <c>null</c>, the node will be restored to the root level.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the node and its descendants were successfully restored to the main navigation structure;
|
||||
/// otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
bool RestoreFromBin(Guid key, Guid? targetParentKey = null);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Placeholder for sharing logic between the document and media navigation services
|
||||
/// for querying the recycle bin navigation structure.
|
||||
/// </summary>
|
||||
public interface IRecycleBinNavigationQueryService
|
||||
{
|
||||
bool TryGetParentKeyInBin(Guid childKey, out Guid? parentKey);
|
||||
|
||||
bool TryGetChildrenKeysInBin(Guid parentKey, out IEnumerable<Guid> childrenKeys);
|
||||
|
||||
bool TryGetDescendantsKeysInBin(Guid parentKey, out IEnumerable<Guid> descendantsKeys);
|
||||
|
||||
bool TryGetAncestorsKeysInBin(Guid childKey, out IEnumerable<Guid> ancestorsKeys);
|
||||
|
||||
bool TryGetSiblingsKeysInBin(Guid key, out IEnumerable<Guid> siblingsKeys);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
internal sealed class MediaNavigationService : ContentNavigationServiceBase, IMediaNavigationQueryService, IMediaNavigationManagementService
|
||||
{
|
||||
public MediaNavigationService(ICoreScopeProvider coreScopeProvider, INavigationRepository navigationRepository)
|
||||
: base(coreScopeProvider, navigationRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task RebuildAsync()
|
||||
=> await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, false);
|
||||
|
||||
public override async Task RebuildBinAsync()
|
||||
=> await HandleRebuildAsync(Constants.Locks.MediaTree, Constants.ObjectTypes.Media, true);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services.Navigation;
|
||||
|
||||
/// <summary>
|
||||
/// Responsible for seeding the in-memory navigation structures at application's startup
|
||||
/// by rebuild the navigation structures.
|
||||
/// </summary>
|
||||
public sealed class NavigationInitializationHostedService : IHostedLifecycleService
|
||||
{
|
||||
private readonly IDocumentNavigationManagementService _documentNavigationManagementService;
|
||||
private readonly IMediaNavigationManagementService _mediaNavigationManagementService;
|
||||
|
||||
public NavigationInitializationHostedService(IDocumentNavigationManagementService documentNavigationManagementService, IMediaNavigationManagementService mediaNavigationManagementService)
|
||||
{
|
||||
_documentNavigationManagementService = documentNavigationManagementService;
|
||||
_mediaNavigationManagementService = mediaNavigationManagementService;
|
||||
}
|
||||
|
||||
public async Task StartingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _documentNavigationManagementService.RebuildAsync();
|
||||
await _documentNavigationManagementService.RebuildBinAsync();
|
||||
await _mediaNavigationManagementService.RebuildAsync();
|
||||
await _mediaNavigationManagementService.RebuildBinAsync();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user