diff --git a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs index 3b67c25856..942073543c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IRelationRepository.cs @@ -9,6 +9,12 @@ namespace Umbraco.Core.Persistence.Repositories { IEnumerable GetPagedRelationsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering); + /// + /// Persist multiple at once + /// + /// + void Save(IEnumerable relations); + /// /// Deletes all relations for a parent for any specified relation type alias /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index d93fd72bfb..65f6dc0472 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -853,21 +853,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty()) .ToDictionary(x => x.Alias, x => x); - foreach(var rel in trackedRelations) - { - if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) - throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); + var toSave = trackedRelations.Select(rel => + { + if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out var relationType)) + throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); - if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) - continue; // This shouldn't happen! + if (!udiToGuids.TryGetValue(rel.Udi, out var guid)) + return null; // This shouldn't happen! - if (!keyToIds.TryGetValue(guid, out var id)) - continue; // This shouldn't happen! + if (!keyToIds.TryGetValue(guid, out var id)) + return null; // This shouldn't happen! - //Create new relation - //TODO: This is N+1, we could do this all in one operation, just need a new method on the relations repo - RelationRepository.Save(new Relation(entity.Id, id, relationType)); - } + return new Relation(entity.Id, id, relationType); + }).WhereNotNull(); + + // Save bulk relations + RelationRepository.Save(toSave); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs index 7765086079..5ff0f8d26d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs @@ -158,6 +158,50 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #endregion + public void Save(IEnumerable relations) + { + foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) + { + if (hasIdentityGroup.Key) + { + // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation + // however we can bulk populate the object types. It might be possible to bulk update + // with SQL but would be pretty ugly and we're not really too worried about that for perf, + // it's the bulk inserts we care about. + var asArray = hasIdentityGroup.ToArray(); + foreach (var relation in hasIdentityGroup) + { + relation.UpdatingEntity(); + var dto = RelationFactory.BuildDto(relation); + Database.Update(dto); + } + PopulateObjectTypes(asArray); + } + else + { + // Do bulk inserts + var entitiesAndDtos = hasIdentityGroup.ToDictionary( + r => // key = entity + { + r.AddingEntity(); + return r; + }, + RelationFactory.BuildDto); // value = DTO + + Database.InsertBulk(entitiesAndDtos.Values); + + // All dtos now have IDs assigned + foreach (var de in entitiesAndDtos) + { + // re-assign ID to the entity + de.Key.Id = de.Value.Id; + } + + PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); + } + } + } + public IEnumerable GetPagedRelationsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) { var sql = GetBaseQuery(false); @@ -189,6 +233,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return result; } + public void DeleteByParent(int parentId, params string[] relationTypeAliases) { var subQuery = Sql().Select(x => x.Id) @@ -204,19 +249,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); } - private void PopulateObjectTypes(IRelation entity) + /// + /// Used to populate the object types after insert/update + /// + /// + private void PopulateObjectTypes(params IRelation[] entities) { - var nodes = Database.Fetch(Sql().Select().From().Where(x => x.NodeId == entity.ChildId || x.NodeId == entity.ParentId)) + var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); + + var nodes = Database.Fetch(Sql().Select().From() + .WhereIn(x => x.NodeId, entityIds)) .ToDictionary(x => x.NodeId, x => x.NodeObjectType); - if(nodes.TryGetValue(entity.ParentId, out var parentObjectType)) + foreach (var e in entities) { - entity.ParentObjectType = parentObjectType.GetValueOrDefault(); - } - - if(nodes.TryGetValue(entity.ChildId, out var childObjectType)) - { - entity.ChildObjectType = childObjectType.GetValueOrDefault(); + if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) + { + e.ParentObjectType = parentObjectType.GetValueOrDefault(); + } + if (nodes.TryGetValue(e.ChildId, out var childObjectType)) + { + e.ChildObjectType = childObjectType.GetValueOrDefault(); + } } } diff --git a/src/Umbraco.Core/Services/IRelationService.cs b/src/Umbraco.Core/Services/IRelationService.cs index 1410b05531..80ef74d539 100644 --- a/src/Umbraco.Core/Services/IRelationService.cs +++ b/src/Umbraco.Core/Services/IRelationService.cs @@ -296,6 +296,8 @@ namespace Umbraco.Core.Services /// Relation to save void Save(IRelation relation); + void Save(IEnumerable relations); + /// /// Saves a /// diff --git a/src/Umbraco.Core/Services/Implement/RelationService.cs b/src/Umbraco.Core/Services/Implement/RelationService.cs index a9d98383c7..05d37d7444 100644 --- a/src/Umbraco.Core/Services/Implement/RelationService.cs +++ b/src/Umbraco.Core/Services/Implement/RelationService.cs @@ -427,6 +427,24 @@ namespace Umbraco.Core.Services.Implement } } + public void Save(IEnumerable relations) + { + using (var scope = ScopeProvider.CreateScope()) + { + var saveEventArgs = new SaveEventArgs(relations); + if (scope.Events.DispatchCancelable(SavingRelation, this, saveEventArgs)) + { + scope.Complete(); + return; + } + + _relationRepository.Save(relations); + scope.Complete(); + saveEventArgs.CanCancel = false; + scope.Events.Dispatch(SavedRelation, this, saveEventArgs); + } + } + /// public void Save(IRelationType relationType) { diff --git a/src/Umbraco.Tests/Services/RelationServiceTests.cs b/src/Umbraco.Tests/Services/RelationServiceTests.cs index 8406179d9a..2ec10811b7 100644 --- a/src/Umbraco.Tests/Services/RelationServiceTests.cs +++ b/src/Umbraco.Tests/Services/RelationServiceTests.cs @@ -86,7 +86,7 @@ namespace Umbraco.Tests.Services ServiceContext.ContentService.Save(content); } - for (var i = 0; i < 6; i++) + for (var i = 0; i < 6; i++) createContentWithMediaRefs(); //create 6 content items referencing the same media var relations = ServiceContext.RelationService.GetByChildId(m1.Id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); @@ -132,7 +132,7 @@ namespace Umbraco.Tests.Services [Test] public void Relation_Returns_Parent_Child_Object_Types_When_Creating() { - var r = CreateNewRelation("Test", "test"); + var r = CreateAndSaveRelation("Test", "test"); Assert.AreEqual(Constants.ObjectTypes.Document, r.ParentObjectType); Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); @@ -141,7 +141,7 @@ namespace Umbraco.Tests.Services [Test] public void Relation_Returns_Parent_Child_Object_Types_When_Getting() { - var r = CreateNewRelation("Test", "test"); + var r = CreateAndSaveRelation("Test", "test"); // re-get r = ServiceContext.RelationService.GetById(r.Id); @@ -150,7 +150,47 @@ namespace Umbraco.Tests.Services Assert.AreEqual(Constants.ObjectTypes.Media, r.ChildObjectType); } - private IRelation CreateNewRelation(string name, string alias) + [Test] + public void Insert_Bulk_Relations() + { + var rs = ServiceContext.RelationService; + + var newRelations = CreateRelations(10); + + Assert.IsTrue(newRelations.All(x => !x.HasIdentity)); + + ServiceContext.RelationService.Save(newRelations); + + Assert.IsTrue(newRelations.All(x => x.HasIdentity)); + } + + [Test] + public void Update_Bulk_Relations() + { + var rs = ServiceContext.RelationService; + + var date = DateTime.Now.AddDays(-10); + var newRelations = CreateRelations(10); + foreach (var r in newRelations) + { + r.CreateDate = date; + r.UpdateDate = date; + } + + //insert + ServiceContext.RelationService.Save(newRelations); + Assert.IsTrue(newRelations.All(x => x.UpdateDate == date)); + + var newDate = DateTime.Now.AddDays(-5); + foreach (var r in newRelations) + r.UpdateDate = newDate; + + //update + ServiceContext.RelationService.Save(newRelations); + Assert.IsTrue(newRelations.All(x => x.UpdateDate == newDate)); + } + + private IRelation CreateAndSaveRelation(string name, string alias) { var rs = ServiceContext.RelationService; var rt = new RelationType(name, alias, false, null, null); @@ -173,6 +213,35 @@ namespace Umbraco.Tests.Services return r; } + /// + /// Creates a bunch of content/media items return relation objects for them (unsaved) + /// + /// + /// + private IEnumerable CreateRelations(int count) + { + var rs = ServiceContext.RelationService; + var rtName = Guid.NewGuid().ToString(); + var rt = new RelationType(rtName, rtName, false, null, null); + rs.Save(rt); + + var ct = MockedContentTypes.CreateBasicContentType(); + ServiceContext.ContentTypeService.Save(ct); + + var mt = MockedContentTypes.CreateImageMediaType("img"); + ServiceContext.MediaTypeService.Save(mt); + + return Enumerable.Range(1, count).Select(index => + { + var c1 = MockedContent.CreateBasicContent(ct); + var c2 = MockedMedia.CreateMediaImage(mt, -1); + ServiceContext.ContentService.Save(c1); + ServiceContext.MediaService.Save(c2); + + return new Relation(c1.Id, c2.Id, rt); + }).ToList(); + } + //TODO: Create a relation for entities of the wrong Entity Type (GUID) based on the Relation Type's defined parent/child object types } } 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 4993b013c7..18aa457862 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 @@ -1,13 +1,15 @@ (function () { 'use strict'; - function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper) { + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper, editorService, mediaHelper, mediaResource, $routeParams) { function link(scope, element, attrs, ctrl) { var evts = []; + var referencesLoaded = false; scope.allowChangeMediaType = false; + scope.loading = true; function onInit() { @@ -94,6 +96,19 @@ setMediaExtension(); }); + /** Loads in the media references one time */ + function loadRelations() { + if (!referencesLoaded) { + referencesLoaded = true; + mediaResource.getReferences($routeParams.id) + .then(function (data) { + scope.loading = false; + scope.references = data; + scope.hasReferences = data.content.length > 0 || data.members.length > 0; + }); + } + } + //ensure to unregister from all events! scope.$on('$destroy', function () { for (var e in evts) { @@ -102,6 +117,15 @@ }); 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") { + loadRelations(); + } + }); + })); } var directive = { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index ca7700c188..58c02b55df 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -552,8 +552,31 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { "Search", args)), 'Failed to retrieve media items for search: ' + query); - } + }, + /** + * @ngdoc method + * @name umbraco.resources.mediaResource#getReferences + * @methodOf umbraco.resources.mediaResource + * + * @description + * Retrieves references of a given media item. + * + * @param {Int} id id of media node to retrieve references for + * @returns {Promise} resourcePromise object. + * + */ + getReferences: function (id) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "mediaApiBaseUrl", + "GetReferences", + { id: id })), + "Failed to retrieve usages for media of id " + id); + + } }; } 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 4f7141559c..a82e1897a1 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 @@ -1,20 +1,25 @@
-
+ + + + +
+ + - +
-
+ +
+ @@ -39,12 +141,11 @@ - + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 19c8642dc5..c95f059934 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2181,6 +2181,9 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Member Types No references to Member Types. Used by + Used in Documents + Used in Members + Used in Media Log Levels 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 0c572fba26..e4cd5765c1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2197,6 +2197,9 @@ To manage your website, simply open the Umbraco back office and start adding con Used in Member Types No references to Member Types. Used by + Used in Documents + Used in Members + Used in Media Log Levels diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index a673f06e1d..36750d74ec 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -36,6 +36,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Web.ContentApps; using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; +using Umbraco.Core.Models.Entities; namespace Umbraco.Web.Editors { @@ -943,5 +944,71 @@ namespace Umbraco.Web.Editors return hasPathAccess; } + + /// + /// Returns the references (usages) for the media item + /// + /// + /// + public MediaReferences GetReferences(int id) + { + var result = new MediaReferences(); + + var relations = Services.RelationService.GetByChildId(id, Constants.Conventions.RelationTypes.RelatedMediaAlias).ToList(); + var relationEntities = Services.RelationService.GetParentEntitiesFromRelations(relations).ToList(); + + var documents = new List(); + var members = new List(); + var media = new List(); + + foreach (var item in relationEntities) + { + switch (item) + { + case DocumentEntitySlim doc: + documents.Add(new MediaReferences.EntityTypeReferences { + Id = doc.Id, + Key = doc.Key, + Udi = Udi.Create(Constants.UdiEntityType.Document, doc.Key), + Icon = doc.ContentTypeIcon, + Name = doc.Name, + Alias = doc.ContentTypeAlias + }); + break; + + case MemberEntitySlim memb: + members.Add(new MediaReferences.EntityTypeReferences + { + Id = memb.Id, + Key = memb.Key, + Udi = Udi.Create(Constants.UdiEntityType.Member, memb.Key), + Icon = memb.ContentTypeIcon, + Name = memb.Name, + Alias = memb.ContentTypeAlias + }); + break; + + case MediaEntitySlim med: + media.Add(new MediaReferences.EntityTypeReferences + { + Id = med.Id, + Key = med.Key, + Udi = Udi.Create(Constants.UdiEntityType.Media, med.Key), + Icon = med.ContentTypeIcon, + Name = med.Name, + Alias = med.ContentTypeAlias + }); + break; + + default: + break; + } + } + + result.Content = documents; + result.Members = members; + result.Media = media; + return result; + } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs new file mode 100644 index 0000000000..b10022b105 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/MediaReferences.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "mediaReferences", Namespace = "")] + public class MediaReferences + { + [DataMember(Name = "content")] + public IEnumerable Content { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "members")] + public IEnumerable Members { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "media")] + public IEnumerable Media { get; set; } = Enumerable.Empty(); + + [DataContract(Name = "entityType", Namespace = "")] + public class EntityTypeReferences : EntityBasic + { + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 60b70ed9a8..361a548123 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -222,6 +222,7 @@ + @@ -1283,4 +1284,4 @@ - + \ No newline at end of file