Merge remote-tracking branch 'origin/v8/feature/media-tracking' into v8/feature/AB3466-paged-relations

# Conflicts:
#	src/Umbraco.Core/Persistence/Repositories/Implement/RelationRepository.cs
This commit is contained in:
Shannon
2019-11-11 21:29:52 +11:00
14 changed files with 437 additions and 41 deletions

View File

@@ -9,6 +9,12 @@ namespace Umbraco.Core.Persistence.Repositories
{
IEnumerable<IRelation> GetPagedRelationsByQuery(IQuery<IRelation> query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering);
/// <summary>
/// Persist multiple <see cref="IRelation"/> at once
/// </summary>
/// <param name="relations"></param>
void Save(IEnumerable<IRelation> relations);
/// <summary>
/// Deletes all relations for a parent for any specified relation type alias
/// </summary>

View File

@@ -853,21 +853,22 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty<int>())
.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);
}

View File

@@ -158,6 +158,50 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
#endregion
public void Save(IEnumerable<IRelation> 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<IRelation> GetPagedRelationsByQuery(IQuery<IRelation> 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<RelationDto>(x => x.Id)
@@ -204,19 +249,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.Execute(Sql().Delete<RelationDto>().WhereIn<RelationDto>(x => x.Id, subQuery));
}
private void PopulateObjectTypes(IRelation entity)
/// <summary>
/// Used to populate the object types after insert/update
/// </summary>
/// <param name="entities"></param>
private void PopulateObjectTypes(params IRelation[] entities)
{
var nodes = Database.Fetch<NodeDto>(Sql().Select<NodeDto>().From<NodeDto>().Where<NodeDto>(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<NodeDto>(Sql().Select<NodeDto>().From<NodeDto>()
.WhereIn<NodeDto>(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();
}
}
}

View File

@@ -296,6 +296,8 @@ namespace Umbraco.Core.Services
/// <param name="relation">Relation to save</param>
void Save(IRelation relation);
void Save(IEnumerable<IRelation> relations);
/// <summary>
/// Saves a <see cref="IRelationType"/>
/// </summary>

View File

@@ -427,6 +427,24 @@ namespace Umbraco.Core.Services.Implement
}
}
public void Save(IEnumerable<IRelation> relations)
{
using (var scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new SaveEventArgs<IRelation>(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);
}
}
/// <inheritdoc />
public void Save(IRelationType relationType)
{

View File

@@ -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;
}
/// <summary>
/// Creates a bunch of content/media items return relation objects for them (unsaved)
/// </summary>
/// <param name="count"></param>
/// <returns></returns>
private IEnumerable<IRelation> 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
}
}

View File

@@ -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 = {

View File

@@ -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);
}
};
}

View File

