Merge pull request #6625 from umbraco/v8/feature/AB2913-DataTypeTracking

Data type usage reporting and deletion warning
This commit is contained in:
Bjarke Berg
2019-10-21 10:10:37 +02:00
committed by GitHub
31 changed files with 3034 additions and 2367 deletions
@@ -7,5 +7,12 @@ namespace Umbraco.Core.Persistence.Repositories
public interface IDataTypeRepository : IReadWriteQueryRepository<int, IDataType>
{
IEnumerable<MoveEventInfo<IDataType>> Move(IDataType toMove, EntityContainer container);
/// <summary>
/// Returns a dictionary of content type <see cref="Udi"/>s and the property type aliases that use a <see cref="IDataType"/>
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
IReadOnlyDictionary<Udi, IEnumerable<string>> FindUsages(int id);
}
}
@@ -279,6 +279,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return moveInfo;
}
public IReadOnlyDictionary<Udi, IEnumerable<string>> FindUsages(int id)
{
if (id == default)
return new Dictionary<Udi, IEnumerable<string>>();
var sql = Sql()
.Select<ContentTypeDto>(ct => ct.Select(node => node.NodeDto))
.AndSelect<PropertyTypeDto>(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName"))
.From<PropertyTypeDto>()
.InnerJoin<ContentTypeDto>().On<ContentTypeDto, PropertyTypeDto>(ct => ct.NodeId, pt => pt.ContentTypeId)
.InnerJoin<NodeDto>().On<NodeDto, ContentTypeDto>(n => n.NodeId, ct => ct.NodeId)
.Where<PropertyTypeDto>(pt => pt.DataTypeId == id)
.OrderBy<NodeDto>(node => node.NodeId)
.AndBy<PropertyTypeDto>(pt => pt.Alias);
var dtos = Database.FetchOneToMany<ContentTypeReferenceDto>(ct => ct.PropertyTypes, sql);
return dtos.ToDictionary(
x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType.Value), x.NodeDto.UniqueId).EnsureClosed(),
x => (IEnumerable<string>)x.PropertyTypes.Select(p => p.Alias).ToList());
}
private string EnsureUniqueNodeName(string nodeName, int id = 0)
{
var template = SqlContext.Templates.Get("Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName", tsql => tsql
@@ -291,5 +313,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return SimilarNodeName.GetUniqueName(names, id, nodeName);
}
[TableName(Constants.DatabaseSchema.Tables.ContentType)]
private class ContentTypeReferenceDto : ContentTypeDto
{
[ResultColumn]
[Reference(ReferenceType.Many)]
public List<PropertyTypeReferenceDto> PropertyTypes { get; set; }
}
[TableName(Constants.DatabaseSchema.Tables.PropertyType)]
private class PropertyTypeReferenceDto
{
[Column("ptAlias")]
public string Alias { get; set; }
[Column("ptName")]
public string Name { get; set; }
}
}
}
@@ -25,7 +25,7 @@ namespace Umbraco.Core.Services
/// <summary>
/// Gets a content type.
/// </summary>
TItem Get(int id);
new TItem Get(int id);
/// <summary>
/// Gets a content type.
@@ -40,6 +40,7 @@ namespace Umbraco.Core.Services
int Count();
IEnumerable<TItem> GetAll(params int[] ids);
IEnumerable<TItem> GetAll(IEnumerable<Guid> ids);
IEnumerable<TItem> GetDescendants(int id, bool andSelf); // parent-child axis
IEnumerable<TItem> GetComposedOf(int id); // composition axis
@@ -10,6 +10,13 @@ namespace Umbraco.Core.Services
/// </summary>
public interface IDataTypeService : IService
{
/// <summary>
/// Returns a dictionary of content type <see cref="Udi"/>s and the property type aliases that use a <see cref="IDataType"/>
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id);
Attempt<OperationResult<OperationResultType, EntityContainer>> CreateContainer(int parentId, string name, int userId = Constants.Security.SuperUserId);
Attempt<OperationResult> SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId);
EntityContainer GetContainer(int containerId);
@@ -252,12 +252,12 @@ namespace Umbraco.Core.Services.Implement
}
}
public IEnumerable<TItem> GetAll(params Guid[] ids)
public IEnumerable<TItem> GetAll(IEnumerable<Guid> ids)
{
using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
scope.ReadLock(ReadLockIds);
return Repository.GetMany(ids);
return Repository.GetMany(ids.ToArray());
}
}
@@ -466,6 +466,14 @@ namespace Umbraco.Core.Services.Implement
}
}
public IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id)
{
using (var scope = ScopeProvider.CreateScope(autoComplete:true))
{
return _dataTypeRepository.FindUsages(id);
}
}
private void Audit(AuditType type, int userId, int objectId)
{
_auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType)));
@@ -28,6 +28,68 @@ namespace Umbraco.Tests.Persistence.Repositories
return new EntityContainerRepository(scopeAccessor, AppCaches.Disabled, Logger, Constants.ObjectTypes.DataTypeContainer);
}
[Test]
public void Can_Find_Usages()
{
var provider = TestObjects.GetScopeProvider(Logger);
using (provider.CreateScope())
{
var dtRepo = CreateRepository();
IDataType dataType1 = new DataType(new RadioButtonsPropertyEditor(Logger, ServiceContext.TextService)) { Name = "dt1" };
dtRepo.Save(dataType1);
IDataType dataType2 = new DataType(new RadioButtonsPropertyEditor(Logger, ServiceContext.TextService)) { Name = "dt2" };
dtRepo.Save(dataType2);
var ctRepo = Factory.GetInstance<IContentTypeRepository>();
IContentType ct = new ContentType(-1)
{
Alias = "ct1",
Name = "CT1",
AllowedAsRoot = true,
Icon = "icon-home",
PropertyGroups = new PropertyGroupCollection
{
new PropertyGroup(true)
{
Name = "PG1",
PropertyTypes = new PropertyTypeCollection(true)
{
new PropertyType(dataType1, "pt1")
{
Name = "PT1"
},
new PropertyType(dataType1, "pt2")
{
Name = "PT2"
},
new PropertyType(dataType2, "pt3")
{
Name = "PT3"
}
}
}
}
};
ctRepo.Save(ct);
var usages = dtRepo.FindUsages(dataType1.Id);
var key = usages.First().Key;
Assert.AreEqual(ct.Key, ((GuidUdi)key).Guid);
Assert.AreEqual(2, usages[key].Count());
Assert.AreEqual("pt1", usages[key].ElementAt(0));
Assert.AreEqual("pt2", usages[key].ElementAt(1));
usages = dtRepo.FindUsages(dataType2.Id);
key = usages.First().Key;
Assert.AreEqual(ct.Key, ((GuidUdi)key).Guid);
Assert.AreEqual(1, usages[key].Count());
Assert.AreEqual("pt3", usages[key].ElementAt(0));
}
}
[Test]
public void Can_Move()
{
@@ -329,6 +329,11 @@ namespace Umbraco.Tests.TestHelpers
{
throw new NotImplementedException();
}
public IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id)
{
throw new NotImplementedException();
}
}
#endregion
@@ -241,3 +241,7 @@ table th[class*="span"],
background-color: darken(@infoBackground, 5%);
}
}
.table .icon {
vertical-align: bottom;
}
@@ -55,7 +55,8 @@ function confirmDirective() {
onConfirm: '=',
onCancel: '=',
caption: '@',
confirmButtonStyle: '@'
confirmButtonStyle: '@',
confirmLabelKey: '@'
},
link: function (scope, element, attr, ctrl) {
scope.showCancel = false;
@@ -0,0 +1,20 @@
/**
* @ngdoc filter
* @name umbraco.filters.filter:CMS_joinArray
* @namespace umbCmsJoinArray
*
* param {array} array of string or objects, if an object use the third argument to specify which prop to list.
* param {seperator} string containing the seperator to add between joined values.
* param {prop} string used if joining an array of objects, set the name of properties to join.
*
* @description
* Join an array of string or an array of objects, with a costum seperator.
*
*/
angular.module("umbraco.filters").filter('umbCmsJoinArray', function () {
return function join(array, separator, prop) {
return (!angular.isUndefined(prop) ? array.map(function (item) {
return item[prop];
}) : array).join(separator || '');
};
});
@@ -43,6 +43,30 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) {
"Failed to retrieve pre values for editor alias " + editorAlias);
},
/**
* @ngdoc method
* @name umbraco.resources.dataTypeResource#getReferences
* @methodOf umbraco.resources.dataTypeResource
*
* @description
* Retrieves references of a given data type.
*
* @param {Int} id id of datatype to retrieve references for
* @returns {Promise} resourcePromise object.
*
*/
getReferences: function (id) {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"dataTypeApiBaseUrl",
"GetReferences",
{ id: id })),
"Failed to retrieve usages for data type of id " + id);
},
/**
* @ngdoc method
* @name umbraco.resources.dataTypeResource#getById
+2 -2
View File
@@ -138,14 +138,14 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService',
var toRetain = navigationService.retainQueryStrings(currentRouteParams, next.params);
//if toRetain is not null it means that there are missing query strings and we need to update the current params
//if toRetain is not null it means that there are missing query strings and we need to update the current params.
if (toRetain) {
$route.updateParams(toRetain);
}
//check if the location being changed is only due to global/state query strings which means the location change
//isn't actually going to cause a route change.
if (!toRetain && navigationService.isRouteChangingNavigation(currentRouteParams, next.params)) {
if (navigationService.isRouteChangingNavigation(currentRouteParams, next.params)) {
//The location change will cause a route change, continue the route if the query strings haven't been updated.
$route.reload();
@@ -160,6 +160,7 @@ input.umb-table__input {
font-size: 20px;
line-height: 20px;
color: @ui-option-type;
vertical-align: bottom;
}
.umb-table-body__checkicon,
@@ -245,7 +246,7 @@ input.umb-table__input {
.umb-table-cell {
display: flex;
flex-flow: row nowrap;
flex: 1 1 1%; //NOTE 1% is a Internet Explore hack, so that cells don't collapse
flex: 1 1 5%;
position: relative;
margin: auto 14px;
padding: 6px 2px;
@@ -258,6 +259,11 @@ input.umb-table__input {
white-space: nowrap; //NOTE Disable/Enable this to keep textstring on one line
text-overflow: ellipsis;
}
.umb-table-cell.--noOverflow > * {
overflow: visible;
white-space: normal;
text-overflow: unset;
}
.umb-table-cell:first-of-type:not(.not-fixed) {
flex: 0 0 25px;
@@ -269,6 +275,9 @@ input.umb-table__input {
flex: 0 0 auto !important;
}
.umb-table-cell--nano {
flex: 0 0 50px;
}
.umb-table-cell--small {
flex: .5 .5 1%;
max-width: 12.5%;
@@ -285,8 +294,8 @@ input.umb-table__input {
// Increases the space for the name cell
.umb-table__name {
flex: 1 1 25%;
max-width: 25%;
flex: 1 1 20%;
max-width: 300px;
}
.umb-table__loading-overlay {
@@ -62,7 +62,7 @@ table {
}
.table tr > td:first-child {
.table:not(.table-bordered) tr > td:first-child {
border-left: 4px solid transparent;
}
.table tr.--selected > td:first-child {
@@ -14,7 +14,7 @@
action="confirm()"
button-style="{{confirmButtonStyle || 'primary'}}"
state="confirmButtonState"
label-key="general_ok">
label-key="{{confirmLabelKey || 'general_ok'}}">
</umb-button>
</div>
</div>
@@ -8,7 +8,13 @@
*/
function DataTypeDeleteController($scope, dataTypeResource, treeService, navigationService, localizationService) {
$scope.performDelete = function() {
var vm = this;
vm.propertyJoinSeparator = ', <i class="icon-alert red"></i>';
vm.hasReferences = false;
vm.references = [];
vm.performDelete = function() {
//mark it for deletion (used in the UI)
$scope.currentNode.loading = true;
@@ -24,7 +30,7 @@ function DataTypeDeleteController($scope, dataTypeResource, treeService, navigat
});
};
$scope.performContainerDelete = function () {
vm.performContainerDelete = function () {
//mark it for deletion (used in the UI)
$scope.currentNode.loading = true;
@@ -41,16 +47,36 @@ function DataTypeDeleteController($scope, dataTypeResource, treeService, navigat
};
$scope.cancel = function() {
vm.cancel = function() {
navigationService.hideDialog();
};
$scope.labels = {};
vm.labels = {};
localizationService
.format(["editdatatype_yesDelete", "editdatatype_andAllRelated"], "%0% " + $scope.currentNode.name + " %1%")
.localize("editdatatype_acceptDeleteConsequence", [$scope.currentNode.name])
.then(function (data) {
$scope.labels.deleteConfirm = data;
vm.labels.deleteConfirm = data;
});
var init = function() {
if($scope.currentNode.nodeType === "dataTypes") {
vm.loading = true;
dataTypeResource.getReferences($scope.currentNode.id)
.then(function (data) {
vm.loading = false;
vm.references = data;
vm.hasReferences = data.documentTypes.length > 0 || data.mediaTypes.length > 0 || data.memberTypes.length > 0;
});
}
}
init();
}
angular.module("umbraco").controller("Umbraco.Editors.DataType.DeleteController", DataTypeDeleteController);
@@ -6,35 +6,20 @@
* @description
* The controller for the content editor
*/
function DataTypeEditController($scope, $routeParams, appState, navigationService, dataTypeResource, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper, eventsService) {
//setup scope vars
$scope.page = {};
$scope.page.loading = false;
$scope.page.nameLocked = false;
$scope.page.menu = {};
$scope.page.menu.currentSection = appState.getSectionState("currentSection");
$scope.page.menu.currentNode = null;
function DataTypeEditController($scope, $routeParams, appState, navigationService, dataTypeResource, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper, eventsService, localizationService) {
var evts = [];
//method used to configure the pre-values when we retrieve them from the server
function createPreValueProps(preVals) {
$scope.preValues = [];
for (var i = 0; i < preVals.length; i++) {
$scope.preValues.push({
hideLabel: preVals[i].hideLabel,
alias: preVals[i].key,
description: preVals[i].description,
label: preVals[i].label,
view: preVals[i].view,
value: preVals[i].value,
config: preVals[i].config
});
}
}
var vm = this;
//setup scope vars
vm.page = {};
vm.page.loading = false;
vm.page.menu = {};
vm.page.menu.currentSection = appState.getSectionState("currentSection");
vm.page.menu.currentNode = null;
//set up the standard data type props
$scope.properties = {
vm.properties = {
selectedEditor: {
alias: "selectedEditor",
description: "Select a property editor",
@@ -47,52 +32,51 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic
};
//setup the pre-values as props
$scope.preValues = [];
if ($routeParams.create) {
$scope.page.loading = true;
$scope.showIdentifier = false;
//we are creating so get an empty data type item
dataTypeResource.getScaffold($routeParams.id)
.then(function(data) {
$scope.preValuesLoaded = true;
$scope.content = data;
setHeaderNameState($scope.content);
//set a shared state
editorState.set($scope.content);
$scope.page.loading = false;
vm.preValues = [];
//method used to configure the pre-values when we retrieve them from the server
function createPreValueProps(preVals) {
vm.preValues = [];
for (var i = 0; i < preVals.length; i++) {
vm.preValues.push({
hideLabel: preVals[i].hideLabel,
alias: preVals[i].key,
description: preVals[i].description,
label: preVals[i].label,
view: preVals[i].view,
value: preVals[i].value,
config: preVals[i].config
});
}
}
else {
loadDataType();
function setHeaderNameState(content) {
if(content.isSystem == 1) {
vm.page.nameLocked = true;
}
}
function loadDataType() {
$scope.page.loading = true;
$scope.showIdentifier = true;
vm.page.loading = true;
vm.showIdentifier = true;
//we are editing so get the content item from the server
dataTypeResource.getById($routeParams.id)
.then(function(data) {
$scope.preValuesLoaded = true;
$scope.content = data;
vm.preValuesLoaded = true;
vm.content = data;
createPreValueProps($scope.content.preValues);
createPreValueProps(vm.content.preValues);
setHeaderNameState($scope.content);
setHeaderNameState(vm.content);
//share state
editorState.set($scope.content);
editorState.set(vm.content);
//in one particular special case, after we've created a new item we redirect back to the edit
// route but there might be server validation errors in the collection which we need to display
@@ -101,49 +85,22 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic
serverValidationManager.notifyAndClearAllSubscriptions();
navigationService.syncTree({ tree: "datatypes", path: data.path }).then(function (syncArgs) {
$scope.page.menu.currentNode = syncArgs.node;
vm.page.menu.currentNode = syncArgs.node;
});
$scope.page.loading = false;
vm.page.loading = false;
});
}
$scope.$watch("content.selectedEditor", function (newVal, oldVal) {
//when the value changes, we need to dynamically load in the new editor
if (newVal && (newVal != oldVal && (oldVal || $routeParams.create))) {
//we are editing so get the content item from the server
var currDataTypeId = $routeParams.create ? undefined : $routeParams.id;
dataTypeResource.getPreValues(newVal, currDataTypeId)
.then(function (data) {
$scope.preValuesLoaded = true;
$scope.content.preValues = data;
createPreValueProps($scope.content.preValues);
setHeaderNameState($scope.content);
//share state
editorState.set($scope.content);
});
}
});
function setHeaderNameState(content) {
if(content.isSystem == 1) {
$scope.page.nameLocked = true;
}
}
$scope.save = function() {
function saveDataType() {
if (formHelper.submitForm({ scope: $scope })) {
$scope.page.saveButtonState = "busy";
vm.page.saveButtonState = "busy";
dataTypeResource.save($scope.content, $scope.preValues, $routeParams.create)
dataTypeResource.save(vm.content, vm.preValues, $routeParams.create)
.then(function(data) {
formHelper.resetForm({ scope: $scope });
@@ -156,18 +113,18 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic
}
});
setHeaderNameState($scope.content);
setHeaderNameState(vm.content);
//share state
editorState.set($scope.content);
editorState.set(vm.content);
navigationService.syncTree({ tree: "datatypes", path: data.path, forceReload: true }).then(function (syncArgs) {
$scope.page.menu.currentNode = syncArgs.node;
vm.page.menu.currentNode = syncArgs.node;
});
$scope.page.saveButtonState = "success";
vm.page.saveButtonState = "success";
dataTypeHelper.rebindChangedProperties($scope.content, data);
dataTypeHelper.rebindChangedProperties(vm.content, data);
}, function(err) {
@@ -177,19 +134,21 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic
err: err
});
$scope.page.saveButtonState = "error";
vm.page.saveButtonState = "error";
//share state
editorState.set($scope.content);
editorState.set(vm.content);
});
}
};
vm.save = saveDataType;
evts.push(eventsService.on("app.refreshEditor", function(name, error) {
loadDataType();
}));
//ensure to unregister from all events!
$scope.$on('$destroy', function () {
for (var e in evts) {
@@ -197,6 +156,80 @@ function DataTypeEditController($scope, $routeParams, appState, navigationServic
}
});
function init() {
$scope.$watch("vm.content.selectedEditor", function (newVal, oldVal) {
//when the value changes, we need to dynamically load in the new editor
if (newVal && (newVal != oldVal && (oldVal || $routeParams.create))) {
//we are editing so get the content item from the server
var currDataTypeId = $routeParams.create ? undefined : $routeParams.id;
dataTypeResource.getPreValues(newVal, currDataTypeId)
.then(function (data) {
vm.preValuesLoaded = true;
vm.content.preValues = data;
createPreValueProps(vm.content.preValues);
setHeaderNameState(vm.content);
//share state
editorState.set(vm.content);
});
}
});
if ($routeParams.create) {
vm.page.loading = true;
vm.showIdentifier = false;
//we are creating so get an empty data type item
dataTypeResource.getScaffold($routeParams.id)
.then(function(data) {
vm.preValuesLoaded = true;
vm.content = data;
setHeaderNameState(vm.content);
//set a shared state
editorState.set(vm.content);
vm.page.loading = false;
});
}
else {
loadDataType();
}
var labelKeys = [
"general_settings",
"references_tabName"
];
localizationService.localizeMany(labelKeys).then(function (values) {
vm.page.navigation = [
{
"name": values[0],
"alias": "settings",
"icon": "icon-settings",
"view": "views/datatypes/views/datatype.settings.html",
"active": true
},
{
"name": values[1],
"alias": "references",
"icon": "icon-molecular-network",
"view": "views/datatypes/views/datatype.references.html"
}
];
});
}
init();
}
angular.module("umbraco").controller("Umbraco.Editors.DataType.EditController", DataTypeEditController);
@@ -1,29 +1,122 @@
<div class="umb-dialog umb-pane" ng-controller="Umbraco.Editors.DataType.DeleteController">
<div class="umb-dialog umb-pane" ng-controller="Umbraco.Editors.DataType.DeleteController as vm">
<div class="umb-dialog-body">
<p class="abstract">
<localize key="defaultdialogs_confirmdelete">Are you sure you want to delete</localize>&nbsp;<strong>{{currentNode.name}}</strong>?
</p>
<ng-switch on="currentNode.nodeType">
<div ng-switch-when="container">
<umb-confirm on-confirm="performContainerDelete"
on-cancel="cancel">
<p class="abstract">
<localize key="defaultdialogs_confirmdelete">Are you sure you want to delete</localize>&nbsp;<strong>{{currentNode.name}}</strong>?
</p>
<umb-confirm on-confirm="vm.performContainerDelete"
on-cancel="vm.cancel"
confirm-button-style="danger"
confirm-label-key="general_delete">
</umb-confirm>
</div>
<div ng-switch-default>
<p>
<i class="icon-alert red"></i> <strong class="red"><localize key="editdatatype_allPropTypes">All property types & property data</localize></strong>
<localize key="editdatatype_willBeDeleted">using this data type will be deleted permanently, please confirm you want to delete these as well</localize>.
</p>
<hr />
<umb-checkbox model="confirmed" text="{{labels.deleteConfirm}}">
</umb-checkbox>
<umb-load-indicator ng-if="vm.loading === true"></umb-load-indicator>
<umb-confirm ng-if="confirmed" on-confirm="performDelete" on-cancel="cancel">
<div ng-if="vm.loading === false && vm.hasReferences === false">
<p class="abstract">
<localize key="defaultdialogs_confirmdelete">Are you sure you want to delete</localize>&nbsp;<strong>{{currentNode.name}}</strong>?
</p>
</div>
<div ng-if="vm.loading === false && vm.hasReferences === true">
<p class="abstract">
<localize key="editdatatype_hasReferencesDeleteConsequence" tokens="[currentNode.name]">Deleting <strong>{{currentNode.name}}</strong> will have the following consequence</localize>
</p>
<hr/>
<!-- Document Types -->
<div ng-if="vm.references.documentTypes.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="treeHeaders_documentTypes"></localize>
</h5>
<table class="table table-condensed table-bordered">
<thead>
<tr>
<th><localize key="general_name">Name</localize></th>
<th><localize key="general_properties">Properties</localize></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="relation in vm.references.documentTypes">
<td><span title="{{::relation.name}}({{::relation.alias}})"><i class="umb-table-body__icon {{relation.icon}}"></i>{{::relation.name}}</span></td>
<td><span><span class="red" ng-repeat="property in relation.properties"><i class="icon icon-alert red"></i>{{::property.name}}{{$last ? '' : ', '}}</span></span></td>
</tr>
</tbody>
</table>
</div>
<!-- Media Types -->
<div ng-if="vm.references.mediaTypes.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="treeHeaders_mediaTypes"></localize>
</h5>
<table class="table table-condensed table-bordered">
<thead>
<tr>
<th><localize key="general_name">Name</localize></th>
<th><localize key="general_properties">Properties</localize></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="relation in vm.references.mediaTypes">
<td><span title="{{::relation.name}}({{::relation.alias}})"><i class="umb-table-body__icon {{relation.icon}}"></i>{{::relation.name}}</span></td>
<td><span><span class="red" ng-repeat="property in relation.properties"><i class="icon icon-alert red"></i>{{::property.name}}{{$last ? '' : ', '}}</span></span></td>
</tr>
</tbody>
</table>
</div>
<!-- Member Types -->
<div ng-if="vm.references.memberTypes.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="treeHeaders_memberTypes"></localize>
</h5>
<table class="table table-condensed table-bordered">
<thead>
<tr>
<th><localize key="general_name">Name</localize></th>
<th><localize key="general_properties">Properties</localize></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="relation in vm.references.memberTypes">
<td><span title="{{::relation.name}}({{::relation.alias}})"><i class="umb-table-body__icon {{relation.icon}}"></i>{{::relation.name}}</span></td>
<td><span><span class="red" ng-repeat="property in relation.properties"><i class="icon icon-alert red"></i>{{::property.name}}{{$last ? '' : ', '}}</span></span></td>
</tr>
</tbody>
</table>
</div>
<umb-checkbox model="vm.confirmed" text="{{vm.labels.deleteConfirm}}">
</umb-checkbox>
</div>
<umb-confirm ng-if="vm.confirmed || vm.hasReferences === false"
on-confirm="vm.performDelete"
on-cancel="vm.cancel"
confirm-button-style="danger"
confirm-label-key="general_delete">
</umb-confirm>
</div>
</ng-switch>
@@ -1,81 +1,33 @@
<div data-element="editor-data-types" ng-controller="Umbraco.Editors.DataType.EditController">
<div data-element="editor-data-types" ng-controller="Umbraco.Editors.DataType.EditController as vm">
<umb-load-indicator ng-if="page.loading"></umb-load-indicator>
<form name="contentForm" novalidate val-form-manager ng-submit="vm.save()">
<umb-load-indicator ng-if="vm.page.loading"></umb-load-indicator>
<umb-editor-view ng-if="!vm.page.loading">
<umb-editor-header name="vm.content.name"
name-locked="vm.page.nameLocked"
hide-icon="true"
hide-description="true"
hide-alias="true"
navigation="vm.page.navigation">
</umb-editor-header>
<form name="contentForm"
ng-submit="save()"
novalidate
val-form-manager>
<umb-editor-container class="form-horizontal">
<umb-editor-sub-views sub-views="vm.page.navigation" model="vm">
</umb-editor-sub-views>
</umb-editor-container>
<umb-editor-view ng-if="!page.loading">
<umb-editor-footer>
<umb-editor-footer-content-right>
<umb-button type="submit"
button-style="success"
state="vm.page.saveButtonState"
shortcut="ctrl+s"
label="Save"
label-key="buttons_save">
</umb-button>
</umb-editor-footer-content-right>
</umb-editor-footer>
</umb-editor-view>
</form>
<umb-editor-header
name="content.name"
name-locked="page.nameLocked"
hide-icon="true"
hide-description="true"
hide-alias="true">
</umb-editor-header>
<umb-editor-container class="form-horizontal">
<umb-box>
<umb-box-content>
<umb-control-group label="Id" ng-if="showIdentifier">
<div>{{content.id}}</div>
<small>{{content.key}}</small>
</umb-control-group>
<umb-property property="properties.selectedEditor">
<div>
<select name="selectedEditor"
ng-model="content.selectedEditor"
required
ng-options="e.alias as e.name for e in content.availableEditors"></select>
<span ng-messages="contentForm.selectedEditor.$error" show-validation-on-submit>
<span class="help-inline" ng-message="required"><localize key="general_required">Required</localize></span>
</span>
</div>
</umb-property>
<umb-property property="properties.selectedEditorId">
<div>{{content.selectedEditor}}</div>
</umb-property>
<umb-property property="preValue"
ng-repeat="preValue in preValues">
<umb-property-editor model="preValue" is-pre-value="true"></umb-property-editor>
</umb-property>
</umb-box-content>
</umb-box>
</umb-editor-container>
<umb-editor-footer>
<umb-editor-footer-content-right>
<umb-button
type="submit"
button-style="success"
state="page.saveButtonState"
shortcut="ctrl+s"
label="Save"
label-key="buttons_save">
</umb-button>
</umb-editor-footer-content-right>
</umb-editor-footer>
</umb-editor-view>
</form>
</div>
@@ -0,0 +1,55 @@
/**
* @ngdoc controller
* @name Umbraco.Editors.DataType.ReferencesController
* @function
*
* @description
* The controller for the references view of the datatype editor
*/
function DataTypeReferencesController($scope, $routeParams, dataTypeResource, eventsService, $timeout) {
var vm = this;
var evts = [];
var referencesLoaded = false;
vm.references = {};
vm.hasReferences = false;
vm.view = {};
vm.view.loading = true;
/** Loads in the data type references one time */
function loadRelations() {
if (!referencesLoaded) {
referencesLoaded = true;
dataTypeResource.getReferences($routeParams.id)
.then(function (data) {
vm.view.loading = false;
vm.references = data;
vm.hasReferences = data.documentTypes.length > 0 || data.mediaTypes.length > 0 || data.memberTypes.length > 0;
});
}
}
// load data type references when the references tab is activated
evts.push(eventsService.on("app.tabChange", function (event, args) {
$timeout(function () {
if (args.alias === "references") {
loadRelations();
}
});
}));
//ensure to unregister from all events!
$scope.$on('$destroy', function () {
for (var e in evts) {
eventsService.unsubscribe(evts[e]);
}
});
}
angular.module("umbraco").controller("Umbraco.Editors.DataType.ReferencesController", DataTypeReferencesController);
@@ -0,0 +1,112 @@
<div ng-controller="Umbraco.Editors.DataType.ReferencesController as vm">
<umb-load-indicator ng-if="vm.view.loading === true"></umb-load-indicator>
<umb-box ng-if="vm.view.loading === false && vm.hasReferences === false">
<umb-box-header title-key="references_tabName"></umb-box-header>
<umb-box-content>
<umb-empty-state size="small">
<localize key="references_DataTypeNoReferences">This Data Type has no references.</localize>
</umb-empty-state>
</umb-box-content>
</umb-box>
<div ng-if="vm.view.loading === false && vm.hasReferences === true">
<!-- Document Types -->
<div ng-if="vm.references.documentTypes.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="references_labelUsedByDocumentTypes"></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"><localize key="references_usedByProperties">Used in</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 vm.references.documentTypes">
<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 --noOverflow"><span>{{::reference.properties | umbCmsJoinArray:', ':'name'}}</span></div>
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/documentTypes/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
</div>
</div>
</div>
</div>
<!-- Media Types -->
<div ng-if="vm.references.mediaTypes.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="references_labelUsedByMediaTypes"></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"><localize key="references_usedByProperties">Used in</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 vm.references.mediaTypes">
<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 --noOverflow"><span>{{::reference.properties | umbCmsJoinArray:', ':'name'}}</span></div>
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/documentTypes/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
</div>
</div>
</div>
</div>
<!-- Member Types -->
<div ng-if="vm.references.memberTypes.length > 0">
<h5 class="mt4" style="margin-bottom: 20px;">
<localize key="references_labelUsedByMemberTypes"></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"><localize key="references_usedByProperties">Used in</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 vm.references.memberTypes">
<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 --noOverflow"><span>{{::reference.properties | umbCmsJoinArray:', ':'name'}}</span></div>
<div class="umb-table-cell umb-table-cell--nano"><a href="#/settings/documentTypes/edit/{{::reference.id}}"><localize key="general_open">Open</localize></a></div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,34 @@
<umb-box>
<umb-box-content>
<umb-control-group label="Id" ng-if="model.showIdentifier">
<div>{{model.content.id}}</div>
<small>{{model.content.key}}</small>
</umb-control-group>
<umb-property property="model.properties.selectedEditor">
<div>
<select name="selectedEditor"
ng-model="model.content.selectedEditor"
required
ng-options="e.alias as e.name for e in model.content.availableEditors"></select>
<span ng-messages="contentForm.selectedEditor.$error" show-validation-on-submit>
<span class="help-inline" ng-message="required"><localize key="general_required">Required</localize></span>
</span>
</div>
</umb-property>
<umb-property property="model.properties.selectedEditorId">
<div>{{model.content.selectedEditor}}</div>
</umb-property>
<umb-property property="preValue"
ng-repeat="preValue in model.preValues">
<umb-property-editor model="preValue" is-pre-value="true"></umb-property-editor>
</umb-property>
</umb-box-content>
</umb-box>
@@ -130,7 +130,7 @@ function MacrosEditController($scope, $q, $routeParams, macroResource, editorSta
// navigation
vm.labels.settings = values[0];
vm.labels.parameters = values[1];
vm.page.navigation = [
{
"name": vm.labels.settings,
@@ -0,0 +1,43 @@
(function () {
describe('umbCmsJoinArray filter', function() {
var $umbCmsJoinArray;
var testCases = [
{input:[{param:'a'},{param:'b'},{param:'c'}], separator:', ', prop:'param' , expectedResult: 'a, b, c'},
{input:[{param:'a'},{param:'b'},{param:'c'}], separator:' ', prop:'param' , expectedResult: 'a b c'},
{input:[{param:'a'},{param:'b'},{param:'c'}], separator:'', prop:'param' , expectedResult: 'abc'},
{input:[{param:'a'},{param:'b'},{param:'c'}], separator:'', prop:'wrong' , expectedResult: ''},
{input:[], separator:', ', prop:'param' , expectedResult: ''},
{input:[{param:'a'},{param:'b'},{param:'c'}], separator:', ', prop:null , expectedResult: ', , '},
{input:[{param:'a'},{param:'b'},{param:'c'}], separator:null, prop:'param' , expectedResult: 'abc'},
];
var testCasesWithExpectedError = [
{input:'test', separator:', ', prop:'param'},
{input:null, separator:', ', prop:'param'},
{input:undefined, separator:', ', prop:'param'},
];
beforeEach(module('umbraco'));
beforeEach(inject(function($filter) {
$umbCmsJoinArray = $filter('umbCmsJoinArray');
}));
testCases.forEach(function(test){
it('Blackbox tests with expected result=\''+test.expectedResult+'\'', function() {
expect($umbCmsJoinArray(test.input, test.separator, test.prop)).toBe(test.expectedResult);
});
});
testCasesWithExpectedError.forEach(function(test){
it('Blackbox tests with expected error. Input=\''+test.input+'\'', function() {
expect(function() { $umbCmsJoinArray(test.input, test.separator, test.prop)}).toThrow();
});
});
});
}());
@@ -566,6 +566,8 @@
<key alias="selectFolder">Vælg den mappe, der skal flyttes</key>
<key alias="inTheTree">til i træstrukturen nedenfor</key>
<key alias="wasMoved">blev flyttet under</key>
<key alias="hasReferencesDeleteConsequence"><![CDATA[Ved sletning af <strong>%0%</strong> fjernes egnskaber og egnskabernes data fra følgende elementer]]></key>
<key alias="acceptDeleteConsequence">I understand this action will delete the properties and data based on this Data Type</key>
</area>
<area alias="errorHandling">
<key alias="errorButDataWasSaved">Dine data er blevet gemt, men før du kan udgive denne side er der nogle fejl der skal rettes:</key>
@@ -1717,4 +1719,16 @@ Mange hilsner fra Umbraco robotten
<key alias="currentLanguage">Aktivt sprog</key>
<key alias="switchLanguage">Skift sprog til</key>
</area>
<area alias="references">
<key alias="tabName">Referencer</key>
<key alias="DataTypeNoReferences">Denne Data Type har ingen referencer.</key>
<key alias="labelsedByDocumentTypes">Brugt i Dokument Typer</key>
<key alias="noDocumentTypes">Ingen referencer til Dokument Typer.</key>
<key alias="labelUsedByMediaTypes">Brugt i Medie Typer</key>
<key alias="noMediaTypes">Ingen referencer til Medie Typer.</key>
<key alias="labelUsedByMemberTypes">Brugt i Medlems Typer</key>
<key alias="noMemberTypes">Ingen referencer til Medlems Typer.</key>
<key alias="usedByProperties">Brugt af</key>
</area>
</language>
File diff suppressed because it is too large Load Diff
@@ -572,6 +572,8 @@
<key alias="selectFolder">Select the folder to move</key>
<key alias="inTheTree">to in the tree structure below</key>
<key alias="wasMoved">was moved underneath</key>
<key alias="hasReferencesDeleteConsequence"><![CDATA[Deleting <strong>%0%</strong> will delete the properties and their data from the following items]]></key>
<key alias="acceptDeleteConsequence">I understand this action will delete the properties and data based on this Data Type</key>
</area>
<area alias="errorHandling">
<key alias="errorButDataWasSaved">Your data has been saved, but before you can publish this page there are some errors you need to fix first:</key>
@@ -2170,4 +2172,15 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="currentLanguage">Current language</key>
<key alias="switchLanguage">Switch language to</key>
</area>
<area alias="references">
<key alias="tabName">References</key>
<key alias="DataTypeNoReferences">This Data Type has no references.</key>
<key alias="labelUsedByDocumentTypes">Used in Document Types</key>
<key alias="noDocumentTypes">No references to Document Types.</key>
<key alias="labelUsedByMediaTypes">Used in Media Types</key>
<key alias="noMediaTypes">No references to Media Types.</key>
<key alias="labelUsedByMemberTypes">Used in Member Types</key>
<key alias="noMemberTypes">No references to Member Types.</key>
<key alias="usedByProperties">Used by</key>
</area>
</language>
@@ -283,6 +283,60 @@ namespace Umbraco.Web.Editors
: Request.CreateNotificationValidationErrorResponse(result.Exception.Message);
}
/// <summary>
/// Returns the references (usages) for the data type
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public DataTypeReferences GetReferences(int id)
{
var result = new DataTypeReferences();
var usages = Services.DataTypeService.GetReferences(id);
foreach(var groupOfEntityType in usages.GroupBy(x => x.Key.EntityType))
{
//get all the GUIDs for the content types to find
var guidsAndPropertyAliases = groupOfEntityType.ToDictionary(i => ((GuidUdi)i.Key).Guid, i => i.Value);
if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.DocumentType))
result.DocumentTypes = GetContentTypeUsages(Services.ContentTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases);
else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MediaType))
result.MediaTypes = GetContentTypeUsages(Services.MediaTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases);
else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MemberType))
result.MemberTypes = GetContentTypeUsages(Services.MemberTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases);
}
return result;
}
/// <summary>
/// Maps the found content types and usages to the resulting model
/// </summary>
/// <param name="cts"></param>
/// <param name="usages"></param>
/// <returns></returns>
private IEnumerable<DataTypeReferences.ContentTypeReferences> GetContentTypeUsages(
IEnumerable<IContentTypeBase> cts,
IReadOnlyDictionary<Guid, IEnumerable<string>> usages)
{
return cts.Select(x => new DataTypeReferences.ContentTypeReferences
{
Id = x.Id,
Key = x.Key,
Alias = x.Alias,
Icon = x.Icon,
Name = x.Name,
Udi = new GuidUdi(ObjectTypes.GetUdiType(UmbracoObjectTypes.DocumentType), x.Key),
//only select matching properties
Properties = x.PropertyTypes.Where(p => usages[x.Key].InvariantContains(p.Alias))
.Select(p => new DataTypeReferences.ContentTypeReferences.PropertyTypeReferences
{
Alias = p.Alias,
Name = p.Name
})
});
}
#region ReadOnly actions to return basic data - allow access for: content ,media, members, settings, developer
/// <summary>
/// Gets the content json for all data types
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
{
[DataContract(Name = "dataTypeReferences", Namespace = "")]
public class DataTypeReferences
{
[DataMember(Name = "documentTypes")]
public IEnumerable<ContentTypeReferences> DocumentTypes { get; set; } = Enumerable.Empty<ContentTypeReferences>();
[DataMember(Name = "mediaTypes")]
public IEnumerable<ContentTypeReferences> MediaTypes { get; set; } = Enumerable.Empty<ContentTypeReferences>();
[DataMember(Name = "memberTypes")]
public IEnumerable<ContentTypeReferences> MemberTypes { get; set; } = Enumerable.Empty<ContentTypeReferences>();
[DataContract(Name = "contentType", Namespace = "")]
public class ContentTypeReferences : EntityBasic
{
[DataMember(Name = "properties")]
public object Properties { get; set; }
[DataContract(Name = "property", Namespace = "")]
public class PropertyTypeReferences
{
[DataMember(Name = "name")]
public string Name { get; set; }
[DataMember(Name = "alias")]
public string Alias { get; set; }
}
}
}
}
+1
View File
@@ -212,6 +212,7 @@
<Compile Include="Media\UploadAutoFillProperties.cs" />
<Compile Include="Migrations\PostMigrations\PublishedSnapshotRebuilder.cs" />
<Compile Include="Models\AnchorsModel.cs" />
<Compile Include="Models\ContentEditing\DataTypeReferences.cs" />
<Compile Include="Models\ContentEditing\LinkDisplay.cs" />
<Compile Include="Models\ContentEditing\MacroDisplay.cs" />
<Compile Include="Models\ContentEditing\MacroParameterDisplay.cs" />