Merge branch 'dev-v7-contenttypeeditor' of https://github.com/umbraco/Umbraco-CMS into dev-v7-contenttypeeditor

This commit is contained in:
Per Ploug
2015-06-06 08:40:11 +02:00
15 changed files with 402 additions and 78 deletions
@@ -853,7 +853,7 @@ AND umbracoNode.id <> @id",
//first we want to get the main content type data this is 1 : 1 with umbraco node data
var ct = result
.Where(x => x.ctId == currentCtId)
.Where(x => (int)x.ctId == currentCtId)
.Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId })
.DistinctBy(x => (int)x.ctId)
.FirstOrDefault();
@@ -4,6 +4,7 @@ using System.Linq;
using AutoMapper;
using Moq;
using NUnit.Framework;
using umbraco;
using umbraco.cms.presentation;
using Umbraco.Core;
using Umbraco.Core.Logging;
@@ -177,11 +178,35 @@ namespace Umbraco.Tests.Models.Mapping
Assert.AreEqual(display.Thumbnail, result.Thumbnail);
Assert.AreEqual(display.IsContainer, result.IsContainer);
Assert.AreEqual(display.AllowAsRoot, result.AllowedAsRoot);
Assert.AreEqual(display.CreateDate, result.CreateDate);
Assert.AreEqual(display.UpdateDate, result.UpdateDate);
//TODO: Now we need to assert all of the more complicated parts
Assert.AreEqual(1, result.PropertyGroups.Count);
Assert.AreEqual(1, result.PropertyGroups[0].PropertyTypes.Count);
Assert.AreEqual(display.Groups.Count(), result.PropertyGroups.Count);
for (var i = 0; i < display.Groups.Count(); i++)
{
Assert.AreEqual(display.Groups.ElementAt(i).Id, result.PropertyGroups.ElementAt(i).Id);
Assert.AreEqual(display.Groups.ElementAt(i).Name, result.PropertyGroups.ElementAt(i).Name);
var propTypes = display.Groups.ElementAt(i).Properties;
Assert.AreEqual(propTypes.Count(), result.PropertyTypes.Count());
for (var j = 0; j < propTypes.Count(); j++)
{
Assert.AreEqual(propTypes.ElementAt(j).Id, result.PropertyTypes.ElementAt(j).Id);
Assert.AreEqual(propTypes.ElementAt(j).DataTypeId, result.PropertyTypes.ElementAt(j).DataTypeDefinitionId);
}
}
Assert.AreEqual(display.AllowedTemplates.Count(), result.AllowedTemplates.Count());
for (var i = 0; i < display.AllowedTemplates.Count(); i++)
{
Assert.AreEqual(display.AllowedTemplates.ElementAt(i).Id, result.AllowedTemplates.ElementAt(i).Id);
}
Assert.AreEqual(display.AllowedContentTypes.Count(), result.AllowedContentTypes.Count());
for (var i = 0; i < display.AllowedContentTypes.Count(); i++)
{
Assert.AreEqual(display.AllowedContentTypes.ElementAt(i), result.AllowedContentTypes.ElementAt(i).Id.Value);
}
}
[Test]
@@ -231,13 +256,39 @@ namespace Umbraco.Tests.Models.Mapping
Assert.AreEqual(contentType.Path, result.Path);
Assert.AreEqual(contentType.Thumbnail, result.Thumbnail);
Assert.AreEqual(contentType.IsContainer, result.IsContainer);
Assert.AreEqual(contentType.CreateDate, result.CreateDate);
Assert.AreEqual(contentType.UpdateDate, result.UpdateDate);
Assert.AreEqual(contentType.DefaultTemplate.Alias, result.DefaultTemplate.Alias);
//TODO: Now we need to assert all of the more complicated parts
Assert.AreEqual(2, result.Groups.Count());
Assert.AreEqual(2, result.Groups.ElementAt(0).Properties.Count());
Assert.AreEqual(2, result.Groups.ElementAt(1).Properties.Count());
Assert.AreEqual(contentType.PropertyGroups.Count(), result.Groups.Count());
for (var i = 0; i < contentType.PropertyGroups.Count(); i++)
{
Assert.AreEqual(contentType.PropertyGroups.ElementAt(i).Id, result.Groups.ElementAt(i).Id);
Assert.AreEqual(contentType.PropertyGroups.ElementAt(i).Name, result.Groups.ElementAt(i).Name);
var propTypes = contentType.PropertyGroups.ElementAt(i).PropertyTypes;
Assert.AreEqual(propTypes.Count(), result.Groups.ElementAt(i).Properties.Count());
for (var j = 0; j < propTypes.Count(); j++)
{
Assert.AreEqual(propTypes.ElementAt(j).Id, result.Groups.ElementAt(i).Properties.ElementAt(j).Id);
Assert.AreEqual(propTypes.ElementAt(j).DataTypeDefinitionId, result.Groups.ElementAt(i).Properties.ElementAt(j).DataTypeId);
}
}
Assert.AreEqual(contentType.AllowedTemplates.Count(), result.AllowedTemplates.Count());
for (var i = 0; i < contentType.AllowedTemplates.Count(); i++)
{
Assert.AreEqual(contentType.AllowedTemplates.ElementAt(i).Id, result.AllowedTemplates.ElementAt(i).Id);
}
Assert.AreEqual(contentType.AllowedContentTypes.Count(), result.AllowedContentTypes.Count());
for (var i = 0; i < contentType.AllowedContentTypes.Count(); i++)
{
Assert.AreEqual(contentType.AllowedContentTypes.ElementAt(i).Id.Value, result.AllowedContentTypes.ElementAt(i));
}
}
private ContentTypeDisplay CreateSimpleContentTypeDisplay()
@@ -246,7 +297,22 @@ namespace Umbraco.Tests.Models.Mapping
{
Alias = "test",
AllowAsRoot = true,
AllowedTemplates = new List<EntityBasic>(),
AllowedTemplates = new List<EntityBasic>
{
new EntityBasic
{
Id = 555,
Alias = "template1",
Name = "Template1"
},
new EntityBasic
{
Id = 556,
Alias = "template2",
Name = "Template2"
}
},
AllowedContentTypes = new [] {666, 667},
AvailableCompositeContentTypes = new List<EntityBasic>(),
DefaultTemplate = new EntityBasic(){ Alias = "test" },
Description = "hello world",
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AutoMapper;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
@@ -16,9 +17,11 @@ using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Tests.Persistence.Repositories
{
[RequiresAutoMapperMappings]
[DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)]
[TestFixture]
public class ContentTypeRepositoryTest : BaseDatabaseFactoryTest
@@ -108,12 +111,44 @@ namespace Umbraco.Tests.Persistence.Repositories
// Assert
Assert.That(contentType.HasIdentity, Is.True);
Assert.That(contentType.PropertyGroups.All(x => x.HasIdentity), Is.True);
Assert.That(contentType.PropertyTypes.All(x => x.HasIdentity), Is.True);
Assert.That(contentType.Path.Contains(","), Is.True);
Assert.That(contentType.SortOrder, Is.GreaterThan(0));
}
}
[Test]
public void Can_Perform_Add_On_ContentTypeRepository_After_Model_Mapping()
{
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
using (var repository = CreateRepository(unitOfWork))
{
// Act
var contentType = (IContentType)MockedContentTypes.CreateSimpleContentType();
var display = Mapper.Map<ContentTypeDisplay>(contentType);
//simulate what would happen in the controller, we'd never map to a 'existing' content type,
// we'd map to an new content type when updating.
var mapped = Mapper.Map<IContentType>(display);
repository.AddOrUpdate(mapped);
unitOfWork.Commit();
//re-get
contentType = repository.Get(mapped.Id);
// Assert
Assert.That(contentType.HasIdentity, Is.True);
Assert.That(contentType.PropertyGroups.All(x => x.HasIdentity), Is.True);
Assert.That(contentType.PropertyTypes.All(x => x.HasIdentity), Is.True);
Assert.That(contentType.Path.Contains(","), Is.True);
Assert.That(contentType.SortOrder, Is.GreaterThan(0));
}
}
[Test]
public void Can_Perform_Update_On_ContentTypeRepository()
{
@@ -149,6 +184,66 @@ namespace Umbraco.Tests.Persistence.Repositories
}
[Test]
public void Can_Perform_Update_On_ContentTypeRepository_After_Model_Mapping()
{
// Arrange
var provider = new PetaPocoUnitOfWorkProvider(Logger);
var unitOfWork = provider.GetUnitOfWork();
using (var repository = CreateRepository(unitOfWork))
{
// Act
var contentType = repository.Get(NodeDto.NodeIdSeed + 1);
var display = Mapper.Map<ContentTypeDisplay>(contentType);
display.Thumbnail = "Doc2.png";
var contentGroup = display.Groups.Single(x => x.Name == "Content");
//add property
contentGroup.Properties = contentGroup.Properties.Concat(new[]
{
new PropertyTypeDisplay()
{
Alias = "subtitle",
Editor = "test",
Label = "Subtitle",
Description = "Optional Subtitle",
Validation = new PropertyTypeValidation()
{
Mandatory = false,
Pattern = ""
},
SortOrder = 1,
DataTypeId = -88
}
});
//simulate what would happen in the controller, we'd never map to a 'new' content type,
// we'd map to an existing content type when updating.
var mapped = Mapper.Map(display, contentType);
repository.AddOrUpdate(mapped);
unitOfWork.Commit();
var dirty = mapped.IsDirty();
//re-get
contentType = repository.Get(NodeDto.NodeIdSeed + 1);
// Assert
Assert.That(contentType.HasIdentity, Is.True);
Assert.That(dirty, Is.False);
Assert.That(contentType.Thumbnail, Is.EqualTo("Doc2.png"));
Assert.That(contentType.PropertyTypes.Any(x => x.Alias == "subtitle"), Is.True);
foreach (var propertyType in contentType.PropertyTypes)
{
Assert.IsTrue(propertyType.HasIdentity);
Assert.Greater(propertyType.Id, 0);
}
}
}
[Test]
public void Can_Perform_Delete_On_ContentTypeRepository()
{
@@ -13,6 +13,7 @@ using Umbraco.Tests.TestHelpers.Entities;
namespace Umbraco.Tests.Services
{
[DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)]
[TestFixture, RequiresSTA]
public class ContentTypeServiceTests : BaseServiceTest
@@ -4,7 +4,7 @@
* @restrict E
**/
angular.module("umbraco.directives")
.directive('umbOverlay', function () {
.directive('umbOverlay', function ($timeout) {
return {
scope: {
@@ -37,8 +37,59 @@ angular.module("umbraco.directives")
}
cssClass += " " + shadow;
scope.overlayCssClass = cssClass;
function setTargetPosition() {
var position = {
right: "inherit",
left: "inherit",
top: "inherit",
bottom: "inherit"
};
// viewport size
var viewportWidth = $(window).innerWidth();
var viewportHeight = $(window).innerHeight();
// mouse click position
var mousePositionClickX = scope.model.event.pageX;
var mousePositionClickY = scope.model.event.pageY;
// element size
var elementHeight = element.context.clientHeight;
var elementWidth = element.context.clientWidth;
// move element to this position
position.left = mousePositionClickX - (elementWidth / 2);
position.top = mousePositionClickY - (elementHeight / 2);
// check to see if element is outside screen
// outside right
if( position.left + elementWidth > viewportWidth) {
position.right = 0;
position.left = "inherit";
}
// outside bottom
if( position.top + elementHeight > viewportHeight ) {
position.bottom = 0;
position.top = "inherit";
}
element.css(position);
}
$timeout(function() {
if(scope.position === "target") {
setTargetPosition();
}
});
scope.closeOverLay = function(){
if(scope.model.close){
scope.model.close(scope.model);
@@ -64,7 +64,7 @@ angular.module('umbraco.directives')
scope.$on("$destroy", function() {
$(document).off("click", oneTimeClick);
});
}, 1000);
}); // Temp removal of 1 sec timeout to prevent bug where overlay does not open. We need to find a better solution.
};
})
@@ -26,6 +26,34 @@
}
}
// TARGET OVERLAY
.umb-overlay-target {
position: fixed;
width: 400px;
height: 400px;
overflow: hidden;
border: 1px solid #ccc;
background: white;
padding: 7px;
z-index: 996660;
box-sizing: border-box;
.umb-overlay-header{
border-bottom: 1px solid #d9d9d9;
color: #999;
padding: 10px;
margin-top: 0;
text-align: center;
}
.umb-overlay-container{
position: absolute;
top: 68px;
left: 7px;
right: 7px;
bottom: 7px;
overflow: auto;
}
}
@@ -692,4 +692,54 @@
}
/* ---------- DIALOG ---------- */
.dialog-grid {
text-align: center;
display: flex;
flex-wrap: wrap;
align-content: space-between;
}
.dialog-grid-element {
flex: 0 0 80px;
height: 80px;
padding: 5px;
overflow: hidden;
font-size: 11px;
margin: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
.dialog-grid-element:hover {
background: #2e8aea;
}
.dialog-grid-element a {
color: #222;
text-decoration: none;
}
.dialog-grid-element i {
font-size: 30px;
color: #999;
display: block;
margin-bottom: 10px;
}
.dialog-grid-element a:hover {
background: #2e8aea;
color: white;
}
.dialog-grid-element a:hover i {
color: white;
}
@@ -0,0 +1,15 @@
/**
* @ngdoc controller
* @name Umbraco.Editors.DocumentType.PropertyController
* @function
*
* @description
* The controller for the content type editor property dialog
*/
function CompositionsController($scope, contentTypeResource) {
}
angular.module("umbraco").controller("Umbraco.Editors.DocumentType.CompositionsController", CompositionsController);
@@ -0,0 +1,8 @@
<div class="dialog-grid">
<div class="dialog-grid-element" ng-repeat="contentType in model.contentTypes | filter:{show:true}">
<a href="" ng-click="model.chooseContentType(contentType)">
<i class="icon {{contentType.icon}}"></i>
{{ contentType.name }}
</a>
</div>
</div>
@@ -6,12 +6,11 @@
* @description
* The controller for the content type editor property dialog
*/
function PermissionsController($scope, contentTypeResource) {
function PermissionsController($scope, contentTypeResource, $log) {
/* ----------- SCOPE VARIABLES ----------- */
$scope.contentTypes = [];
$scope.showPlaceholderDetails = false;
/* ---------- INIT ---------- */
@@ -47,46 +46,6 @@ function PermissionsController($scope, contentTypeResource) {
}
$scope.addSelectedContentType = function(selectedContentType) {
if(selectedContentType.alias !== "undefined" || selectedContentType.alias !== null) {
var reformatedContentType = {
"name": selectedContentType.name,
"id": {
"m_boxed": {
"m_value": selectedContentType.id
}
},
"icon": selectedContentType.icon,
"key": selectedContentType.key,
"alias": selectedContentType.alias
};
// push selected content type to allowed array
$scope.contentType.allowedContentTypes.push(reformatedContentType);
// hide selected content type from content types array
for (var contentTypeIndex = 0; contentTypeIndex < $scope.contentTypes.length; contentTypeIndex++) {
var contentType = $scope.contentTypes[contentTypeIndex];
if( selectedContentType.alias === contentType.alias ) {
contentType.show = false;
}
}
// hide placeholder details
$scope.showPlaceholderDetails = false;
}
};
$scope.togglePlaceholderDetails = function() {
$scope.showPlaceholderDetails = !$scope.showPlaceholderDetails;
};
$scope.removeAllowedChildNode = function(selectedContentType) {
// splice from array
@@ -105,6 +64,53 @@ function PermissionsController($scope, contentTypeResource) {
};
$scope.addItemOverlay = function ($event) {
$scope.showDialog = false;
$scope.dialogModel = {};
$scope.dialogModel.title = "Choose content type";
$scope.dialogModel.contentTypes = $scope.contentTypes;
$scope.dialogModel.event = $event;
$scope.dialogModel.view = "views/documentType/dialogs/contenttypes/contenttypes.html";
$scope.showDialog = true;
$scope.dialogModel.chooseContentType = function(selectedContentType) {
// format content type to match service
var reformatedContentType = {
"name": selectedContentType.name,
"id": {
"m_boxed": {
"m_value": selectedContentType.id
}
},
"icon": selectedContentType.icon,
"key": selectedContentType.key,
"alias": selectedContentType.alias
};
// push to content type model
$scope.contentType.allowedContentTypes.push(reformatedContentType);
// hide selected content type from content types array
for (var contentTypeIndex = 0; contentTypeIndex < $scope.contentTypes.length; contentTypeIndex++) {
var contentType = $scope.contentTypes[contentTypeIndex];
if( selectedContentType.alias === contentType.alias ) {
contentType.show = false;
}
}
$scope.showDialog = false;
$scope.dialogModel = null;
};
$scope.dialogModel.close = function(){
$scope.showDialog = false;
$scope.dialogModel = null;
};
};
}
@@ -51,14 +51,8 @@
</div>
<!-- NODE PLACEHOLDER -->
<div class="node node-placeholder" ng-if="(contentTypes | filter:{show:true}).length > 0">
<div class="node-placeholder-text" ng-click="togglePlaceholderDetails()" ng-if="showPlaceholderDetails==false">Allow child node</div>
<div class="node-placeholder-details" ng-if="showPlaceholderDetails">
<select class="node-select" ng-model="selectedContentType" ng-options="contentType as contentType.name for contentType in contentTypes | filter:{show:true} | orderBy:'name'">
<option value="">Select content type</option>
</select>
<a href="" class="btn" ng-click="addSelectedContentType(selectedContentType)">Allow</a>
</div>
<div class="node node-placeholder" ng-if="(contentTypes | filter:{show:true}).length > 0" ng-click="addItemOverlay($event)">
<div class="node-placeholder-text">Allow child node</div>
</div>
<!-- ALL NODES ADDED -->
@@ -74,4 +68,13 @@
</div>
<umb-overlay
ng-style=""
ng-if="showDialog"
model="dialogModel"
animation="slide-in-right"
position="target"
view="dialogModel.view">
</umb-overlay>
</div>
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using System;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Umbraco.Core;
using Umbraco.Core.IO;
@@ -14,6 +15,11 @@ namespace Umbraco.Web.Models.ContentEditing
[DataContract(Name = "contentType", Namespace = "")]
public class ContentTypeBasic : EntityBasic
{
[DataMember(Name = "updateDate")]
public DateTime UpdateDate { get; set; }
[DataMember(Name = "createDate")]
public DateTime CreateDate { get; set; }
[DataMember(Name = "description")]
public string Description { get; set; }
@@ -46,8 +46,6 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(dto => dto.AllowedAsRoot, expression => expression.MapFrom(display => display.AllowAsRoot))
.ForMember(dto => dto.CreatorId, expression => expression.Ignore())
.ForMember(dto => dto.Level, expression => expression.Ignore())
.ForMember(dto => dto.CreateDate, expression => expression.Ignore())
.ForMember(dto => dto.UpdateDate, expression => expression.Ignore())
.ForMember(dto => dto.SortOrder, expression => expression.Ignore())
//mapped in aftermap
.ForMember(dto => dto.AllowedContentTypes, expression => expression.Ignore())
@@ -65,7 +63,7 @@ namespace Umbraco.Web.Models.Mapping
//Sync allowed child types
var allowedTypes = source.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i));
dest.AllowedContentTypes = allowedTypes;
dest.AllowedContentTypes = allowedTypes.ToArray();
//sync compositions
var current = dest.CompositionAliases().ToArray();
@@ -108,22 +106,17 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(
dto => dto.CompositeContentTypes,
expression => expression.MapFrom(dto => dto.ContentTypeComposition))
.ForMember(
dto => dto.CompositeContentTypes,
expression => expression.MapFrom(dto => dto.ContentTypeComposition))
.ForMember(
dto => dto.Groups,
expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, _propertyEditorResolver)));
config.CreateMap<PropertyGroupDisplay, PropertyGroup>()
.ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0))
.ForMember(g => g.CreateDate, expression => expression.Ignore())
.ForMember(g => g.Key, expression => expression.Ignore())
.ForMember(g => g.CreateDate, expression => expression.Ignore())
.ForMember(g => g.HasIdentity, expression => expression.Ignore())
.ForMember(g => g.UpdateDate, expression => expression.Ignore())
.ForMember(g => g.HasIdentity, expression => expression.Ignore())
.ForMember(dto => dto.CreateDate, expression => expression.Ignore())
.ForMember(dto => dto.UpdateDate, expression => expression.Ignore())
//only map if a parent is actually set
.ForMember(g => g.ParentId, expression => expression.Condition(display => display.ParentGroupId > 0))
@@ -150,6 +143,9 @@ namespace Umbraco.Web.Models.Mapping
if (dataType == null) throw new NullReferenceException("No data type found with id " + propertyTypeDisplay.DataTypeId);
return new PropertyType(dataType, propertyTypeDisplay.Alias);
})
.ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0))
.ForMember(dto => dto.CreateDate, expression => expression.Ignore())
.ForMember(dto => dto.UpdateDate, expression => expression.Ignore())
.ForMember(type => type.PropertyGroupId, expression => expression.MapFrom(display => new Lazy<int>(() => display.GroupId, LazyThreadSafetyMode.None)))
.ForMember(type => type.Key, expression => expression.Ignore())
.ForMember(type => type.HelpText, expression => expression.Ignore())
@@ -158,9 +154,6 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(type => type.Alias, expression => expression.Ignore())
//ignore because this is obsolete and shouldn't be used
.ForMember(type => type.DataTypeId, expression => expression.Ignore())
//ignore because these are 'readonly'
.ForMember(type => type.CreateDate, expression => expression.Ignore())
.ForMember(type => type.UpdateDate, expression => expression.Ignore())
.ForMember(type => type.Mandatory, expression => expression.MapFrom(display => display.Validation.Mandatory))
.ForMember(type => type.ValidationRegExp, expression => expression.MapFrom(display => display.Validation.Pattern))
.ForMember(type => type.PropertyEditorAlias, expression => expression.MapFrom(display => display.Editor))
@@ -165,7 +165,9 @@ namespace Umbraco.Web.Models.Mapping
ContentTypeId = contentType.Id,
ContentTypeName = contentType.Name,
GroupId = groupId,
Inherited = inherited
Inherited = inherited,
DataTypeId = p.DataTypeDefinitionId,
SortOrder = p.SortOrder
});
}