@@ -1,20 +1,25 @@
<div class="umb-package-details">
<div class="umb-package-details__main-content">
<umb-load-indicator ng-if="loading === true"></umb-load-indicator>
<!-- Main Column -->
<div class="umb-package-details__main-content" ng-if="loading === false">
<!-- Links -->
<umb-box data-element="node-info-urls">
<umb-box-header title-key="general_links"></umb-box-header>
<umb-box-content class="block-form">
<umb-empty-state
ng-if="!nodeUrl"
size="small">
<umb-empty-state ng-if="!nodeUrl"
size="small">
<localize key="content_noMediaLink"></localize>
</umb-empty-state>
<ul ng-if="nodeUrl" class="nav nav-stacked" style="margin-bottom: 0;">
<li>
<a ng-attr-href="{{node.extension !== 'svg' ? nodeUrl : undefined}}" ng-click="node.extension === 'svg' && openSVG()" target="_blank">
<i class="icon icon-out"></i>
<span>{{nodeFileName}}</span>
<i class="icon icon-out"></i>
<span>{{nodeFileName}}</span>
</a>
</li>
@@ -23,9 +28,106 @@
</umb-box-content>
</umb-box>
<!-- Media Tracking (NO Items) -->
<umb-box ng-if="loading === false && hasReferences === false">
<umb-box-header title-key="references_tabName"></umb-box-header>
<umb-box-content>
<umb-empty-state size="small">
This Media item has no references.
</umb-empty-state>
</umb-box-content>
</umb-box>
<!-- Media Tracking (With Items) -->
<div ng-if="loading === false && hasReferences === true">
<!-- Content -->
<div ng-if="references.content.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="references_labelUsedByDocuments">Used in Documents</localize>
</h5>
<div class="umb-table">
<div class="umb-table-head">
<div class="umb-table-row">
<div class="umb-table-cell"></div>
<div class="umb-table-cell umb-table__name not-fixed"><localize key="general_name">Name</localize></div>
<div class="umb-table-cell"><localize key="content_alias">Alias</localize></div>
<div class="umb-table-cell umb-table-cell--nano"><localize key="general_open" style="visibility:hidden;">Open</localize></div>
</div>
</div>
<div class="umb-table-body">
<div class="umb-table-row" ng-repeat="reference in references.content">
<div class="umb-table-cell"><i class="umb-table-body__icon {{reference.icon}}"></i></div>
<div class="umb-table-cell umb-table__name"><span>{{::reference.name}}</span></div>
<div class="umb-table-cell"><span title="{{::reference.alias}}">{{::reference.alias}}</span></div>
<div class="umb-table-cell umb-table-cell--nano"><a ng-href="#/content/content/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
</div>
</div>
</div>
</div>
<!-- Members -->
<div ng-if="references.members.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="references_labelUsedByMembers">Used in Members</localize>
</h5>
<div class="umb-table">
<div class="umb-table-head">
<div class="umb-table-row">
<div class="umb-table-cell"></div>
<div class="umb-table-cell umb-table__name not-fixed"><localize key="general_name">Name</localize></div>
<div class="umb-table-cell"><localize key="content_alias">Alias</localize></div>
<div class="umb-table-cell umb-table-cell--nano"><localize key="general_open" style="visibility:hidden;">Open</localize></div>
</div>
</div>
<div class="umb-table-body">
<div class="umb-table-row" ng-repeat="reference in references.members">
<div class="umb-table-cell"><i class="umb-table-body__icon {{reference.icon}}"></i></div>
<div class="umb-table-cell umb-table__name"><span>{{::reference.name}}</span></div>
<div class="umb-table-cell"><span title="{{::reference.alias}}">{{::reference.alias}}</span></div>
<div class="umb-table-cell umb-table-cell--nano"><a href="#/member/member/edit/{{::reference.key}}"><localize key="general_open">Open</localize></a></div>
</div>
</div>
</div>
</div>
<!-- Media -->
<div ng-if="references.media.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="references_labelUsedByMedia">Used in Media</localize>
</h5>
<div class="umb-table">
<div class="umb-table-head">
<div class="umb-table-row">
<div class="umb-table-cell"></div>
<div class="umb-table-cell umb-table__name not-fixed"><localize key="general_name">Name</localize></div>
<div class="umb-table-cell"><localize key="content_alias">Alias</localize></div>
<div class="umb-table-cell umb-table-cell--nano"><localize key="general_open" style="visibility:hidden;">Open</localize></div>
</div>
</div>
<div class="umb-table-body">
<div class="umb-table-row" ng-repeat="reference in references.media">
<div class="umb-table-cell"><i class="umb-table-body__icon {{reference.icon}}"></i></div>
<div class="umb-table-cell umb-table__name"><span>{{::reference.name}}</span></div>
<div class="umb-table-cell"><span title="{{::reference.alias}}">{{::reference.alias}}</span></div>
<div class="umb-table-cell umb-table-cell--nano"><a href="#/media/media/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="umb-package-details__sidebar">
<!-- Sidebar -->
<div class="umb-package-details__sidebar" ng-if="loading === false">
<!-- General Info -->
<umb-box data-element="node-info-general">
<umb-box-header title-key="general_general"></umb-box-header>
<umb-box-content class="block-form">
@@ -39,12 +141,11 @@
</umb-control-group>
<umb-control-group data-element="node-info-media-type" label="@content_mediatype">
<umb-node-preview
style="max-width: 100%; margin-bottom: 0px;"
icon="node.icon"
name="node.contentTypeName"
allow-open="allowChangeMediaType"
on-open="openMediaType(mediaType)">
<umb-node-preview style="max-width: 100%; margin-bottom: 0px;"
icon="node.icon"
name="node.contentTypeName"
allow-open="allowChangeMediaType"
on-open="openMediaType(mediaType)">
</umb-node-preview>
</umb-control-group>

View File

@@ -2181,6 +2181,9 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="labelUsedByMemberTypes">Used in Member Types</key>
<key alias="noMemberTypes">No references to Member Types.</key>
<key alias="usedByProperties">Used by</key>
<key alias="labelUsedByDocuments">Used in Documents</key>
<key alias="labelUsedByMembers">Used in Members</key>
<key alias="labelUsedByMedia">Used in Media</key>
</area>
<area alias="logViewer">
<key alias="logLevels">Log Levels</key>

View File

@@ -2197,6 +2197,9 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="labelUsedByMemberTypes">Used in Member Types</key>
<key alias="noMemberTypes">No references to Member Types.</key>
<key alias="usedByProperties">Used by</key>
<key alias="labelUsedByDocuments">Used in Documents</key>
<key alias="labelUsedByMembers">Used in Members</key>
<key alias="labelUsedByMedia">Used in Media</key>
</area>
<area alias="logViewer">
<key alias="logLevels">Log Levels</key>

View File

@@ -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;
}
/// <summary>
/// Returns the references (usages) for the media item
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
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<MediaReferences.EntityTypeReferences>();
var members = new List<MediaReferences.EntityTypeReferences>();
var media = new List<MediaReferences.EntityTypeReferences>();
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;
}
}
}

View File

@@ -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<EntityTypeReferences> Content { get; set; } = Enumerable.Empty<EntityTypeReferences>();
[DataMember(Name = "members")]
public IEnumerable<EntityTypeReferences> Members { get; set; } = Enumerable.Empty<EntityTypeReferences>();
[DataMember(Name = "media")]
public IEnumerable<EntityTypeReferences> Media { get; set; } = Enumerable.Empty<EntityTypeReferences>();
[DataContract(Name = "entityType", Namespace = "")]
public class EntityTypeReferences : EntityBasic
{
}
}
}

View File

@@ -222,6 +222,7 @@
<Compile Include="Models\ContentEditing\LinkDisplay.cs" />
<Compile Include="Models\ContentEditing\MacroDisplay.cs" />
<Compile Include="Models\ContentEditing\MacroParameterDisplay.cs" />
<Compile Include="Models\ContentEditing\MediaReferences.cs" />
<Compile Include="Models\ContentEditing\UrlAndAnchors.cs" />
<Compile Include="Models\Mapping\CommonMapper.cs" />
<Compile Include="Models\Mapping\MapperContextExtensions.cs" />
@@ -1283,4 +1284,4 @@
<Output TaskParameter="SerializationAssembly" ItemName="SerializationAssembly" />
</SGen>
</Target>
</Project>
</Project>