From d9d5dc5626837ce6d5834bc75f8dd41a3a6c0c61 Mon Sep 17 00:00:00 2001 From: Elitsa Marinovska <21998037+elit0451@users.noreply.github.com> Date: Mon, 7 Mar 2022 22:46:16 +0100 Subject: [PATCH] Item tracking improvements (#11919) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding TrackedReferencesController * Adding/changing views * Adding/Editing js files * RelationService changes * RelationRepository changes * Adding missing translations * Adding/Modifying tests * Beginning of #9125 migration * Introducing a new component + refactoring based on that * Abstracting + refactoring * Work on content unpublishing * Work on media.delete * Various small changes * Beginning of #9119 migration * Changes on content.delete * Various fixes * Adding new keys used in the listview bulk actions * Adding methods to get the items used in relations from array of ids * Adding the checkLinkedItems function to the trackedReferencesResource * Passing the selected items from a listview to unpublish and delete * Adding umb-tracked-references-listview * Adding umb-tracked-references-listview-table with language column * Fixes for tracked references * Changes in listview unpublish dialog * Changes in listview delete dialog * Removing Variants logic as it is not currently supported * Visual fixes * Closing dialogs on click * Fix wording * Fix breaking changes * Change to a single title "Items in use" instead of 2 different for Content and Media * No need for obsoleting because we can change new controllers * Return ActionResult from actions * V9: Prevent delete or unpublish of items that have references (#12047) * Introducing config settings that prevent delete or unpublish of items referenced by other items * Disable deletion of content items and show a new warning * Disable deletion of media items and show a new warning * Disable deletion of list view items * Disable unpublish and bulk unpublish * Add a new warning * V9: Displaying descendants in use as part of item tracking (#12039) * Replace HasReferencesInDescendants with GetPagedDescendantsInReferences * Display descendants in use on parent's info tab * Add getPagedDescendantsInReferences to trackedReferencesResource * Add lang keys for Descendants in use * Refactoring controller actions * Don't call check descendants usage when it is a new item * rename busfy to busy * always show references * rearrange for scrollbar to appear at the edge of the dialog * use the word referenced instead of used * change fallback texts * Added "IsDependency" to relation types * refactor of umb-tracked-references * rename checkLinkedItems to getPagedReferencedItems * rename check to load, to be consistent with the rest. * Refactored backend . Needs frontend fixes * Cleanup * Use filters * Front-end refactor to match refactored end-points * Fixed bug + warning * Fixed query (more then 2100 descensdants) and optimized it (using nested select instead of inner join). * remove comment * hideNoneDependencies including varying text for the configuration * Hack for SqlCE :( * some final adjustments for item tracking ui * Unbreak change Co-authored-by: Niels Lyngsø Co-authored-by: Bjarke Berg --- .../Configuration/Models/ContentSettings.cs | 14 ++ .../Events/RelateOnCopyNotificationHandler.cs | 3 +- .../ContentEditing/RelationTypeDisplay.cs | 6 + .../Models/ContentEditing/RelationTypeSave.cs | 6 + src/Umbraco.Core/Models/IRelationType.cs | 9 + .../Models/Mapping/RelationMapDefinition.cs | 10 + src/Umbraco.Core/Models/RelationItem.cs | 44 ++++ src/Umbraco.Core/Models/RelationType.cs | 19 +- .../Repositories/IRelationRepository.cs | 2 +- .../ITrackedReferencesRepository.cs | 14 ++ src/Umbraco.Core/Services/IRelationService.cs | 6 +- .../Services/ITrackedReferencesService.cs | 15 ++ .../UmbracoBuilder.Repositories.cs | 1 + .../UmbracoBuilder.Services.cs | 1 + .../RelateOnTrashNotificationHandler.cs | 4 +- .../Migrations/Install/DatabaseDataCreator.cs | 10 +- .../Migrations/Upgrade/UmbracoPlan.cs | 1 + ...UpdateRelationTypesToHandleDependencies.cs | 34 +++ .../Persistence/Dtos/RelationTypeDto.cs | 4 + .../Factories/RelationTypeFactory.cs | 10 +- .../Persistence/Mappers/RelationTypeMapper.cs | 1 + .../Persistence/NPocoSqlExtensions.cs | 29 ++- .../Implement/RelationRepository.cs | 90 ++++++++ .../Implement/TrackedReferencesRepository.cs | 194 ++++++++++++++++++ .../Services/Implement/RelationService.cs | 2 +- .../Implement/TrackedReferencesService.cs | 50 +++++ .../Controllers/BackOfficeServerVariables.cs | 8 +- .../Controllers/MediaController.cs | 5 +- .../Controllers/RelationTypeController.cs | 3 +- .../TrackedReferencesController.cs | 71 +++++++ .../media/umbmedianodeinfo.directive.js | 67 ------ .../umbtrackedreferences.component.js | 125 +++++++++++ ...mbtrackedreferencesbulkaction.component.js | 79 +++++++ .../umbtrackedreferencestable.component.js | 106 ++++++++++ .../src/common/resources/media.resource.js | 63 +++--- .../resources/trackedreferences.resource.js | 178 ++++++++++++++++ .../services/umbdataformatter.service.js | 1 + .../src/less/components/overlays.less | 6 +- .../src/less/components/umb-table.less | 1 + .../content/umb-content-node-info.html | 3 + .../components/media/umb-media-node-info.html | 126 +----------- .../umb-tracked-references-bulk-action.html | 13 ++ .../umb-tracked-references-table.html | 35 ++++ .../references/umb-tracked-references.html | 36 ++++ .../content/content.delete.controller.js | 27 ++- .../src/views/content/delete.html | 22 +- .../content/overlays/unpublish.controller.js | 21 ++ .../src/views/content/overlays/unpublish.html | 9 +- .../src/views/media/delete.html | 12 +- .../views/media/media.delete.controller.js | 29 ++- .../listview/listview.controller.js | 4 +- .../listview/overlays/delete.controller.js | 40 ++++ .../listview/overlays/delete.html | 10 +- .../overlays/listviewunpublish.controller.js | 23 +++ .../listview/overlays/listviewunpublish.html | 8 + .../src/views/relationTypes/create.html | 28 +++ .../views/relationTypes/edit.controller.js | 5 +- .../relationTypes/views/relationType.html | 22 +- .../views/relationType.settings.controller.js | 7 +- .../src/views/users/user.controller.js | 2 +- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 12 ++ .../umbraco/config/lang/en_us.xml | 33 ++- .../Builders/RelationTypeBuilder.cs | 14 +- .../Repositories/RelationRepositoryTest.cs | 156 ++++++++++++-- .../RelationTypeRepositoryTest.cs | 11 +- .../Services/ContentServiceTests.cs | 2 +- .../Services/RelationServiceTests.cs | 2 +- .../Builders/RelationBuilderTests.cs | 3 + .../Builders/RelationTypeBuilderTests.cs | 5 +- 69 files changed, 1709 insertions(+), 303 deletions(-) create mode 100644 src/Umbraco.Core/Models/RelationItem.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs create mode 100644 src/Umbraco.Core/Services/ITrackedReferencesService.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs create mode 100644 src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs create mode 100644 src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs create mode 100644 src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencesbulkaction.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-bulk-action.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.controller.js diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index 93a97355d9..e6e5c7006f 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -157,6 +157,8 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const string StaticLoginBackgroundImage = "assets/img/login.jpg"; internal const string StaticLoginLogoImage = "assets/img/application/umbraco_logo_white.svg"; internal const bool StaticHideBackOfficeLogo = false; + internal const bool StaticDisableDeleteWhenReferenced = false; + internal const bool StaticDisableUnpublishWhenReferenced = false; /// /// Gets or sets a value for the content notification settings. @@ -226,6 +228,18 @@ namespace Umbraco.Cms.Core.Configuration.Models [DefaultValue(StaticHideBackOfficeLogo)] public bool HideBackOfficeLogo { get; set; } = StaticHideBackOfficeLogo; + /// + /// Gets or sets a value indicating whether to disable the deletion of items referenced by other items. + /// + [DefaultValue(StaticDisableDeleteWhenReferenced)] + public bool DisableDeleteWhenReferenced { get; set; } = StaticDisableDeleteWhenReferenced; + + /// + /// Gets or sets a value indicating whether to disable the unpublishing of items referenced by other items. + /// + [DefaultValue(StaticDisableUnpublishWhenReferenced)] + public bool DisableUnpublishWhenReferenced { get; set; } = StaticDisableUnpublishWhenReferenced; + /// /// Get or sets the model representing the global content version cleanup policy /// diff --git a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs index 0a6518a1ca..3a73173127 100644 --- a/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs +++ b/src/Umbraco.Core/Events/RelateOnCopyNotificationHandler.cs @@ -33,7 +33,8 @@ namespace Umbraco.Cms.Core.Events Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, true, Constants.ObjectTypes.Document, - Constants.ObjectTypes.Document); + Constants.ObjectTypes.Document, + false); _relationService.Save(relationType); } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs index 27f0f525df..6a4c8e5f81 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeDisplay.cs @@ -55,5 +55,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing /// [DataMember(Name = "notifications")] public List Notifications { get; private set; } + + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs index b72a03eec4..f541158095 100644 --- a/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs +++ b/src/Umbraco.Core/Models/ContentEditing/RelationTypeSave.cs @@ -23,5 +23,11 @@ namespace Umbraco.Cms.Core.Models.ContentEditing /// [DataMember(Name = "childObjectType", IsRequired = false)] public Guid? ChildObjectType { get; set; } + + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember(Name = "isDependency", IsRequired = true)] + public bool IsDependency { get; set; } } } diff --git a/src/Umbraco.Core/Models/IRelationType.cs b/src/Umbraco.Core/Models/IRelationType.cs index 9efde4b939..3ee1517f55 100644 --- a/src/Umbraco.Core/Models/IRelationType.cs +++ b/src/Umbraco.Core/Models/IRelationType.cs @@ -4,6 +4,15 @@ using Umbraco.Cms.Core.Models.Entities; namespace Umbraco.Cms.Core.Models { + public interface IRelationTypeWithIsDependency : IRelationType + { + /// + /// Gets or sets a boolean indicating whether the RelationType should be returned in "Used by"-queries. + /// + [DataMember] + bool IsDependency { get; set; } + } + public interface IRelationType : IEntity, IRememberBeingDirty { /// diff --git a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs index 41caa526e2..2b333652b9 100644 --- a/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Core/Models/Mapping/RelationMapDefinition.cs @@ -30,6 +30,11 @@ namespace Umbraco.Cms.Core.Models.Mapping target.ChildObjectType = source.ChildObjectType; target.Id = source.Id; target.IsBidirectional = source.IsBidirectional; + + if (source is IRelationTypeWithIsDependency sourceWithIsDependency) + { + target.IsDependency = sourceWithIsDependency.IsDependency; + } target.Key = source.Key; target.Name = source.Name; target.Alias = source.Alias; @@ -74,6 +79,11 @@ namespace Umbraco.Cms.Core.Models.Mapping target.ChildObjectType = source.ChildObjectType; target.Id = source.Id.TryConvertTo().Result; target.IsBidirectional = source.IsBidirectional; + if (target is IRelationTypeWithIsDependency targetWithIsDependency) + { + targetWithIsDependency.IsDependency = source.IsDependency; + } + target.Key = source.Key; target.Name = source.Name; target.ParentObjectType = source.ParentObjectType; diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs new file mode 100644 index 0000000000..cebbc20951 --- /dev/null +++ b/src/Umbraco.Core/Models/RelationItem.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract(Name = "relationItem", Namespace = "")] + public class RelationItem + { + [DataMember(Name = "id")] + public int NodeId { get; set; } + + [DataMember(Name = "key")] + public Guid NodeKey { get; set; } + + [DataMember(Name = "name")] + public string NodeName { get; set; } + + [DataMember(Name = "type")] + public string NodeType { get; set; } + + [DataMember(Name = "udi")] + public Udi NodeUdi => Udi.Create(NodeType, NodeKey); + + [DataMember(Name = "icon")] + public string ContentTypeIcon { get; set; } + + [DataMember(Name = "alias")] + public string ContentTypeAlias { get; set; } + + [DataMember(Name = "contentTypeName")] + public string ContentTypeName { get; set; } + + [DataMember(Name = "relationTypeName")] + public string RelationTypeName { get; set; } + + [DataMember(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } + + [DataMember(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } + + } +} diff --git a/src/Umbraco.Core/Models/RelationType.cs b/src/Umbraco.Core/Models/RelationType.cs index 2def0eb636..5de0aaac8f 100644 --- a/src/Umbraco.Core/Models/RelationType.cs +++ b/src/Umbraco.Core/Models/RelationType.cs @@ -9,20 +9,28 @@ namespace Umbraco.Cms.Core.Models /// [Serializable] [DataContract(IsReference = true)] - public class RelationType : EntityBase, IRelationType + public class RelationType : EntityBase, IRelationType, IRelationTypeWithIsDependency { private string _name; private string _alias; private bool _isBidirectional; + private bool _isDependency; private Guid? _parentObjectType; private Guid? _childObjectType; public RelationType(string alias, string name) - : this(name: name, alias: alias, false, null, null) + : this(name: name, alias: alias, false, null, null, false) { } + [Obsolete("Use ctor with isDependency parameter")] public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType) + :this(name,alias,isBidrectional, parentObjectType, childObjectType, false) + { + + } + + public RelationType(string name, string alias, bool isBidrectional, Guid? parentObjectType, Guid? childObjectType, bool isDependency) { if (name == null) throw new ArgumentNullException(nameof(name)); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(name)); @@ -32,6 +40,7 @@ namespace Umbraco.Cms.Core.Models _name = name; _alias = alias; _isBidirectional = isBidrectional; + _isDependency = isDependency; _parentObjectType = parentObjectType; _childObjectType = childObjectType; } @@ -88,5 +97,11 @@ namespace Umbraco.Cms.Core.Models set => SetPropertyValueAndDetectChanges(value, ref _childObjectType, nameof(ChildObjectType)); } + + public bool IsDependency + { + get => _isDependency; + set => SetPropertyValueAndDetectChanges(value, ref _isDependency, nameof(IsDependency)); + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index cff64d4a79..c863ef4d8e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs new file mode 100644 index 0000000000..42746a9565 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.Core.Persistence.Repositories +{ + public interface ITrackedReferencesRepository + { + IEnumerable GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); + IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); + IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency,out long totalRecords); + } +} diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index ce00f774f8..4d0e977d38 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; @@ -215,7 +215,7 @@ namespace Umbraco.Cms.Core.Services /// /// /// - /// + /// An enumerable list of IEnumerable GetPagedParentEntitiesByChildId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); /// @@ -225,7 +225,7 @@ namespace Umbraco.Cms.Core.Services /// /// /// - /// + /// An enumerable list of IEnumerable GetPagedChildEntitiesByParentId(int id, long pageIndex, int pageSize, out long totalChildren, params UmbracoObjectTypes[] entityTypes); /// diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs new file mode 100644 index 0000000000..eee8a324df --- /dev/null +++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs @@ -0,0 +1,15 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface ITrackedReferencesService + { + PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); + + + PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency); + + + PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency); + } +} diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index f9dc43cbd5..966b54633b 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -47,6 +47,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 90d08b93fe..915b815033 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -63,6 +63,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); diff --git a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs index 4222ac800e..b06248c79e 100644 --- a/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Events/RelateOnTrashNotificationHandler.cs @@ -65,7 +65,7 @@ namespace Umbraco.Cms.Core.Events var documentObjectType = Constants.ObjectTypes.Document; const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName; - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); _relationService.Save(relationType); } @@ -123,7 +123,7 @@ namespace Umbraco.Cms.Core.Events { var documentObjectType = Constants.ObjectTypes.Document; const string relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; - relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType); + relationType = new RelationType(relationTypeName, relationTypeAlias, false, documentObjectType, documentObjectType, false); _relationService.Save(relationType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 25dadb0c85..b19802996b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -421,21 +421,21 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install private void CreateRelationTypeData() { - var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName }; + var relationType = new RelationTypeDto { Id = 1, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = true, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateDocumentOnCopyName, IsDependency = false}; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }; + relationType = new RelationTypeDto { Id = 2, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Document, ParentObjectType = Cms.Core.Constants.ObjectTypes.Document, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName, IsDependency = false }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName }; + relationType = new RelationTypeDto { Id = 3, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = Cms.Core.Constants.ObjectTypes.Media, ParentObjectType = Cms.Core.Constants.ObjectTypes.Media, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, IsDependency = false }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName }; + relationType = new RelationTypeDto { Id = 4, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedMediaName, IsDependency = true }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); - relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName }; + relationType = new RelationTypeDto { Id = 5, Alias = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, ChildObjectType = null, ParentObjectType = null, Dual = false, Name = Cms.Core.Constants.Conventions.RelationTypes.RelatedDocumentName, IsDependency = true }; relationType.UniqueId = CreateUniqueRelationTypeId(relationType.Alias, relationType.Name); _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.RelationType, "id", false, relationType); } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 11de1209b3..d3a920a60b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -283,6 +283,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade // TO 9.4.0 To("{DBBA1EA0-25A1-4863-90FB-5D306FB6F1E1}"); + To("{DED98755-4059-41BB-ADBD-3FEAB12D1D7B}"); } } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs new file mode 100644 index 0000000000..1c8fe7ed72 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_9_4_0/UpdateRelationTypesToHandleDependencies.cs @@ -0,0 +1,34 @@ +using System.Linq; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_9_4_0 +{ + internal class UpdateRelationTypesToHandleDependencies : MigrationBase + { + public UpdateRelationTypesToHandleDependencies(IMigrationContext context) + : base(context) + { + } + + protected override void Migrate() + { + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList(); + + AddColumnIfNotExists(columns, "isDependency"); + + var aliasesWithDependencies = new[] + { + Core.Constants.Conventions.RelationTypes.RelatedDocumentAlias, + Core.Constants.Conventions.RelationTypes.RelatedMediaAlias + }; + + Database.Execute( + Sql() + .Update(u => u.Set(x => x.IsDependency, true)) + .WhereIn(x => x.Alias, aliasesWithDependencies)); + + } + } +} diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs index 50d7960ff8..388fa58941 100644 --- a/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs +++ b/src/Umbraco.Infrastructure/Persistence/Dtos/RelationTypeDto.cs @@ -40,5 +40,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos [Length(100)] [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] public string Alias { get; set; } + + [Constraint(Default = "0")] + [Column("isDependency")] + public bool IsDependency { get; set; } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs index 51f5261199..93cb74cd74 100644 --- a/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs +++ b/src/Umbraco.Infrastructure/Persistence/Factories/RelationTypeFactory.cs @@ -9,7 +9,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories public static IRelationType BuildEntity(RelationTypeDto dto) { - var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType); + var entity = new RelationType(dto.Name, dto.Alias, dto.Dual, dto.ParentObjectType, dto.ChildObjectType, dto.IsDependency); try { @@ -30,11 +30,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories public static RelationTypeDto BuildDto(IRelationType entity) { + var isDependency = false; + if (entity is IRelationTypeWithIsDependency relationTypeWithIsDependency) + { + isDependency = relationTypeWithIsDependency.IsDependency; + } var dto = new RelationTypeDto { Alias = entity.Alias, ChildObjectType = entity.ChildObjectType, Dual = entity.IsBidirectional, + IsDependency = isDependency, Name = entity.Name, ParentObjectType = entity.ParentObjectType, UniqueId = entity.Key @@ -47,6 +53,8 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories return dto; } + + #endregion } } diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs index 965a659631..732563fef7 100644 --- a/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs +++ b/src/Umbraco.Infrastructure/Persistence/Mappers/RelationTypeMapper.cs @@ -22,6 +22,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers DefineMap(nameof(RelationType.Alias), nameof(RelationTypeDto.Alias)); DefineMap(nameof(RelationType.ChildObjectType), nameof(RelationTypeDto.ChildObjectType)); DefineMap(nameof(RelationType.IsBidirectional), nameof(RelationTypeDto.Dual)); + DefineMap(nameof(RelationType.IsDependency), nameof(RelationTypeDto.IsDependency)); DefineMap(nameof(RelationType.Name), nameof(RelationTypeDto.Name)); DefineMap(nameof(RelationType.ParentObjectType), nameof(RelationTypeDto.ParentObjectType)); } diff --git a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs index ed51d4a182..6b7c34dc15 100644 --- a/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs +++ b/src/Umbraco.Infrastructure/Persistence/NPocoSqlExtensions.cs @@ -72,7 +72,27 @@ namespace Umbraco.Extensions /// The Sql statement. public static Sql WhereIn(this Sql sql, Expression> field, Sql values) { - return sql.WhereIn(field, values, false); + return WhereIn(sql, field, values, false, null); + } + + public static Sql WhereIn(this Sql sql, Expression> field, Sql values, string tableAlias) + { + return sql.WhereIn(field, values, false, tableAlias); + } + + + public static Sql WhereLike(this Sql sql, Expression> fieldSelector, Sql valuesSql) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + sql.Where(fieldName + " LIKE (" + valuesSql.SQL + ")", valuesSql.Arguments); + return sql; + } + + public static Sql WhereLike(this Sql sql, Expression> fieldSelector, string likeValue) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + sql.Where(fieldName + " LIKE ('" + likeValue + "')"); + return sql; } /// @@ -130,7 +150,12 @@ namespace Umbraco.Extensions private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql valuesSql, bool not) { - var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector); + return WhereIn(sql, fieldSelector, valuesSql, not, null); + } + + private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql valuesSql, bool not, string tableAlias) + { + var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector, tableAlias); sql.Where(fieldName + (not ? " NOT" : "") +" IN (" + valuesSql.SQL + ")", valuesSql.Arguments); return sql; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index 749fc9d77b..a18782ca82 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Cms.Core; @@ -171,6 +172,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement } public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + return GetPagedParentEntitiesByChildId(childId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + } + + public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data @@ -184,10 +190,36 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.Where(rel => rel.ChildId == childId); sql.Where((rel, node) => rel.ParentId == childId || node.NodeId != childId); + + if (relationTypes != null && relationTypes.Any()) + { + sql.WhereIn(rel => rel.RelationType, relationTypes); + } + }); + } + + public IEnumerable GetPagedParentEntitiesByChildIds(int[] childIds, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) + { + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.WhereIn(rel => rel.ChildId, childIds); + sql.WhereAny(s => s.WhereIn(rel => rel.ParentId, childIds), s => s.WhereNotIn(node => node.NodeId, childIds)); + + if (relationTypes != null && relationTypes.Any()) + { + sql.WhereIn(rel => rel.RelationType, relationTypes); + } }); } public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + return GetPagedChildEntitiesByParentId(parentId, pageIndex, pageSize, out totalRecords, new int[0], entityTypes); + } + + public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, int[] relationTypes, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data @@ -201,9 +233,29 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.Where(rel => rel.ParentId == parentId); sql.Where((rel, node) => rel.ChildId == parentId || node.NodeId != parentId); + + if (relationTypes != null && relationTypes.Any()) + { + sql.WhereIn(rel => rel.RelationType, relationTypes); + } }); } + public IEnumerable GetPagedEntitiesForItemsInRelation(int[] itemIds, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) + { + return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => + { + SqlJoinRelations(sql); + + sql.WhereIn(rel => rel.ChildId, itemIds); + sql.Where((rel, node) => rel.ChildId == node.NodeId); + sql.Where(type => type.IsDependency); + }); + } + + + + public void Save(IEnumerable relations) { foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) @@ -399,4 +451,42 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement sql.OrderByDescending(orderBy); } } + + internal class RelationItemDto + { + [Column(Name = "nodeId")] + public int ChildNodeId { get; set; } + + [Column(Name = "nodeKey")] + public Guid ChildNodeKey { get; set; } + + [Column(Name = "nodeName")] + public string ChildNodeName { get; set; } + + [Column(Name = "nodeObjectType")] + public Guid ChildNodeObjectType { get; set; } + + [Column(Name = "contentTypeIcon")] + public string ChildContentTypeIcon { get; set; } + + [Column(Name = "contentTypeAlias")] + public string ChildContentTypeAlias { get; set; } + + [Column(Name = "contentTypeName")] + public string ChildContentTypeName { get; set; } + + + + [Column(Name = "relationTypeName")] + public string RelationTypeName { get; set; } + + [Column(Name = "relationTypeAlias")] + public string RelationTypeAlias { get; set; } + + [Column(Name = "relationTypeIsDependency")] + public bool RelationTypeIsDependency { get; set; } + + [Column(Name = "relationTypeIsBidirectional")] + public bool RelationTypeIsBidirectional { get; set; } + } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs new file mode 100644 index 0000000000..f5fb945464 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement +{ + internal class TrackedReferencesRepository : ITrackedReferencesRepository + { + private readonly IScopeAccessor _scopeAccessor; + + public TrackedReferencesRepository(IScopeAccessor scopeAccessor) + { + _scopeAccessor = scopeAccessor; + } + + public IEnumerable GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, + bool filterMustBeIsDependency, out long totalRecords) + { + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"pn", aliasRight:"c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn"); + + if (ids.Any()) + { + sql = sql.Where(x => ids.Contains(x.NodeId), "pn"); + } + + if (filterMustBeIsDependency) + { + sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + + // Ordering is required for paging + sql = sql.OrderBy(x => x.Alias); + + var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + return pagedResult.Items.Select(MapDtoToEntity); + } + + public IEnumerable GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency, + out long totalRecords) + { + var syntax = _scopeAccessor.AmbientScope.Database.SqlContext.SqlSyntax; + + // Gets the path of the parent with ",%" added + var subsubQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() + .Select(syntax.GetConcat("[node].[path]", "',%'")) + .From("node") + .Where(x => x.NodeId == parentId, "node"); + + + // Gets the descendants of the parent node + Sql subQuery; + + if (_scopeAccessor.AmbientScope.Database.DatabaseType.IsSqlCe()) + { + // SqlCE do not support nested selects that returns a scalar. So we need to do this in multiple queries + + var pathForLike = _scopeAccessor.AmbientScope.Database.ExecuteScalar(subsubQuery); + + subQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .WhereLike(x => x.Path, pathForLike); + } + else + { + subQuery = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() + .Select(x => x.NodeId) + .From() + .WhereLike(x => x.Path, subsubQuery); + } + + + + // Get all relations where parent is in the sub query + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[pn].[id] as nodeId", + "[pn].[uniqueId] as nodeKey", + "[pn].[text] as nodeName", + "[pn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"pn", aliasRight:"c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn") + .WhereIn((System.Linq.Expressions.Expression>)(x => x.NodeId), subQuery, "pn"); + if (filterMustBeIsDependency) + { + sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + // Ordering is required for paging + sql = sql.OrderBy(x => x.Alias); + + var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + return pagedResult.Items.Select(MapDtoToEntity); + } + + public IEnumerable GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency, out long totalRecords) + { + var sql = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select( + "[cn].[id] as nodeId", + "[cn].[uniqueId] as nodeKey", + "[cn].[text] as nodeName", + "[cn].[nodeObjectType] as nodeObjectType", + "[ct].[icon] as contentTypeIcon", + "[ct].[alias] as contentTypeAlias", + "[ctn].[text] as contentTypeName", + "[umbracoRelationType].[alias] as relationTypeAlias", + "[umbracoRelationType].[name] as relationTypeName", + "[umbracoRelationType].[isDependency] as relationTypeIsDependency", + "[umbracoRelationType].[dual] as relationTypeIsBidirectional") + .From("r") + .InnerJoin("umbracoRelationType").On((left, right) => left.RelationType == right.Id, aliasLeft: "r", aliasRight:"umbracoRelationType") + .InnerJoin("cn").On((r, cn, rt) => (!rt.Dual && r.ParentId == cn.NodeId) || (rt.Dual && (r.ChildId == cn.NodeId || r.ParentId == cn.NodeId )), aliasLeft: "r", aliasRight:"cn", aliasOther: "umbracoRelationType" ) + .InnerJoin("pn").On((r, pn, cn) => (pn.NodeId == r.ChildId && cn.NodeId == r.ParentId) || (pn.NodeId == r.ParentId && cn.NodeId == r.ChildId), aliasLeft: "r", aliasRight:"pn", aliasOther:"cn" ) + .LeftJoin("c").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"cn", aliasRight:"c") + .LeftJoin("ct").On((left, right) => left.ContentTypeId == right.NodeId, aliasLeft:"c", aliasRight:"ct") + .LeftJoin("ctn").On((left, right) => left.NodeId == right.NodeId, aliasLeft:"ct", aliasRight:"ctn"); + + if (ids.Any()) + { + sql = sql.Where(x => ids.Contains(x.NodeId), "pn"); + } + + if (filterMustBeIsDependency) + { + sql = sql.Where(rt => rt.IsDependency, "umbracoRelationType"); + } + + // Ordering is required for paging + sql = sql.OrderBy(x => x.Alias); + + var pagedResult = _scopeAccessor.AmbientScope.Database.Page(pageIndex + 1, pageSize, sql); + totalRecords = Convert.ToInt32(pagedResult.TotalItems); + + return pagedResult.Items.Select(MapDtoToEntity); + } + + private RelationItem MapDtoToEntity(RelationItemDto dto) + { + var type = ObjectTypes.GetUdiType(dto.ChildNodeObjectType); + return new RelationItem() + { + NodeId = dto.ChildNodeId, + NodeKey = dto.ChildNodeKey, + NodeType = ObjectTypes.GetUdiType(dto.ChildNodeObjectType), + NodeName = dto.ChildNodeName, + RelationTypeName = dto.RelationTypeName, + RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, + RelationTypeIsDependency = dto.RelationTypeIsDependency, + ContentTypeAlias = dto.ChildContentTypeAlias, + ContentTypeIcon = dto.ChildContentTypeIcon, + ContentTypeName = dto.ChildContentTypeName, + }; + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs b/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs index 7a5d10c222..d52411a9c8 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/RelationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; diff --git a/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs b/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs new file mode 100644 index 0000000000..0f1eefb6f2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/TrackedReferencesService.cs @@ -0,0 +1,50 @@ +using System.Linq; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; + +namespace Umbraco.Cms.Core.Services.Implement +{ + public class TrackedReferencesService : ITrackedReferencesService + { + private readonly ITrackedReferencesRepository _trackedReferencesRepository; + private readonly IScopeProvider _scopeProvider; + private readonly IEntityService _entityService; + + public TrackedReferencesService(ITrackedReferencesRepository trackedReferencesRepository, IScopeProvider scopeProvider, IEntityService entityService) + { + _trackedReferencesRepository = trackedReferencesRepository; + _scopeProvider = scopeProvider; + _entityService = entityService; + } + + public PagedResult GetPagedRelationsForItems(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + var items = _trackedReferencesRepository.GetPagedRelationsForItems(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + + return new PagedResult(totalItems, pageIndex, pageSize) { Items = items }; + } + + public PagedResult GetPagedItemsWithRelations(int[] ids, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + var items = _trackedReferencesRepository.GetPagedItemsWithRelations(ids, pageIndex, pageSize, filterMustBeIsDependency, out var totalItems); + + return new PagedResult(totalItems, pageIndex, pageSize) { Items = items }; + } + + public PagedResult GetPagedDescendantsInReferences(int parentId, long pageIndex, int pageSize, bool filterMustBeIsDependency) + { + using IScope scope = _scopeProvider.CreateScope(autoComplete: true); + + var items = _trackedReferencesRepository.GetPagedDescendantsInReferences( + parentId, + pageIndex, + pageSize, + filterMustBeIsDependency, + out var totalItems); + return new PagedResult(totalItems, pageIndex, pageSize) { Items = items }; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 0d9d8b903d..b74c4ea7b0 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -100,7 +100,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers var keepOnlyKeys = new Dictionary { {"umbracoUrls", new[] {"authenticationApiBaseUrl", "serverVarsJs", "externalLoginsUrl", "currentUserApiBaseUrl", "previewHubUrl", "iconApiBaseUrl"}}, - {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum", "hideBackofficeLogo"}}, + {"umbracoSettings", new[] {"allowPasswordReset", "imageFileTypes", "maxFileSize", "loginBackgroundImage", "loginLogoImage", "canSendRequiredEmail", "usernameIsEmail", "minimumPasswordLength", "minimumPasswordNonAlphaNum", "hideBackofficeLogo", "disableDeleteWhenReferenced", "disableUnpublishWhenReferenced"}}, {"application", new[] {"applicationPath", "cacheBuster"}}, {"isDebuggingEnabled", new string[] { }}, {"features", new [] {"disabledFeatures"}} @@ -378,6 +378,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { "previewHubUrl", _previewRoutes.GetPreviewHubRoute() }, + { + "trackedReferencesApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetPagedReferences(0, 1, 1, false)) + } } }, { @@ -409,6 +413,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers {"loginBackgroundImage", _contentSettings.LoginBackgroundImage}, {"loginLogoImage", _contentSettings.LoginLogoImage }, {"hideBackofficeLogo", _contentSettings.HideBackOfficeLogo }, + {"disableDeleteWhenReferenced", _contentSettings.DisableDeleteWhenReferenced }, + {"disableUnpublishWhenReferenced", _contentSettings.DisableUnpublishWhenReferenced }, {"showUserInvite", _emailSender.CanSendRequiredEmail()}, {"canSendRequiredEmail", _emailSender.CanSendRequiredEmail()}, {"showAllowSegmentationForDocumentTypes", false}, diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs index b901d5b431..d8fa641891 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaController.cs @@ -33,12 +33,9 @@ using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Web.BackOffice.ActionResults; using Umbraco.Cms.Web.BackOffice.Authorization; -using Umbraco.Cms.Web.BackOffice.Extensions; using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.BackOffice.ModelBinders; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; using Umbraco.Extensions; @@ -1085,7 +1082,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return new ActionResult(toMove); } - + [Obsolete("Please use TrackedReferencesController.GetPagedReferences() instead. Scheduled for removal in V11.")] public PagedResult GetPagedReferences(int id, string entityType, int pageNumber = 1, int pageSize = 100) { if (pageNumber <= 0 || pageSize <= 0) diff --git a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs index d63f6b0eda..197148a2a3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/RelationTypeController.cs @@ -148,7 +148,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers relationType.Name.ToSafeAlias(_shortStringHelper, true), relationType.IsBidirectional, relationType.ParentObjectType, - relationType.ChildObjectType); + relationType.ChildObjectType, + relationType.IsDependency); try { diff --git a/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs new file mode 100644 index 0000000000..2cef8d61af --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/TrackedReferencesController.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.BackOffice.ModelBinders; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + [PluginController(Constants.Web.Mvc.BackOfficeApiArea)] + [Authorize(Policy = AuthorizationPolicies.SectionAccessContentOrMedia)] + public class TrackedReferencesController : BackOfficeNotificationsController + { + private readonly ITrackedReferencesService _relationService; + private readonly IEntityService _entityService; + + public TrackedReferencesController(ITrackedReferencesService relationService, + IEntityService entityService) + { + _relationService = relationService; + _entityService = entityService; + } + + // Used by info tabs on content, media etc. So this is basically finding childs of relations. + public ActionResult> GetPagedReferences(int id, int pageNumber = 1, + int pageSize = 100, bool filterMustBeIsDependency = false) + { + if (pageNumber <= 0 || pageSize <= 0) + { + return BadRequest("Both pageNumber and pageSize must be greater than zero"); + } + + return _relationService.GetPagedRelationsForItems(new []{id}, pageNumber - 1, pageSize, filterMustBeIsDependency); + } + + // Used on delete, finds + public ActionResult> GetPagedDescendantsInReferences(int parentId, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true) + { + if (pageNumber <= 0 || pageSize <= 0) + { + return BadRequest("Both pageNumber and pageSize must be greater than zero"); + } + + + return _relationService.GetPagedDescendantsInReferences(parentId, pageNumber - 1, pageSize, filterMustBeIsDependency); + + } + + // Used by unpublish content. So this is basically finding parents of relations. + [HttpGet] + [HttpPost] + public ActionResult> GetPagedReferencedItems([FromJsonPath] int[] ids, int pageNumber = 1, int pageSize = 100, bool filterMustBeIsDependency = true) + { + if (pageNumber <= 0 || pageSize <= 0) + { + return BadRequest("Both pageNumber and pageSize must be greater than zero"); + } + + return _relationService.GetPagedItemsWithRelations(ids, pageNumber - 1, pageSize, filterMustBeIsDependency); + + } + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js index c07777ca60..2a65c67a8d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -8,22 +8,6 @@ var evts = []; scope.allowChangeMediaType = false; - scope.loading = true; - - scope.changeContentPageNumber = changeContentPageNumber; - scope.contentOptions = {}; - scope.contentOptions.entityType = "DOCUMENT"; - scope.hasContentReferences = false; - - scope.changeMediaPageNumber = changeMediaPageNumber; - scope.mediaOptions = {}; - scope.mediaOptions.entityType = "MEDIA"; - scope.hasMediaReferences = false; - - scope.changeMemberPageNumber = changeMemberPageNumber; - scope.memberOptions = {}; - scope.memberOptions.entityType = "MEMBER"; - scope.hasMemberReferences = false; function onInit() { @@ -110,45 +94,6 @@ setMediaExtension(); }); - function changeContentPageNumber(pageNumber) { - scope.contentOptions.pageNumber = pageNumber; - loadContentRelations(); - } - - function changeMediaPageNumber(pageNumber) { - scope.mediaOptions.pageNumber = pageNumber; - loadMediaRelations(); - } - - function changeMemberPageNumber(pageNumber) { - scope.memberOptions.pageNumber = pageNumber; - loadMemberRelations(); - } - - function loadContentRelations() { - return mediaResource.getPagedReferences(scope.node.id, scope.contentOptions) - .then(function (data) { - scope.contentReferences = data; - scope.hasContentReferences = data.items.length > 0; - }); - } - - function loadMediaRelations() { - return mediaResource.getPagedReferences(scope.node.id, scope.mediaOptions) - .then(data => { - scope.mediaReferences = data; - scope.hasMediaReferences = data.items.length > 0; - }); - } - - function loadMemberRelations() { - return mediaResource.getPagedReferences(scope.node.id, scope.memberOptions) - .then(data => { - scope.memberReferences = data; - scope.hasMemberReferences = data.items.length > 0; - }); - } - //ensure to unregister from all events! scope.$on('$destroy', function () { for (var e in evts) { @@ -157,18 +102,6 @@ }); onInit(); - - // load media type references when the 'info' tab is first activated/switched to - evts.push(eventsService.on("app.tabChange", function (event, args) { - $timeout(function () { - if (args.alias === "umbInfo") { - - $q.all([loadContentRelations(), loadMediaRelations(), loadMemberRelations()]).then(function () { - scope.loading = false; - }); - } - }); - })); } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js new file mode 100644 index 0000000000..000e87146c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferences.component.js @@ -0,0 +1,125 @@ +(function () { + 'use strict'; + + /** + * A component to render the tracked references of an item + */ + + function umbTrackedReferencesController($q, trackedReferencesResource, localizationService) { + + var vm = this; + + vm.changeReferencesPageNumber = changeReferencesPageNumber; + vm.changeDescendantsPageNumber = changeDescendantsPageNumber; + + vm.$onInit = onInit; + + function onInit() { + + vm.referencesTitle = this.hideNoneDependencies ? "The following items depends on this" : "Referenced by the following items"; + vm.referencedDescendantsTitle = this.hideNoneDependencies ? "The following descending items has dependencies" : "The following descending items are referenced"; + + localizationService.localize(this.hideNoneDependencies ? "references_labelDependsOnThis" : "references_labelUsedByItems").then(function (value) { + vm.referencesTitle = value; + }); + + localizationService.localize(this.hideNoneDependencies ? "references_labelDependentDescendants" : "references_labelUsedDescendants").then(function (value) { + vm.referencedDescendantsTitle = value; + }); + + vm.descendantsOptions = {}; + vm.descendantsOptions.filterMustBeIsDependency = this.hideNoneDependencies; + vm.hasReferencesInDescendants = false; + + vm.referencesOptions = {}; + vm.referencesOptions.filterMustBeIsDependency = this.hideNoneDependencies; + vm.hasReferences = false; + + this.loading = true; + this.hideNoResult = this.hideNoResult || false; + + // when vm.id == 0 it means that this is a new item, so it has no references yet + if (vm.id === 0) { + vm.loading = false; + if(vm.onLoadingComplete) { + vm.onLoadingComplete(); + } + return; + } + + // Make array of promises to load: + var promises = [loadReferencesRelations()]; + + // only load descendants if we want to show them. + if (vm.showDescendants) { + promises.push(loadDescendantsUsage()); + } + + $q.all(promises).then(function () { + vm.loading = false; + if(vm.onLoadingComplete) { + vm.onLoadingComplete(); + } + }); + } + + function changeReferencesPageNumber(pageNumber) { + vm.referencesOptions.pageNumber = pageNumber; + loadReferencesRelations(); + } + + function changeDescendantsPageNumber(pageNumber) { + vm.descendantsOptions.pageNumber = pageNumber; + loadDescendantsUsage(); + } + + function loadReferencesRelations() { + return trackedReferencesResource.getPagedReferences(vm.id, vm.referencesOptions) + .then(function (data) { + vm.references = data; + + if (data.items.length > 0) { + vm.hasReferences = data.items.length > 0; + activateWarning(); + } + }); + } + + function loadDescendantsUsage() { + return trackedReferencesResource.getPagedDescendantsInReferences(vm.id, vm.descendantsOptions) + .then(function (data) { + vm.referencedDescendants = data; + + if (data.items.length > 0) { + vm.hasReferencesInDescendants = data.items.length > 0; + activateWarning(); + } + }); + } + + function activateWarning() { + if (vm.onWarning) { + vm.onWarning(); + } + } + } + + var umbTrackedReferencesComponent = { + templateUrl: 'views/components/references/umb-tracked-references.html', + transclude: true, + bindings: { + id: "<", + hideNoResult: " s.id); + + return trackedReferencesResource.getPagedReferencedItems(ids, vm.referencesOptions) + .then(function (data) { + vm.referencedItems = data; + + if (data.items.length > 0) { + vm.hasReferences = data.items.length > 0; + activateWarning(); + } + }); + } + + function activateWarning() { + if (vm.onWarning) { + vm.onWarning(); + } + } + } + + var umbTrackedReferencesBulkActionComponent = { + templateUrl: 'views/components/references/umb-tracked-references-bulk-action.html', + transclude: true, + bindings: { + selection: "<", + hideNoResult: " + * var options = { + * pageSize : 25, + * pageNumber : 1, + * entityType : 'DOCUMENT' + * }; + * trackedReferencesResource.getPagedReferences(1, options) + * .then(function(data) { + * console.log(data); + * }); + * + * + * @param {int} id Id of the item to query for tracked references + * @param {Object} args optional args object + * @param {Int} args.pageSize the pagesize of the returned list (default 25) + * @param {Int} args.pageNumber the current page index (default 1) + * @param {Int} args.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ getPagedReferences: function (id, options) { - - var defaults = { - pageSize: 25, - pageNumber: 1, - entityType: "DOCUMENT" - }; - if (options === undefined) { - options = {}; - } - //overwrite the defaults if there are any specified - Utilities.extend(defaults, options); - //now copy back to the options we will use - options = defaults; - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "mediaApiBaseUrl", - "GetPagedReferences", - { - id: id, - entityType: options.entityType, - pageNumber: options.pageNumber, - pageSize: options.pageSize - } - )), - "Failed to retrieve usages for media of id " + id); + return trackedReferencesResource.getPagedReferences(id, options); } - }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js new file mode 100644 index 0000000000..cd64c89589 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js @@ -0,0 +1,178 @@ +/** + * @ngdoc service + * @name umbraco.resources.trackedReferencesResource + * @description Loads in data for tracked references + **/ +function trackedReferencesResource($q, $http, umbRequestHelper) { + + return { + + /** + * @ngdoc method + * @name umbraco.resources.trackedReferencesResource#getPagedReferences + * @methodOf umbraco.resources.trackedReferencesResource + * + * @description + * Gets a page list of tracked references for the current item, so you can see where an item is being used + * + * ##usage + *
+         * var options = {
+         *      pageSize : 25,
+         *      pageNumber : 1,
+         *      entityType : 'DOCUMENT'
+         *  };
+         * trackedReferencesResource.getPagedReferences(1, options)
+         *    .then(function(data) {
+         *        console.log(data);
+         *    });
+         * 
+ * + * @param {int} id Id of the item to query for tracked references + * @param {Object} args optional args object + * @param {Int} args.pageSize the pagesize of the returned list (default 25) + * @param {Int} args.pageNumber the current page index (default 1) + * @param {String} args.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ + getPagedReferences: function (id, args) { + + var defaults = { + pageSize: 10, + pageNumber: 1, + entityType: "DOCUMENT" + }; + if (args === undefined) { + args = {}; + } + + //overwrite the defaults if there are any specified + var options = Utilities.extend(defaults, args); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "trackedReferencesApiBaseUrl", + "GetPagedReferences", + { + id: id, + entityType: options.entityType, + pageNumber: options.pageNumber, + pageSize: options.pageSize, + filterMustBeIsDependency: options.filterMustBeIsDependency + } + )), + "Failed to retrieve usages for entity of id " + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.trackedReferencesResource#getPagedDescendantsInReferences + * @methodOf umbraco.resources.trackedReferencesResource + * + * @description + * Gets a page list of the child nodes of the current item used in any kind of relation + * + * ##usage + *
+         * var options = {
+         *      pageSize : 25,
+         *      pageNumber : 1,
+         *      entityType : 'DOCUMENT'
+         *  };
+         * trackedReferencesResource.getPagedDescendantsInReferences(1, options)
+         *    .then(function(data) {
+         *        console.log(data);
+         *    });
+         * 
+ * + * @param {int} id Id of the item to query for child nodes used in relation + * @param {Object} args optional args object + * @param {Int} args.pageSize the pagesize of the returned list (default 25) + * @param {Int} args.pageNumber the current page index (default 1) + * @param {String} args.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ + getPagedDescendantsInReferences: function (id, args) { + + var defaults = { + pageSize: 10, + pageNumber: 1, + entityType: "DOCUMENT" + }; + if (args === undefined) { + args = {}; + } + + //overwrite the defaults if there are any specified + var options = Utilities.extend(defaults, args); + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "trackedReferencesApiBaseUrl", + "GetPagedDescendantsInReferences", + { + parentId: id, + entityType: options.entityType, + pageNumber: options.pageNumber, + pageSize: options.pageSize + } + )), + "Failed to retrieve usages for descendants of parent with id " + id); + }, + + /** + * @ngdoc method + * @name umbraco.resources.trackedReferencesResource#getPagedReferencedItems + * @methodOf umbraco.resources.trackedReferencesResource + * + * @description + * Checks if any of the items are used in a relation and returns a page list, so you can see which items are being used + * + * ##usage + *
+         * var ids = [123,3453,2334,2343];
+         * var options = {
+         *      pageSize : 25,
+         *      pageNumber : 1,
+         *      entityType : 'DOCUMENT'
+         *  };
+         *
+         * trackedReferencesResource.getPagedReferencedItems(ids, options)
+         *    .then(function(data) {
+         *        console.log(data);
+         *    });
+         * 
+ * + * @param {Array} ids array of the selected items ids to query for references + * @param {Object} options optional options object + * @param {Int} options.pageSize the pagesize of the returned list (default 25) + * @param {Int} options.pageNumber the current page index (default 1) + * @param {String} options.entityType the type of tracked entity (default : DOCUMENT). Possible values DOCUMENT, MEDIA + * @returns {Promise} resourcePromise object. + * + */ + getPagedReferencedItems: function (ids, options) { + var query = `entityType=${options.entityType}&pageNumber=${options.pageNumber}&pageSize=${options.pageSize}`; + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "trackedReferencesApiBaseUrl", + "getPagedReferencedItems", + query), + { + ids: ids, + entityType: options.entityType, + pageNumber: options.pageNumber, + pageSize: options.pageSize + }), + "Failed to check for references of nodes with ids " + ids); + } + } +} + +angular.module('umbraco.resources').factory('trackedReferencesResource', trackedReferencesResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index a8f62dca4b..38b56b7661 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -461,6 +461,7 @@ alias: relationType.alias, key: relationType.key, isBidirectional: relationType.isBidirectional, + isDependency: relationType.isDependency, parentObjectType: relationType.parentObjectType, childObjectType: relationType.childObjectType }; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 12cce286d6..60020066c0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -10,6 +10,10 @@ .scoped-view{ display: none; } + + .abstract { + margin-bottom : 20px; + } } .umb-overlay__form { @@ -51,7 +55,7 @@ } .umb-overlay__title { - font-size: 16px; + font-size: 20px; color: @black; line-height: 16px; font-weight: bold; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index 6c1e5058d2..73ef47133c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -162,6 +162,7 @@ input.umb-table__input { font-size: 14px; font-weight: bold; text-decoration: none; + cursor: pointer; &:hover { color: @ui-option-type-hover; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 381880da2e..6429e39db6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -20,6 +20,9 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index 1e72950410..010102b920 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -3,7 +3,7 @@ -
+
@@ -27,131 +27,11 @@ - - - - - - This Media item has no references. - - - - - -
- -
- -
- Used in Documents -
- -
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
-
- - -
- -
- Used in Members -
- -
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
-
- - -
- -
- Used in Media -
- -
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
-
- -
+
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-bulk-action.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-bulk-action.html new file mode 100644 index 0000000000..8d3c36f196 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-bulk-action.html @@ -0,0 +1,13 @@ + + +
+ +
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html new file mode 100644 index 0000000000..afc8f9a3e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html @@ -0,0 +1,35 @@ +
+
+ {{vm.headline}} +
+ +
+
+
+
+
Node Name
+
Type Name
+
Type
+
Relation
+
+
+
+
+
+ +
{{::reference.contentTypeName}}
+
{{::reference.type}}
+
{{::reference.relationTypeName}}
+
+
+
+ + +
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html new file mode 100644 index 0000000000..fd788cc598 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html @@ -0,0 +1,36 @@ + + + + + + + This item is not referenced + + + + +
+ + +
+ + +
+ +
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js index 060c0ec7d9..771d1b9280 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js @@ -6,7 +6,7 @@ * @description * The controller for deleting content */ -function ContentDeleteController($scope, $timeout, contentResource, treeService, navigationService, editorState, $location, overlayService, languageResource) { +function ContentDeleteController($scope, $timeout, contentResource, treeService, navigationService, editorState, $location, overlayService, languageResource, localizationService) { /** * Used to toggle UI elements during delete operations @@ -16,6 +16,10 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, $scope.currentNode.loading = isDeleting; $scope.busy = isDeleting; } + + $scope.checkingReferences = true; + $scope.warningText = null; + $scope.disableDelete = false; $scope.performDelete = function() { @@ -77,6 +81,27 @@ function ContentDeleteController($scope, $timeout, contentResource, treeService, }; + $scope.checkingReferencesComplete = () => { + $scope.checkingReferences = false; + }; + + $scope.onReferencesWarning = () => { + // check if the deletion of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableDeleteWhenReferenced) { + // this will only be set to true if we have a warning, indicating that this item or its descendants have reference + $scope.disableDelete = true; + + localizationService.localize("references_deleteDisabledWarning").then((value) => { + $scope.warningText = value; + }); + } + else { + localizationService.localize("references_deleteWarning").then((value) => { + $scope.warningText = value; + }); + } + }; + $scope.cancel = function() { toggleDeleting(false); $scope.close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/delete.html b/src/Umbraco.Web.UI.Client/src/views/content/delete.html index 316d0669c4..765dbda531 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/delete.html @@ -1,7 +1,7 @@ -
-
+
+
-
+
{{currentNode.name}} was deleted @@ -9,20 +9,28 @@
-
-

+

+

Are you sure you want to delete {{currentNode.name}}?

-
+
This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead.
+ + +
+ {{warningText}} +
+
When items are deleted from the recycle bin, they will be gone forever.
- + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js index 936ab3b104..f6c0ea67f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js @@ -6,10 +6,13 @@ var vm = this; var autoSelectedVariants = []; + vm.id = $scope.content.id; + vm.warningText = null; vm.changeSelection = changeSelection; function onInit() { + $scope.model.hideSubmitButton = true; vm.variants = $scope.model.variants; vm.unpublishableVariants = vm.variants.filter(publishedVariantFilter) @@ -108,6 +111,24 @@ }); }); + vm.checkingReferencesComplete = () => { + $scope.model.hideSubmitButton = false; + }; + + vm.onReferencesWarning = () => { + $scope.model.submitButtonStyle = "danger"; + + // check if the unpublishing of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableUnpublishWhenReferenced) { + // this will only be disabled if we have a warning, indicating that this item or its descendants have reference + $scope.model.disableSubmitButton = true; + } + + localizationService.localize("references_unpublishWarning").then((value) => { + vm.warningText = value; + }); + }; + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html index a994fe38ed..b938065bc2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html @@ -49,7 +49,14 @@
-
+ +
+ +
+ +
+ {{vm.warningText}} +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/delete.html b/src/Umbraco.Web.UI.Client/src/views/media/delete.html index 7231ccf2c4..894cf5ef77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/delete.html @@ -10,15 +10,23 @@
-

+

Are you sure you want to delete {{currentNode.name}}?

+ + +
+ {{warningText}} +
+
When items are deleted from the recycle bin, they will be gone forever.
- + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index 8792571377..0761504b6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -6,7 +6,11 @@ * @description * The controller for deleting content */ -function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService) { +function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService, localizationService) { + + $scope.checkingReferences = true; + $scope.warningText = null; + $scope.disableDelete = false; $scope.performDelete = function() { @@ -26,7 +30,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer treeService.removeNode($scope.currentNode); if (rootNode) { - //ensure the recycle bin has child nodes now + //ensure the recycle bin has child nodes now var recycleBin = treeService.getDescendantNode(rootNode, -21); if (recycleBin) { recycleBin.hasChildren = true; @@ -65,6 +69,27 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer }); }; + $scope.checkingReferencesComplete = () => { + $scope.checkingReferences = false; + }; + + $scope.onReferencesWarning = () => { + // check if the deletion of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableDeleteWhenReferenced) { + // this will only be set to true if we have a warning, indicating that this item or its descendants have reference + $scope.disableDelete = true; + + localizationService.localize("references_deleteDisabledWarning").then((value) => { + $scope.warningText = value; + }); + } + else { + localizationService.localize("references_deleteWarning").then((value) => { + $scope.warningText = value; + }); + } + }; + $scope.close = function() { navigationService.hideDialog(); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index c98cd8b039..c3c5dff69b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -389,6 +389,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time view: "views/propertyeditors/listview/overlays/delete.html", deletesVariants: selectionHasVariants(), isTrashed: $scope.isTrashed, + selection: $scope.selection, submitButtonLabelKey: "contentTypeEditor_yesDelete", submitButtonStyle: "danger", submit: function (model) { @@ -498,8 +499,8 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time view: "views/propertyeditors/listview/overlays/listviewunpublish.html", submitButtonLabelKey: "actions_unpublish", submitButtonStyle: "warning", + selection: $scope.selection, submit: function (model) { - // create a comma separated array of selected cultures let selectedCultures = []; if (model.languages && model.languages.length > 0) { @@ -509,7 +510,6 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time } }); } - performUnpublish(selectedCultures); overlayService.close(); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.controller.js new file mode 100644 index 0000000000..85c8b82620 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.controller.js @@ -0,0 +1,40 @@ +(function () { + "use strict"; + + function ListViewDeleteController($scope, localizationService) { + + var vm = this; + vm.loading = true; + vm.disableDelete = false; + + function onInit() { + $scope.model.hideSubmitButton = true; + } + + vm.checkingReferencesComplete = () => { + $scope.model.hideSubmitButton = false; + }; + + vm.onReferencesWarning = () => { + $scope.model.submitButtonStyle = "danger"; + + // check if the deletion of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableDeleteWhenReferenced) { + // this will only be set to true if we have a warning, indicating that this item or its descendants have reference + vm.disableDelete = true; + $scope.model.disableSubmitButton = true; + } + + localizationService.localize("general_delete").then(function (action) { + localizationService.localize("references_listViewDialogWarning", [action.toLowerCase()]).then((value) => { + vm.warningText = value; + }); + }); + }; + + onInit(); + } + + angular.module("umbraco").controller("Umbraco.Overlays.ListViewDeleteController", ListViewDeleteController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html index f8b2b008d3..b137c74260 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/delete.html @@ -1,9 +1,15 @@ -
+
-

+

Are you sure you want to delete?

+ + +
+ {{vm.warningText}} +
+
This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead.
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js index 650b1b8438..cf432c0318 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js @@ -5,6 +5,7 @@ var vm = this; vm.loading = true; + vm.warningText = null; vm.changeSelection = changeSelection; @@ -29,7 +30,9 @@ function onInit() { + vm.selection = $scope.model.selection; vm.languages = $scope.model.languages; + $scope.model.hideSubmitButton = true; if (!$scope.model.title) { localizationService.localize("content_unpublish").then(function (value) { @@ -65,6 +68,26 @@ vm.loading = false; } + vm.checkingReferencesComplete = () => { + $scope.model.hideSubmitButton = false; + }; + + vm.onReferencesWarning = () => { + $scope.model.submitButtonStyle = "danger"; + + // check if the unpublishing of items that have references has been disabled + if (Umbraco.Sys.ServerVariables.umbracoSettings.disableUnpublishWhenReferenced) { + // this will only be disabled if we have a warning, indicating that this item or its descendants have reference + $scope.model.disableSubmitButton = true; + } + + localizationService.localize("content_unpublish").then(function (action) { + localizationService.localize("references_listViewDialogWarning", [action.toLowerCase()]).then((value) => { + vm.warningText = value; + }); + }); + }; + onInit(); //when this dialog is closed, reset all 'publish' flags diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html index 9a6af50f3e..2d255cbb25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html @@ -5,6 +5,14 @@

Unpublishing will remove the selected items and all their descendants from the site.

+
+ +
+ +
+ {{vm.warningText}} +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html index 4e4711cc09..e910dd31ce 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/create.html @@ -55,6 +55,34 @@ + + + + +

+ +

+
    +
  • + +
  • +
  • + +
  • +
+
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js index c8e0af0aa9..01b5367eab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationTypes/views/relationType.settings.controller.js @@ -16,12 +16,17 @@ function RelationTypeSettingsController($scope, localizationService) { var labelKeys = [ "relationType_parentToChild", - "relationType_bidirectional" + "relationType_bidirectional", + "relationType_dependency", + "relationType_noDependency" + ]; localizationService.localizeMany(labelKeys).then(function (data) { vm.labels.parentToChild = data[0]; vm.labels.bidirectional = data[1]; + vm.labels.dependency = data[2]; + vm.labels.noDependency = data[3]; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 684ce6d2f0..79164a2457 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -374,7 +374,7 @@ } function enableUser() { - vm.enableUserButtonState = "busy"; + vm.enableUserButtonState = "busfy"; usersResource.enableUsers([vm.user.id]).then(function (data) { vm.user.userState = "Active"; setUserDisplayState(); diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 3d489a038a..6b5d301c5f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -2395,6 +2395,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont No relations for this Relation Type Relation Type Relations + Is Dependency + Yes + No Getting Started @@ -2472,6 +2475,15 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Used in Documents Used in Members Used in Media + Items in use + Descendants in use + One or more of this item's descendants is being used in a media item. + One or more of this item's descendants is being used in a content item. + One or more of this item's descendants is being used in a member. + This item or its descendants is being used. Deletion can lead to broken links on your website. + This item or its descendants is being used. Unpublishing can lead to broken links on your website. Please take the appropriate actions. + This item or its descendants is being used. Therefore, deletion has been disabled. + The following items you are trying to %0% are used by other content. Delete Saved Search diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index e3f22f90e8..e056647ebc 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -809,6 +809,7 @@ New Next No + Node Name of Off OK @@ -849,6 +850,7 @@ Submit Success Type + Type Name Type to search... under Up @@ -2470,6 +2472,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Parent Child Count + Relation Relations Created Comment @@ -2477,6 +2480,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont No relations for this Relation Type Relation Type Relations + Is Dependency + Yes + No Getting Started @@ -2547,13 +2553,26 @@ To manage your website, simply open the Umbraco backoffice and start adding cont References This Data Type has no references. - Used in Document Types - Used in Media Types - Used in Member Types - Used by - Used in Documents - Used in Members - Used in Media + This item has no references. + Referenced by the following Document Types + Referenced by the following Media Types + Referenced by the following Member Types + Referenced by + Referenced by the following items + The following items depends on this + Referenced by the following Documents + Referenced by the following Members + Referenced by the following Media + The following items are referenced + The following descending items are referenced + The following descending items has dependencies + One or more of this item's descendants is being referenced in a media item. + One or more of this item's descendants is being referenced in a content item. + One or more of this item's descendants is being referenced in a member. + This item or its descendants is being referenced. Deletion can lead to broken links on your website. + This item or its descendants is being referenced. Unpublishing can lead to broken links on your website. Please take the appropriate actions. + This item or its descendants is being referenced. Therefore, deletion has been disabled. + The following items you are trying to %0% are referenced by other content. Delete Saved Search diff --git a/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs index 2bd9dc124d..bb16cadd78 100644 --- a/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/RelationTypeBuilder.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Tests.Common.Builders.Interfaces; namespace Umbraco.Cms.Tests.Common.Builders { public class RelationTypeBuilder - : ChildBuilderBase, + : ChildBuilderBase, IWithIdBuilder, IWithAliasBuilder, IWithNameBuilder, @@ -23,6 +23,7 @@ namespace Umbraco.Cms.Tests.Common.Builders private DateTime? _deleteDate; private int? _id; private bool? _isBidirectional; + private bool? _isDependency; private Guid? _key; private string _name; private Guid? _parentObjectType; @@ -42,6 +43,12 @@ namespace Umbraco.Cms.Tests.Common.Builders { _isBidirectional = isBidirectional; return this; + } + + public RelationTypeBuilder WithIsDependency(bool isDependency) + { + _isDependency = isDependency; + return this; } public RelationTypeBuilder WithChildObjectType(Guid childObjectType) @@ -56,7 +63,7 @@ namespace Umbraco.Cms.Tests.Common.Builders return this; } - public override IRelationType Build() + public override IRelationTypeWithIsDependency Build() { var alias = _alias ?? Guid.NewGuid().ToString(); var name = _name ?? Guid.NewGuid().ToString(); @@ -65,11 +72,12 @@ namespace Umbraco.Cms.Tests.Common.Builders var id = _id ?? 0; Guid key = _key ?? Guid.NewGuid(); var isBidirectional = _isBidirectional ?? false; + var isDependency = _isDependency ?? false; DateTime createDate = _createDate ?? DateTime.Now; DateTime updateDate = _updateDate ?? DateTime.Now; DateTime? deleteDate = _deleteDate ?? null; - return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType) + return new RelationType(name, alias, isBidirectional, parentObjectType, childObjectType, isDependency) { Id = id, Key = key, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs index ee01816f09..bdd5257b45 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationRepositoryTest.cs @@ -189,23 +189,23 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Get parent entities for child id var parents = repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 11, out long totalRecords).ToList(); - Assert.AreEqual(6, totalRecords); - Assert.AreEqual(6, parents.Count); + Assert.AreEqual(9, totalRecords); + Assert.AreEqual(9, parents.Count); - // add the next page + // Add the next page parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 1, 11, out totalRecords)); - Assert.AreEqual(6, totalRecords); - Assert.AreEqual(6, parents.Count); + Assert.AreEqual(9, totalRecords); + Assert.AreEqual(9, parents.Count); var contentEntities = parents.OfType().ToList(); var mediaEntities = parents.OfType().ToList(); var memberEntities = parents.OfType().ToList(); Assert.AreEqual(3, contentEntities.Count); - Assert.AreEqual(0, mediaEntities.Count); + Assert.AreEqual(3, mediaEntities.Count); Assert.AreEqual(3, memberEntities.Count); - // only of a certain type + // Only of a certain type parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Document.GetGuid())); Assert.AreEqual(3, totalRecords); @@ -213,7 +213,32 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos Assert.AreEqual(3, totalRecords); parents.AddRange(repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Media.GetGuid())); - Assert.AreEqual(0, totalRecords); + Assert.AreEqual(3, totalRecords); + + // Test relations on content + var contentParents = repository.GetPagedParentEntitiesByChildId(createdContent[0].Id, 0, int.MaxValue, out totalRecords).ToList(); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, contentParents.Count); + + // Test getting relations of specified relation types + var relatedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + var relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); + + parents = repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 0, 11, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, parents.Count); + + parents = repository.GetPagedParentEntitiesByChildId(createdMedia[0].Id, 1, 11, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(0, parents.Count); + + parents = repository.GetPagedParentEntitiesByChildId(createdContent[0].Id, 0, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(3, parents.Count); + + parents = repository.GetPagedParentEntitiesByChildId(createdContent[0].Id, 1, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(0, parents.Count); } } @@ -255,19 +280,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos // Get parent entities for child id var parents = repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 6, out long totalRecords).ToList(); - Assert.AreEqual(3, totalRecords); - Assert.AreEqual(3, parents.Count); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, parents.Count); - // add the next page + // Add the next page parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 1, 6, out totalRecords)); - Assert.AreEqual(3, totalRecords); - Assert.AreEqual(3, parents.Count); + Assert.AreEqual(6, totalRecords); + Assert.AreEqual(6, parents.Count); var contentEntities = parents.OfType().ToList(); var mediaEntities = parents.OfType().ToList(); var memberEntities = parents.OfType().ToList(); - Assert.AreEqual(0, contentEntities.Count); + Assert.AreEqual(3, contentEntities.Count); Assert.AreEqual(3, mediaEntities.Count); Assert.AreEqual(0, memberEntities.Count); @@ -280,6 +305,18 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos parents.AddRange(repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 100, out totalRecords, UmbracoObjectTypes.Member.GetGuid())); Assert.AreEqual(0, totalRecords); + + // Test getting relations of specified relation types + IRelationType relatedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + IRelationType relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); + + parents = repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 0, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(3, parents.Count); + + parents = repository.GetPagedChildEntitiesByParentId(createdContent[0].Id, 1, 6, out totalRecords, new int[] { relatedContentRelType.Id, relatedMediaRelType.Id }).ToList(); + Assert.AreEqual(3, totalRecords); + Assert.AreEqual(0, parents.Count); } } @@ -296,6 +333,15 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos createdContent.Add(c1); } + // Create related content + var relatedContent = new List(); + for (int i = 0; i < 3; i++) + { + Content c1 = ContentBuilder.CreateBasicContent(contentType); + ContentService.Save(c1); + relatedContent.Add(c1); + } + // Create media createdMedia = new List(); MediaType imageType = MediaTypeBuilder.CreateImageMediaType("myImage"); @@ -313,14 +359,24 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos createdMembers = MemberBuilder.CreateSimpleMembers(memberType, 3).ToList(); GetMemberService().Save(createdMembers); - IRelationType relType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + IRelationType relatedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedMediaAlias); + IRelationType relatedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelatedDocumentAlias); // Relate content to media foreach (IContent content in createdContent) { foreach (IMedia media in createdMedia) { - RelationService.Relate(content.Id, media.Id, relType); + RelationService.Relate(content.Id, media.Id, relatedMediaRelType); + } + } + + // Relate content to content + foreach (IContent relContent in relatedContent) + { + foreach (IContent content in createdContent) + { + RelationService.Relate(relContent.Id, content.Id, relatedContentRelType); } } @@ -329,7 +385,67 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos { foreach (IMedia media in createdMedia) { - RelationService.Relate(member.Id, media.Id, relType); + RelationService.Relate(member.Id, media.Id, relatedMediaRelType); + } + } + + // Create copied content + var copiedContent = new List(); + for (int i = 0; i < 3; i++) + { + Content c1 = ContentBuilder.CreateBasicContent(contentType); + ContentService.Save(c1); + copiedContent.Add(c1); + } + + IRelationType copiedContentRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias); + + // Relate content to content (mimics copy) + foreach (IContent content in createdContent) + { + foreach (IContent cpContent in copiedContent) + { + RelationService.Relate(content.Id, cpContent.Id, copiedContentRelType); + } + } + + // Create trashed content + var trashedContent = new List(); + for (int i = 0; i < 3; i++) + { + Content c1 = ContentBuilder.CreateBasicContent(contentType); + ContentService.Save(c1); + trashedContent.Add(c1); + } + + IRelationType trashedRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias); + + // Relate to trashed content + foreach (IContent trContent in trashedContent) + { + foreach (IContent content in createdContent) + { + RelationService.Relate(trContent.Id, content.Id, trashedRelType); + } + } + + // Create trashed media + var trashedMedia = new List(); + for (int i = 0; i < 3; i++) + { + Media m1 = MediaBuilder.CreateMediaImage(imageType, -1); + MediaService.Save(m1); + trashedMedia.Add(m1); + } + + IRelationType trashedMediaRelType = RelationService.GetRelationTypeByAlias(Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias); + + // Relate to trashed media + foreach (IMedia trMedia in trashedMedia) + { + foreach (IMedia media in createdMedia) + { + RelationService.Relate(trMedia.Id, media.Id, trashedMediaRelType); } } } @@ -417,14 +533,16 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos "relateContentOnCopy", true, Constants.ObjectTypes.Document, - new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972")); + new Guid("C66BA18E-EAF3-4CFF-8A22-41B16D66A972"), + false); _relateContentType = new RelationType( "Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, - new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB")); + new Guid("A2CB7800-F571-4787-9638-BC48539A0EFB"), + false); using (IScope scope = ScopeProvider.CreateScope()) { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs index 4b0a23464f..16c88e7cf9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -36,7 +36,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos RelationTypeRepository repository = CreateRepository(provider); // Act - var relateMemberToContent = new RelationType("Relate Member to Content", "relateMemberToContent", true, Constants.ObjectTypes.Member, Constants.ObjectTypes.Document); + var relateMemberToContent = new RelationType("Relate Member to Content", "relateMemberToContent", true, Constants.ObjectTypes.Member, Constants.ObjectTypes.Document, true); repository.Save(relateMemberToContent); @@ -101,12 +101,13 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos RelationTypeRepository repository = CreateRepository(provider); // Act - IRelationType relationType = repository.Get(8); + var relationType = repository.Get(8) as IRelationTypeWithIsDependency; // Assert Assert.That(relationType, Is.Not.Null); Assert.That(relationType.HasIdentity, Is.True); Assert.That(relationType.IsBidirectional, Is.True); + Assert.That(relationType.IsDependency, Is.True); Assert.That(relationType.Alias, Is.EqualTo("relateContentToMedia")); Assert.That(relationType.Name, Is.EqualTo("Relate Content to Media")); Assert.That(relationType.ChildObjectType, Is.EqualTo(Constants.ObjectTypes.Media)); @@ -215,9 +216,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos public void CreateTestData() { - var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Document); - var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentType); - var relateContentMedia = new RelationType("Relate Content to Media", "relateContentToMedia", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Media); + var relateContent = new RelationType("Relate Content on Copy", "relateContentOnCopy", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Document, false); + var relateContentType = new RelationType("Relate ContentType on Copy", "relateContentTypeOnCopy", true, Constants.ObjectTypes.DocumentType, Constants.ObjectTypes.DocumentType, false); + var relateContentMedia = new RelationType("Relate Content to Media", "relateContentToMedia", true, Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, true); IScopeProvider provider = ScopeProvider; using (IScope scope = provider.CreateScope()) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs index 5b033fcd42..4f0c489516 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentServiceTests.cs @@ -1970,7 +1970,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services UserService.Save(admin); RelationService.Save(new RelationType("test", "test", false, Constants.ObjectTypes.Document, - Constants.ObjectTypes.Document)); + Constants.ObjectTypes.Document, false)); Assert.IsNotNull(RelationService.Relate(content1, content2, "test")); PublicAccessService.Save(new PublicAccessEntry(content1, content2, content2, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs index 3edba86760..0156100f0a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/RelationServiceTests.cs @@ -266,4 +266,4 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // TODO: Create a relation for entities of the wrong Entity Type (GUID) based on the Relation Type's defined parent/child object types } -} +} \ No newline at end of file diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs index 67f887d123..bde8b8f092 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationBuilderTests.cs @@ -44,6 +44,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders .WithAlias(relationTypeAlias) .WithName(relationTypeName) .WithIsBidirectional(false) + .WithIsDependency(true) .WithParentObjectType(parentObjectType) .WithChildObjectType(childObjectType) .Done() @@ -61,6 +62,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders Assert.AreEqual(relationTypeAlias, relation.RelationType.Alias); Assert.AreEqual(relationTypeName, relation.RelationType.Name); Assert.IsFalse(relation.RelationType.IsBidirectional); + + Assert.IsTrue((relation.RelationType as IRelationTypeWithIsDependency).IsDependency); Assert.AreEqual(parentObjectType, relation.RelationType.ParentObjectType); Assert.AreEqual(childObjectType, relation.RelationType.ChildObjectType); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs index 877023f82f..e99f5ae273 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.Common/Builders/RelationTypeBuilderTests.cs @@ -26,11 +26,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders var parentObjectType = Guid.NewGuid(); var childObjectType = Guid.NewGuid(); const bool isBidirectional = true; + const bool isDependency = true; var builder = new RelationTypeBuilder(); // Act - IRelationType relationType = builder + var relationType = builder .WithId(id) .WithAlias(alias) .WithName(name) @@ -41,6 +42,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders .WithParentObjectType(parentObjectType) .WithChildObjectType(childObjectType) .WithIsBidirectional(isBidirectional) + .WithIsDependency(isDependency) .Build(); // Assert @@ -54,6 +56,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Tests.Common.Builders Assert.AreEqual(parentObjectType, relationType.ParentObjectType); Assert.AreEqual(childObjectType, relationType.ChildObjectType); Assert.AreEqual(isBidirectional, relationType.IsBidirectional); + Assert.AreEqual(isDependency, relationType.IsDependency); } } }