From a6b56db2f542a3a9e971f5798cc9fe879d31e211 Mon Sep 17 00:00:00 2001 From: Jordan Lane Date: Thu, 29 Oct 2015 15:16:44 +0000 Subject: [PATCH 01/42] Fixes an issue with the contentService.RePublishAll() where it wouldn't publish an old published version if the newest version of the content wasn't published i.e had a scheduled date set because this would create a new version with status not published --- .../Repositories/ContentRepository.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 60d7d8e7ef..2eab12a6a0 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -234,7 +234,7 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + var descendants = GetPagedResultsByQueryNoFilter(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); var xmlItems = (from descendant in descendants let xml = serializer(descendant) @@ -728,7 +728,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// public void AddOrUpdateContentXml(IContent content, Func xml) - { + { _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); } @@ -787,6 +787,26 @@ namespace Umbraco.Core.Persistence.Repositories } + /// + /// Gets paged content results + /// + /// Query to excute + /// Page number + /// Page size + /// Total records query would return without paging + /// Field to order by + /// Direction to order by + /// An Enumerable list of objects + public IEnumerable GetPagedResultsByQueryNoFilter(IQuery query, long pageIndex, int pageSize, out long totalRecords, + string orderBy, Direction orderDirection) + { + + return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, orderBy, orderDirection); + + } + #endregion #region IRecycleBinRepository members @@ -826,11 +846,11 @@ namespace Umbraco.Core.Persistence.Repositories var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) .ToArray(); - + var ids = dtos .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) .Select(x => x.TemplateId.Value).ToArray(); - + //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); @@ -972,4 +992,4 @@ namespace Umbraco.Core.Persistence.Repositories _contentXmlRepository.Dispose(); } } -} +} \ No newline at end of file From 1e4c9c2505c155e72b8c74215487db1e3a6684c7 Mon Sep 17 00:00:00 2001 From: Jordan Lane Date: Thu, 29 Oct 2015 15:20:35 +0000 Subject: [PATCH 02/42] Set the publish status of new content version to saved if validation of publication fails e.g. a release date of the future or past unpublish date --- src/Umbraco.Core/Services/ContentService.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index da25bd0938..6819ce2859 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1966,6 +1966,10 @@ namespace Umbraco.Core.Services var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateContentRepository(uow)) { + if (published == false) + { + content.ChangePublishedState(PublishedState.Saved); + } //Since this is the Save and Publish method, the content should be saved even though the publish fails or isn't allowed if (content.HasIdentity == false) { From c5cf1fa6b2d3930bc4ee818124feaac217936c71 Mon Sep 17 00:00:00 2001 From: Jordan Lane Date: Thu, 29 Oct 2015 15:21:15 +0000 Subject: [PATCH 03/42] Checks if content has expired or is awaiting release before publishing --- src/Umbraco.Core/Services/ContentService.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 6819ce2859..68038fe9c0 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -2112,6 +2112,22 @@ namespace Umbraco.Core.Services content.Name, content.Id)); return PublishStatusType.FailedPathNotPublished; } + else if (content.ExpireDate.HasValue && content.ExpireDate.Value > DateTime.MinValue && DateTime.Now > content.ExpireDate.Value) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' has expired and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedHasExpired; + } + else if (content.ReleaseDate.HasValue && content.ReleaseDate.Value > DateTime.MinValue && content.ReleaseDate.Value > DateTime.Now) + { + Logger.Info( + string.Format( + "Content '{0}' with Id '{1}' is awaiting release and could not be published.", + content.Name, content.Id)); + return PublishStatusType.FailedAwaitingRelease; + } return PublishStatusType.Success; } From 8e6bac534a9042828c0331cd49b6419a5f521fe4 Mon Sep 17 00:00:00 2001 From: Jordan Lane Date: Thu, 29 Oct 2015 15:23:40 +0000 Subject: [PATCH 04/42] Fix a bug where PublishStatusType would fallback to FailedContentInvalid (and break) if it was expired or trashed and add error message for FailedHasExpired TODO: Add error message to other languages --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 3 +++ src/Umbraco.Web/Editors/ContentController.cs | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 8f493d2582..90c9e0c605 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -689,6 +689,9 @@ To manage your website, simply open the Umbraco back office and start adding con %0% could not be published because the item is scheduled for release. ]]> + diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 1b6fbfa3cb..9e63a45353 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -659,9 +659,17 @@ namespace Umbraco.Web.Editors new[] {string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id)}).Trim()); break; case PublishStatusType.FailedHasExpired: - //TODO: We should add proper error messaging for this! + display.AddWarningNotification( + Services.TextService.Localize("publish"), + Services.TextService.Localize("publish/contentPublishedFailedExpired", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id), + }).Trim()); + break; case PublishStatusType.FailedIsTrashed: //TODO: We should add proper error messaging for this! + break; case PublishStatusType.FailedContentInvalid: display.AddWarningNotification( Services.TextService.Localize("publish"), From ecd024c4621270f1c0436bb1b0bdca67d002ff83 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 8 Jan 2016 13:59:40 +0100 Subject: [PATCH 05/42] U4-7681 Legacy CMSNode.Parent uses old SqlHelper API and doesn't keep a reference --- src/Umbraco.Web/umbraco.presentation/content.cs | 2 +- src/Umbraco.Web/umbraco.presentation/page.cs | 2 +- .../umbraco.presentation/umbraco/channels/api.cs | 4 ++-- .../umbraco/preview/PreviewContent.cs | 4 ++-- .../umbraco/uQuery/MediaExtensions.cs | 6 +++--- src/umbraco.cms/businesslogic/CMSNode.cs | 14 +++++++++++--- src/umbraco.cms/businesslogic/Content.cs | 10 +++++++++- src/umbraco.cms/businesslogic/web/Document.cs | 9 +++------ 8 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 23e44896f4..2d80eb0481 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -191,7 +191,7 @@ namespace umbraco // check if document *is* published, it could be unpublished by an event if (d.Published) { - var parentId = d.Level == 1 ? -1 : d.Parent.Id; + var parentId = d.Level == 1 ? -1 : d.ParentId; // fix sortOrder - see note in UpdateSortOrder var node = GetPreviewOrPublishedNode(d, xmlContentCopy, false); diff --git a/src/Umbraco.Web/umbraco.presentation/page.cs b/src/Umbraco.Web/umbraco.presentation/page.cs index ce2b55152b..e71a290754 100644 --- a/src/Umbraco.Web/umbraco.presentation/page.cs +++ b/src/Umbraco.Web/umbraco.presentation/page.cs @@ -70,7 +70,7 @@ namespace umbraco var docParentId = -1; try { - docParentId = document.Parent.Id; + docParentId = document.ParentId; } catch (ArgumentException) { diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs index 49296884fa..ace78f6803 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/channels/api.cs @@ -93,7 +93,7 @@ namespace umbraco.presentation.channels p.dateCreated = d.CreateDateTime; p.page_title = d.Text; p.page_id = d.Id; - p.page_parent_id = d.Parent.Id; + p.page_parent_id = d.ParentId; blogPostsObjects.Add(p); } @@ -150,7 +150,7 @@ namespace umbraco.presentation.channels p.dateCreated = d.CreateDateTime; p.title = d.Text; p.page_id = d.Id; - p.wp_page_parent_id = d.Parent.Id; + p.wp_page_parent_id = d.ParentId; p.wp_page_parent_title = d.Parent.Text; p.permalink = library.NiceUrl(d.Id); p.description = d.getProperty(userChannel.FieldDescriptionAlias).Value.ToString(); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs index b8d37fbb65..2499c1c817 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/preview/PreviewContent.cs @@ -83,7 +83,7 @@ namespace umbraco.presentation.preview var previewNodes = new List(); - var parentId = documentObject.Level == 1 ? -1 : documentObject.Parent.Id; + var parentId = documentObject.Level == 1 ? -1 : documentObject.ParentId; while (parentId > 0 && XmlContent.GetElementById(parentId.ToString(CultureInfo.InvariantCulture)) == null) { @@ -97,7 +97,7 @@ namespace umbraco.presentation.preview foreach (var document in previewNodes) { //Inject preview xml - parentId = document.Level == 1 ? -1 : document.Parent.Id; + parentId = document.Level == 1 ? -1 : document.ParentId; var previewXml = document.ToPreviewXml(XmlContent); if (document.ContentEntity.Published == false && ApplicationContext.Current.Services.ContentService.HasPublishedVersion(document.Id)) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs index 42b9cd757b..411734ae99 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/uQuery/MediaExtensions.cs @@ -19,13 +19,13 @@ namespace umbraco /// Media nodes as IEnumerable public static IEnumerable GetAncestorMedia(this Media media) { - var ancestor = new Media(media.Parent.Id); + var ancestor = new Media(media.ParentId); while (ancestor != null && ancestor.Id != -1) { yield return ancestor; - ancestor = new Media(ancestor.Parent.Id); + ancestor = new Media(ancestor.ParentId); } } @@ -53,7 +53,7 @@ namespace umbraco { if (media.Parent != null) { - var parentMedia = new Media(media.Parent.Id); + var parentMedia = new Media(media.ParentId); foreach (var siblingMedia in parentMedia.GetChildMedia().Where(childMedia => childMedia.Id != media.Id)) { diff --git a/src/umbraco.cms/businesslogic/CMSNode.cs b/src/umbraco.cms/businesslogic/CMSNode.cs index 661e620056..3b4e9c9b31 100644 --- a/src/umbraco.cms/businesslogic/CMSNode.cs +++ b/src/umbraco.cms/businesslogic/CMSNode.cs @@ -617,6 +617,7 @@ order by level,sortOrder"; /// /// Target CMSNode id [Obsolete("Obsolete, Use Umbraco.Core.Services.ContentService.Move() or Umbraco.Core.Services.MediaService.Move()", false)] + [EditorBrowsable(EditorBrowsableState.Never)] public virtual void Move(int newParentId) { CMSNode parent = new CMSNode(newParentId); @@ -796,6 +797,7 @@ order by level,sortOrder"; internal set { _parentid = value; } } + private IUmbracoEntity _parent; /// /// Given the hierarchical tree structure a CMSNode has only one newParent but can have many children /// @@ -805,15 +807,21 @@ order by level,sortOrder"; get { if (Level == 1) throw new ArgumentException("No newParent node"); - return new CMSNode(_parentid); + if (_parent == null) + { + _parent = ApplicationContext.Current.Services.EntityService.Get(Entity.ParentId); + } + return new CMSNode(_parent); } set { _parentid = value.Id; - SqlHelper.ExecuteNonQuery("update umbracoNode set parentId = " + value.Id.ToString() + " where id = " + this.Id.ToString()); + SqlHelper.ExecuteNonQuery("update umbracoNode set parentId = " + value.Id + " where id = " + this.Id.ToString()); if (Entity != null) Entity.ParentId = value.Id; + + _parent = value.Entity; } } @@ -1183,7 +1191,7 @@ order by level,sortOrder"; x.Attributes.Append(xmlHelper.addAttribute(xd, "id", this.Id.ToString())); x.Attributes.Append(xmlHelper.addAttribute(xd, "key", this.UniqueId.ToString())); if (this.Level > 1) - x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", this.Parent.Id.ToString())); + x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", this.ParentId.ToString())); else x.Attributes.Append(xmlHelper.addAttribute(xd, "parentID", "-1")); x.Attributes.Append(xmlHelper.addAttribute(xd, "level", this.Level.ToString())); diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index 36eafb91a2..f014ddae93 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -115,6 +115,14 @@ namespace umbraco.cms.businesslogic #region Public Properties + /// + /// Get the newParent id of the node + /// + public override int ParentId + { + get { return ContentBase == null ? base.ParentId : ContentBase.ParentId; } + } + /// /// The current Content objects ContentType, which defines the Properties of the Content (data) /// @@ -411,7 +419,7 @@ namespace umbraco.cms.businesslogic x.Attributes.Append(XmlHelper.AddAttribute(xd, "key", this.UniqueId.ToString())); x.Attributes.Append(XmlHelper.AddAttribute(xd, "version", this.Version.ToString())); if (this.Level > 1) - x.Attributes.Append(XmlHelper.AddAttribute(xd, "parentID", this.Parent.Id.ToString())); + x.Attributes.Append(XmlHelper.AddAttribute(xd, "parentID", this.ParentId.ToString())); else x.Attributes.Append(XmlHelper.AddAttribute(xd, "parentID", "-1")); x.Attributes.Append(XmlHelper.AddAttribute(xd, "level", this.Level.ToString())); diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index 0c5f2c3025..f5332cb68c 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -473,7 +473,7 @@ namespace umbraco.cms.businesslogic.web #endregion #region Public Properties - + public override int sortOrder { get @@ -514,10 +514,7 @@ namespace umbraco.cms.businesslogic.web public override int ParentId { - get - { - return ContentEntity == null ? base.ParentId : ContentEntity.ParentId; - } + get { return ContentEntity == null ? base.ParentId : ContentEntity.ParentId; } } public override string Path @@ -1291,7 +1288,7 @@ namespace umbraco.cms.businesslogic.web x.Attributes.Append(addAttribute(xd, "key", UniqueId.ToString())); // x.Attributes.Append(addAttribute(xd, "version", Version.ToString())); if (Level > 1) - x.Attributes.Append(addAttribute(xd, "parentID", Parent.Id.ToString())); + x.Attributes.Append(addAttribute(xd, "parentID", ParentId.ToString())); else x.Attributes.Append(addAttribute(xd, "parentID", "-1")); x.Attributes.Append(addAttribute(xd, "level", Level.ToString())); From e509099cdbfb4fbb88835b6a3806d115155bc18a Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 7 Jan 2016 13:40:37 +0100 Subject: [PATCH 06/42] Setting for enabling inherited documenttypes. --- .../UmbracoSettings/ContentElement.cs | 17 +++++++++++++++++ .../UmbracoSettings/IContentSection.cs | 2 ++ .../config/umbracoSettings.config | 3 +++ src/umbraco.businesslogic/UmbracoSettings.cs | 11 +++++++++++ 4 files changed, 33 insertions(+) diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index ced63f04bb..077658382f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -280,6 +280,18 @@ namespace Umbraco.Core.Configuration.UmbracoSettings } } + [ConfigurationProperty("EnableInheritedDocumentTypes")] + internal InnerTextConfigurationElement EnableInheritedDocumentTypes + { + get + { + return new OptionalInnerTextConfigurationElement( + (InnerTextConfigurationElement)this["EnableInheritedDocumentTypes"], + //set the default + true); + } + } + string IContentSection.NotificationEmailAddress { get { return Notifications.NotificationEmailAddress; } @@ -414,5 +426,10 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { get { return DefaultDocumentTypeProperty; } } + + bool IContentSection.EnableInheritedDocumentTypes + { + get { return ContinouslyUpdateXmlDiskCache; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs index 93e3260b44..ebdd9ae637 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs @@ -59,5 +59,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool GlobalPreviewStorageEnabled { get; } string DefaultDocumentTypeProperty { get; } + + bool EnableInheritedDocumentTypes { get; } } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 5a8af019f0..77d31fda09 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -102,6 +102,9 @@ Textstring + + + true diff --git a/src/umbraco.businesslogic/UmbracoSettings.cs b/src/umbraco.businesslogic/UmbracoSettings.cs index cc821528e2..3d489cfaa4 100644 --- a/src/umbraco.businesslogic/UmbracoSettings.cs +++ b/src/umbraco.businesslogic/UmbracoSettings.cs @@ -609,6 +609,17 @@ namespace umbraco get { return UmbracoConfig.For.UmbracoSettings().Content.DefaultDocumentTypeProperty; } } + /// + /// Enables inherited document types. + /// This feature is not recommended and therefore is not enabled by default in new installations. + /// Inherited document types will not be supported in v8. + /// + //[Obsolete("This will not be supported in v8")] + public static bool EnableInheritedDocumentTypes + { + get { return UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes; } + } + private static string _path; /// From 5360bcff5268cbc960803836cc62c2e39b2de0b6 Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 7 Jan 2016 13:42:17 +0100 Subject: [PATCH 07/42] context menu for creating child doctype. --- src/Umbraco.Web/Trees/ContentTypeTreeController.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index e29f9d6be9..94753afd16 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -4,6 +4,7 @@ using System.Net.Http.Formatting; using umbraco; using umbraco.BusinessLogic.Actions; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi.Filters; @@ -62,6 +63,9 @@ namespace Umbraco.Web.Trees { var menu = new MenuItemCollection(); + var enableInheritedDocumentTypes = UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes; + + if (id == Constants.System.Root.ToInvariantString()) { //set the default to create @@ -97,6 +101,10 @@ namespace Umbraco.Web.Trees } else { + if (enableInheritedDocumentTypes) + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + } menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionExport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity { From 7a1f4b6b4b5b5a3caaffd857cb6996d61d0bfbab Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 7 Jan 2016 13:43:37 +0100 Subject: [PATCH 08/42] include parents as compositions. --- src/Umbraco.Core/Models/ContentType.cs | 9 +++++++++ .../Editors/ContentTypeController.cs | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 355d724fbe..7219230bc2 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Models { @@ -34,6 +35,10 @@ namespace Umbraco.Core.Models [Obsolete("This method is obsolete, use ContentType(IContentType parent, string alias) instead.", false)] public ContentType(IContentType parent) : this(parent, null) { + if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes) + { + AddContentType(parent); + } } /// @@ -45,6 +50,10 @@ namespace Umbraco.Core.Models public ContentType(IContentType parent, string alias) : base(parent, alias) { + if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes) + { + AddContentType(parent); + } _allowedTemplates = new List(); } diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 4622fee1b5..d0c6c32d67 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; @@ -11,6 +12,7 @@ using Umbraco.Core.Services; using Umbraco.Core.PropertyEditors; using System.Net.Http; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; @@ -184,7 +186,18 @@ namespace Umbraco.Web.Editors /// public ContentTypeDisplay GetEmpty(int parentId) { - var ct = new ContentType(parentId); + IContentType ct = null; + if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes && + parentId != Constants.System.Root) + { + var parent = Services.ContentTypeService.GetContentType(parentId); + ct = new ContentType(parent, String.Empty); + } + else + { + ct = new ContentType(parentId); + } + ct.Icon = "icon-document"; var dto = Mapper.Map(ct); From 0d37357376c497199bdbb7dd23a301f6c9013bad Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 7 Jan 2016 13:44:28 +0100 Subject: [PATCH 09/42] Mark compositions coming from parents with metadata. Ensure metadata is not ignored. --- .../Editors/ContentTypeControllerBase.cs | 81 ++++++++++++++++++- .../Models/Mapping/EntityModelMapper.cs | 4 +- 2 files changed, 80 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 975154e6e8..3ea30c02d4 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -9,6 +9,7 @@ using System.Web.Http; using AutoMapper; using Newtonsoft.Json; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Dictionary; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; @@ -57,6 +58,7 @@ namespace Umbraco.Web.Editors //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic IContentTypeComposition[] allContentTypes; + switch (type) { case UmbracoObjectTypes.DocumentType: @@ -90,9 +92,57 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes); + // note: there are many sanity checks missing here and there ;-(( + // make sure once and for all + //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) + // throw new Exception("A parent does not belong to a composition."); - return filtered + // find out if any content type uses this content type + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == contentTypeId)).ToArray(); + if (isUsing.Length > 0) + { + //if already in use a composition, do not allow any composited types + return new List(); + } + + // if it is not used then composition is possible + // hashset guarantees unicity on Id + var list = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + // usable types are those that are top-level + var usableContentTypes = allContentTypes + .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + foreach (var x in usableContentTypes) + list.Add(x); + + // indirect types are those that we use, directly or indirectly + var indirectContentTypes = GetIndirect(source).ToArray(); + foreach (var x in indirectContentTypes) + list.Add(x); + + if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes) + { + // directContentTypes are those we use directly + // they are already in indirectContentTypes, no need to add to the list + var directContentTypes = source.ContentTypeComposition.ToArray(); + + var enabled = usableContentTypes.Select(x => x.Id) // those we can use + .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used + .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used + .Where(x => x != source.ParentId) // but not the parent + .Distinct() + .ToArray(); + + foreach (var x in list) + if (enabled.Contains(x.Id) == false) + x.AdditionalData["compositionIsInheritedFromParent"] = true; + } + + return list + .Where(x => x.Id != contentTypeId) + .OrderBy(x => x.Name) .Select(Mapper.Map) .Select(x => { @@ -101,7 +151,32 @@ namespace Umbraco.Web.Editors }) .ToList(); } - + + private static IEnumerable GetIndirect(IContentTypeComposition ctype) + { + // hashset guarantees unicity on Id + var all = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); + + var stack = new Stack(); + + if (ctype != null) + { + foreach (var x in ctype.ContentTypeComposition) + stack.Push(x); + } + + while (stack.Count > 0) + { + var x = stack.Pop(); + all.Add(x); + foreach (var y in x.ContentTypeComposition) + stack.Push(y); + } + + return all; + } /// /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors diff --git a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs index 5610a70008..f73d7daef1 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs @@ -72,8 +72,8 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(basic => basic.Path, expression => expression.MapFrom(x => x.Path)) .ForMember(basic => basic.ParentId, expression => expression.MapFrom(x => x.ParentId)) - .ForMember(dto => dto.Trashed, expression => expression.Ignore()) - .ForMember(x => x.AdditionalData, expression => expression.Ignore()); + .ForMember(dto => dto.Trashed, expression => expression.Ignore()); + //.ForMember(x => x.AdditionalData, expression => expression.Ignore()); config.CreateMap() //default to document icon From 08c4dede089995c4862d893d6e50a4321133470c Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 7 Jan 2016 13:44:54 +0100 Subject: [PATCH 10/42] Output disabled class in backoffice. --- .../overlays/contenttypeeditor/compositions/compositions.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 3467b8568e..514ba57fa3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -23,7 +23,7 @@
  • -
    +
    Date: Thu, 7 Jan 2016 16:06:58 +0100 Subject: [PATCH 11/42] Pass along parentId when getting available compositions since we need it to get available compositions when creating the empty scaffolding. Include ancestors in the list of compositions. Mark ancestors as disabled/locked. --- .../Models/ContentTypeCompositionBase.cs | 5 ++++ .../common/resources/contenttype.resource.js | 4 +-- .../compositions/compositions.html | 4 +-- .../views/documenttypes/edit.controller.js | 2 +- .../Editors/ContentTypeController.cs | 4 +-- .../Editors/ContentTypeControllerBase.cs | 29 ++++++++++++------- .../Editors/MediaTypeController.cs | 4 +-- .../Editors/MemberTypeController.cs | 4 +-- 8 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 5ac21885d7..53018ce6c1 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -102,6 +102,11 @@ namespace Umbraco.Core.Models throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); _contentTypeComposition.Add(contentType); + //Make sure to include the composition's compositions since this is possible with inheritance + foreach (var contentTypeComposition in contentType.ContentTypeComposition) + { + _contentTypeComposition.Add(contentTypeComposition); + } OnPropertyChanged(ContentTypeCompositionSelector); return true; } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 4b1ed8f9c1..dda6bc38b3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -7,13 +7,13 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { - getAvailableCompositeContentTypes: function (contentTypeId) { + getAvailableCompositeContentTypes: function (contentTypeId, parentId) { return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentTypeApiBaseUrl", "GetAvailableCompositeContentTypes", - [{ contentTypeId: contentTypeId }])), + [{ contentTypeId: contentTypeId }, { parentId: parentId }])), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 514ba57fa3..e82aeb839c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -21,9 +21,9 @@
      -
    • +
    • -
      +
      GetAvailableCompositeContentTypes(int contentTypeId) + public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId, int parentId) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, parentId, UmbracoObjectTypes.DocumentType); } [UmbracoTreeAuthorize( diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 3ea30c02d4..e3799c3c88 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -51,7 +51,7 @@ namespace Umbraco.Web.Editors /// Returns the available composite content types for a given content type ///
/// - protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type) + protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, int parentId, UmbracoObjectTypes type) { IContentTypeComposition source = null; @@ -124,22 +124,29 @@ namespace Umbraco.Web.Editors if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes) { - // directContentTypes are those we use directly - // they are already in indirectContentTypes, no need to add to the list - var directContentTypes = source.ContentTypeComposition.ToArray(); + // get the ancestorIds via the parent + var ancestorIds = new int[0]; + if (parentId > 0) + { + var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId); + ancestorIds = parent.Path.Split(',').Select(int.Parse).ToArray(); + } + + // add all ancestors as compositions (since they are implicitly compositions by inheritance) + foreach (var x in allContentTypes) + if (ancestorIds.Contains(x.Id)) + list.Add(x); - var enabled = usableContentTypes.Select(x => x.Id) // those we can use + // get the ids of compositions that are inherited from ancestors and add metadata to those compositions + var inheritedFromAncestorsIds = usableContentTypes.Select(x => x.Id) // those we can use .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used - .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used - .Where(x => x != source.ParentId) // but not the parent + .Where(x => ancestorIds.Contains(x) == false) // but not the parents .Distinct() .ToArray(); - foreach (var x in list) - if (enabled.Contains(x.Id) == false) - x.AdditionalData["compositionIsInheritedFromParent"] = true; + if (inheritedFromAncestorsIds.Contains(x.Id) == false) + x.AdditionalData["compositionIsInheritedFromAncestors"] = true; } - return list .Where(x => x.Id != contentTypeId) .OrderBy(x => x.Name) diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index d54981ff45..3f2bde1dc9 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -83,9 +83,9 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId) + public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId, int parentId) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, parentId, UmbracoObjectTypes.MediaType); } public ContentTypeCompositionDisplay GetEmpty(int parentId) diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 7716f6ea00..72312669b2 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -79,9 +79,9 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId) + public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId, int parentId) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, parentId, UmbracoObjectTypes.MemberType); } public ContentTypeCompositionDisplay GetEmpty() From a7212f6b607cd9551240dfb15a75dede10b48aea Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 7 Jan 2016 16:31:01 +0100 Subject: [PATCH 12/42] Changing config setting to the default. Referencing wrong config element. --- .../Configuration/UmbracoSettings/ContentElement.cs | 8 ++++---- src/Umbraco.Web.UI/config/umbracoSettings.config | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 077658382f..b2e946a597 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -286,9 +286,9 @@ namespace Umbraco.Core.Configuration.UmbracoSettings get { return new OptionalInnerTextConfigurationElement( - (InnerTextConfigurationElement)this["EnableInheritedDocumentTypes"], - //set the default - true); + (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], + //set the default + false); } } @@ -429,7 +429,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings bool IContentSection.EnableInheritedDocumentTypes { - get { return ContinouslyUpdateXmlDiskCache; } + get { return EnableInheritedDocumentTypes; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index 77d31fda09..f20579e698 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -104,7 +104,7 @@ Textstring - true + false From 4db1599bcb155c02edea88536313c1ff98ce98cc Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 7 Jan 2016 16:56:32 +0100 Subject: [PATCH 13/42] Null checks for when using folders. Updated tree menu items. --- src/Umbraco.Web/Editors/ContentTypeController.cs | 4 +--- src/Umbraco.Web/Editors/ContentTypeControllerBase.cs | 3 ++- src/Umbraco.Web/Trees/ContentTypeTreeController.cs | 10 ++++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index aa414009f1..cd9caec397 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -191,12 +191,10 @@ namespace Umbraco.Web.Editors parentId != Constants.System.Root) { var parent = Services.ContentTypeService.GetContentType(parentId); - ct = new ContentType(parent, String.Empty); + ct = parent != null ? new ContentType(parent, String.Empty) : new ContentType(parentId); } else - { ct = new ContentType(parentId); - } ct.Icon = "icon-document"; diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index e3799c3c88..aa518b1bb6 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -129,7 +129,8 @@ namespace Umbraco.Web.Editors if (parentId > 0) { var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId); - ancestorIds = parent.Path.Split(',').Select(int.Parse).ToArray(); + if (parent != null) + ancestorIds = parent.Path.Split(',').Select(int.Parse).ToArray(); } // add all ancestors as compositions (since they are implicitly compositions by inheritance) diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 94753afd16..b6a0bfab66 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -95,7 +95,7 @@ namespace Umbraco.Web.Trees if (container.HasChildren() == false) { //can delete doc type - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); } menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); } @@ -104,8 +104,12 @@ namespace Umbraco.Web.Trees if (enableInheritedDocumentTypes) { menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); + } + else + { + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); } - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionExport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity { Id = int.Parse(id), @@ -114,6 +118,8 @@ namespace Umbraco.Web.Trees Name = "" }, "documenttypes", "settings"); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias)), true); + if (enableInheritedDocumentTypes) + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), true); } return menu; From 3676de0fe8eeddf2e2f4594a1212531e5f5400e0 Mon Sep 17 00:00:00 2001 From: Claus Date: Fri, 8 Jan 2016 12:57:50 +0100 Subject: [PATCH 14/42] Reverting code to add additional meta data on compositions. Added locked compositions to content type object. Changed UI to use the lockedCompositeContentTypes instead. --- .../compositions/compositions.html | 2 +- .../Editors/ContentTypeControllerBase.cs | 15 ++------ .../ContentTypeCompositionDisplay.cs | 4 +++ .../ContentTypeModelMapperExtensions.cs | 4 +++ .../Models/Mapping/EntityModelMapper.cs | 4 +-- .../Mapping/LockedCompositionsResolver.cs | 36 +++++++++++++++++++ .../Trees/ContentTypeTreeController.cs | 1 - src/Umbraco.Web/Umbraco.Web.csproj | 1 + 8 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index e82aeb839c..31f47702e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -21,7 +21,7 @@
    -
  • +
  • x.Id) // those we can use - .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used - .Where(x => ancestorIds.Contains(x) == false) // but not the parents - .Distinct() - .ToArray(); - foreach (var x in list) - if (inheritedFromAncestorsIds.Contains(x.Id) == false) - x.AdditionalData["compositionIsInheritedFromAncestors"] = true; } return list .Where(x => x.Id != contentTypeId) diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs index 9a138dd828..5f6e2c5ce5 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -44,6 +44,10 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "compositeContentTypes")] public IEnumerable CompositeContentTypes { get; set; } + //Locked compositions + [DataMember(Name = "lockedCompositeContentTypes")] + public IEnumerable LockedCompositeContentTypes { get; set; } + [DataMember(Name = "allowAsRoot")] public bool AllowAsRoot { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index e59a474afb..a6a7e79942 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -81,6 +81,10 @@ namespace Umbraco.Web.Models.Mapping dto => dto.CompositeContentTypes, expression => expression.MapFrom(dto => dto.ContentTypeComposition)) + .ForMember( + dto => dto.LockedCompositeContentTypes, + expression => expression.ResolveUsing(new LockedCompositionsResolver(applicationContext))) + .ForMember( dto => dto.Groups, expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, propertyEditorResolver))); diff --git a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs index f73d7daef1..5610a70008 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs @@ -72,8 +72,8 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(basic => basic.Path, expression => expression.MapFrom(x => x.Path)) .ForMember(basic => basic.ParentId, expression => expression.MapFrom(x => x.ParentId)) - .ForMember(dto => dto.Trashed, expression => expression.Ignore()); - //.ForMember(x => x.AdditionalData, expression => expression.Ignore()); + .ForMember(dto => dto.Trashed, expression => expression.Ignore()) + .ForMember(x => x.AdditionalData, expression => expression.Ignore()); config.CreateMap() //default to document icon diff --git a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs new file mode 100644 index 0000000000..435947c5de --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs @@ -0,0 +1,36 @@ +using AutoMapper; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models.Mapping +{ + internal class LockedCompositionsResolver : ValueResolver> + { + private readonly ApplicationContext _applicationContext; + + public LockedCompositionsResolver(ApplicationContext applicationContext) + { + _applicationContext = applicationContext; + } + + protected override IEnumerable ResolveCore(IContentTypeComposition source) + { + // get ancestor ids from path (exclude current node id) + var ancestorIds = source.Path.Substring(0, source.Path.LastIndexOf(',')).Split(',').Select(int.Parse); + var aliases = new List(); + // loop through all content types and return ordered aliases of ancestors + var allContentTypes = _applicationContext.Services.ContentTypeService.GetAllContentTypes().ToArray(); + foreach (var ancestorId in ancestorIds) + { + var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor != null) + { + aliases.Add(ancestor.Alias); + } + } + return aliases.OrderBy(x => x); + } + } +} diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index b6a0bfab66..5bb8695f9b 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -65,7 +65,6 @@ namespace Umbraco.Web.Trees var enableInheritedDocumentTypes = UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes; - if (id == Constants.System.Root.ToInvariantString()) { //set the default to create diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 89780c9aae..f41ad00cba 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -309,6 +309,7 @@ + From f62920dc475f86efeee47011db226ccb4f3d3cd9 Mon Sep 17 00:00:00 2001 From: Claus Date: Fri, 8 Jan 2016 13:59:39 +0100 Subject: [PATCH 15/42] Removed adding compositions from parents as this breaks core functionality. Ignoring property in mappings where it isnt needed. Updating the LockedCompositionsResolver to get parent and use path instead of trying to use path which can be null on GetEmpty. --- .../Models/ContentTypeCompositionBase.cs | 5 ---- .../ContentTypeModelMapperExtensions.cs | 5 ++-- .../Mapping/LockedCompositionsResolver.cs | 23 ++++++++++++------- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 53018ce6c1..5ac21885d7 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -102,11 +102,6 @@ namespace Umbraco.Core.Models throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); _contentTypeComposition.Add(contentType); - //Make sure to include the composition's compositions since this is possible with inheritance - foreach (var contentTypeComposition in contentType.ContentTypeComposition) - { - _contentTypeComposition.Add(contentTypeComposition); - } OnPropertyChanged(ContentTypeCompositionSelector); return true; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index a6a7e79942..5ad728ccab 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -54,10 +54,11 @@ namespace Umbraco.Web.Models.Mapping { return mapping .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) - .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) .ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore()) .ForMember(dto => dto.Notifications, expression => expression.Ignore()) - .ForMember(dto => dto.Errors, expression => expression.Ignore()); + .ForMember(dto => dto.Errors, expression => expression.Ignore()) + .ForMember(dto => dto.LockedCompositeContentTypes, exp => exp.Ignore()); } public static IMappingExpression MapBaseContentTypeEntityToDisplay( diff --git a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs index 435947c5de..d3d692d10d 100644 --- a/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/LockedCompositionsResolver.cs @@ -17,17 +17,24 @@ namespace Umbraco.Web.Models.Mapping protected override IEnumerable ResolveCore(IContentTypeComposition source) { - // get ancestor ids from path (exclude current node id) - var ancestorIds = source.Path.Substring(0, source.Path.LastIndexOf(',')).Split(',').Select(int.Parse); var aliases = new List(); - // loop through all content types and return ordered aliases of ancestors - var allContentTypes = _applicationContext.Services.ContentTypeService.GetAllContentTypes().ToArray(); - foreach (var ancestorId in ancestorIds) + // get ancestor ids from path of parent if not root + if (source.ParentId != Constants.System.Root) { - var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); - if (ancestor != null) + var parent = _applicationContext.Services.ContentTypeService.GetContentType(source.ParentId); + if (parent != null) { - aliases.Add(ancestor.Alias); + var ancestorIds = parent.Path.Split(',').Select(int.Parse); + // loop through all content types and return ordered aliases of ancestors + var allContentTypes = _applicationContext.Services.ContentTypeService.GetAllContentTypes().ToArray(); + foreach (var ancestorId in ancestorIds) + { + var ancestor = allContentTypes.FirstOrDefault(x => x.Id == ancestorId); + if (ancestor != null) + { + aliases.Add(ancestor.Alias); + } + } } } return aliases.OrderBy(x => x); From 05c8e10ef8dfb2e88abf0b661fbb605a9c21755b Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Sat, 9 Jan 2016 14:03:36 +0100 Subject: [PATCH 16/42] set disabled state on compositions in overlay + a little bit of restyling --- .../components/umbgroupsbuilder.directive.js | 2 - .../less/components/umb-checkbox-list.less | 36 ++++++-- .../compositions/compositions.controller.js | 27 ++++++ .../compositions/compositions.html | 86 +++++++++++-------- 4 files changed, 104 insertions(+), 47 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index b6fc6eb26c..936057fa6e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -154,8 +154,6 @@ scope.compositionsDialogModel = {}; scope.compositionsDialogModel.title = "Compositions"; scope.compositionsDialogModel.contentType = scope.model; - scope.compositionsDialogModel.availableCompositeContentTypes = scope.model.availableCompositeContentTypes; - scope.compositionsDialogModel.compositeContentTypes = scope.model.compositeContentTypes; scope.compositionsDialogModel.view = "views/common/overlays/contenttypeeditor/compositions/compositions.html"; scope.compositionsDialogModel.show = true; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less index 6098eb07e7..f604b79d04 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less @@ -6,20 +6,27 @@ .umb-checkbox-list__item { display: flex; align-items: center; - margin-bottom: 1px; - //border-bottom: 1px solid @grayLight; + margin-bottom: 2px; } -.umb-checkbox-list__item.-selected { - background: @blue; - color: white; +.umb-checkbox-list__item:last-child { + border-bottom: none; +} + +.umb-checkbox-list__item:hover { + background-color: @grayLighter; +} + +.umb-checkbox-list__item.-selected, +.umb-checkbox-list__item.-disabled { + background-color: fade(@blueDark, 4%); + font-weight: bold; } .umb-checkbox-list__item-checkbox { display: flex; justify-content: center; align-items: center; - background: @grayLighter; flex: 0 0 30px; height: 30px; margin-right: 10px; @@ -34,7 +41,22 @@ } .umb-checkbox-list__item-text { - font-size: 14px; + font-size: 13px; + margin-bottom: 0; + flex: 1 1 auto; +} + +.umb-checkbox-list__item-text.-faded { + opacity: 0.5; +} + +.umb-checkbox-list__item.-disabled .umb-checkbox-list__item-text { + cursor: not-allowed; +} + +.umb-checkbox-list__item-caption { + font-size: 11px; + margin-left: 2px; } .umb-checkbox-list__no-data { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js new file mode 100644 index 0000000000..9aba858e7d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js @@ -0,0 +1,27 @@ + (function() { + "use strict"; + + function CompositionsOverlay($scope) { + + var vm = this; + + vm.isDisabled = isDisabled; + vm.isSelected = isSelected; + + function isSelected(alias) { + if($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { + return true; + } + } + + function isDisabled(alias) { + if($scope.model.contentType.lockedCompositeContentTypes.indexOf(alias) !== -1) { + return true; + } + } + + } + + angular.module("umbraco").controller("Umbraco.Overlays.CompositionsOverlay", CompositionsOverlay); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 31f47702e1..41324ca62b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -1,39 +1,49 @@ -
    - +
    + +
    + +
    + +
    + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. +
    + + + This content type is used in a composition, and therefore cannot be composed itself. + + +
      +
    • + +
      + +
      + + + + +
    • +
    +
    - -
    - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. -
    - - - This content type is used in a composition, and therefore cannot be composed itself. - - -
      -
    • - -
      - -
      - -
      - - {{ compositeContentType.name }} -
      - -
    • -
    From ffa7a1457aeb0ce122ea83b58eb439e2e5c8afe1 Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 11 Jan 2016 13:46:20 +0100 Subject: [PATCH 17/42] Merge branch '7.4.0' into temp-U4-7634 Conflicts: src/Umbraco.Web/Editors/ContentTypeControllerBase.cs --- .../Services/ContentTypeServiceExtensions.cs | 18 +++-- .../Editors/ContentTypeControllerBase.cs | 77 +------------------ 2 files changed, 12 insertions(+), 83 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 6b440c6b2c..16dc068a10 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; namespace Umbraco.Core.Services @@ -15,8 +16,6 @@ namespace Umbraco.Core.Services IContentTypeComposition source, IContentTypeComposition[] allContentTypes) { - - if (source == null) throw new ArgumentNullException("source"); //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic // note: there are many sanity checks missing here and there ;-(( @@ -25,11 +24,14 @@ namespace Umbraco.Core.Services // throw new Exception("A parent does not belong to a composition."); // find out if any content type uses this content type - var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == source.Id)).ToArray(); - if (isUsing.Length > 0) + if (source != null) { - //if already in use a composition, do not allow any composited types - return new List(); + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == source.Id)).ToArray(); + if (isUsing.Length > 0) + { + //if already in use a composition, do not allow any composited types + return new List(); + } } // if it is not used then composition is possible @@ -59,9 +61,9 @@ namespace Umbraco.Core.Services // .Where(x => x != source.ParentId) // but not the parent // .Distinct() // .ToArray(); - + return list - .Where(x => x.Id != source.Id) + .Where(x => x.Id != (source != null ? source.Id : 0)) .OrderBy(x => x.Name) .ToList(); } diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 1f4523661a..8e6ba92994 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -92,56 +92,9 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - // note: there are many sanity checks missing here and there ;-(( - // make sure once and for all - //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) - // throw new Exception("A parent does not belong to a composition."); + var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes); - // find out if any content type uses this content type - var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == contentTypeId)).ToArray(); - if (isUsing.Length > 0) - { - //if already in use a composition, do not allow any composited types - return new List(); - } - - // if it is not used then composition is possible - // hashset guarantees unicity on Id - var list = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - // usable types are those that are top-level - var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); - foreach (var x in usableContentTypes) - list.Add(x); - - // indirect types are those that we use, directly or indirectly - var indirectContentTypes = GetIndirect(source).ToArray(); - foreach (var x in indirectContentTypes) - list.Add(x); - - if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes) - { - // get the ancestorIds via the parent - var ancestorIds = new int[0]; - if (parentId > 0) - { - var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId); - if (parent != null) - ancestorIds = parent.Path.Split(',').Select(int.Parse).ToArray(); - } - - // add all ancestors as compositions (since they are implicitly "compositions" by inheritance and should - // be in the list even though they can't be deselected) - foreach (var x in allContentTypes) - if (ancestorIds.Contains(x.Id)) - list.Add(x); - } - return list - .Where(x => x.Id != contentTypeId) - .OrderBy(x => x.Name) + return filtered .Select(Mapper.Map) .Select(x => { @@ -151,32 +104,6 @@ namespace Umbraco.Web.Editors .ToList(); } - private static IEnumerable GetIndirect(IContentTypeComposition ctype) - { - // hashset guarantees unicity on Id - var all = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - - var stack = new Stack(); - - if (ctype != null) - { - foreach (var x in ctype.ContentTypeComposition) - stack.Push(x); - } - - while (stack.Count > 0) - { - var x = stack.Pop(); - all.Add(x); - foreach (var y in x.ContentTypeComposition) - stack.Push(y); - } - - return all; - } - /// /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors /// From cb05f3fe03311f1576e0101962d0d4a3ea4f8ccc Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 11 Jan 2016 14:04:03 +0100 Subject: [PATCH 18/42] Reverted changes no longer needed after work on inherited/nested doctypes. Changed default to allow inherited doctypes. --- .../Configuration/UmbracoSettings/ContentElement.cs | 2 +- src/Umbraco.Core/Models/ContentType.cs | 9 --------- .../src/common/resources/contenttype.resource.js | 4 ++-- .../src/views/documenttypes/edit.controller.js | 2 +- src/Umbraco.Web.UI/config/umbracoSettings.config | 2 +- src/Umbraco.Web/Editors/ContentTypeController.cs | 4 ++-- src/Umbraco.Web/Editors/ContentTypeControllerBase.cs | 2 +- src/Umbraco.Web/Editors/MediaTypeController.cs | 4 ++-- src/Umbraco.Web/Editors/MemberTypeController.cs | 4 ++-- 9 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index b2e946a597..1b1404a897 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -288,7 +288,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings return new OptionalInnerTextConfigurationElement( (InnerTextConfigurationElement) this["EnableInheritedDocumentTypes"], //set the default - false); + true); } } diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs index 7219230bc2..355d724fbe 100644 --- a/src/Umbraco.Core/Models/ContentType.cs +++ b/src/Umbraco.Core/Models/ContentType.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using Umbraco.Core.Configuration; namespace Umbraco.Core.Models { @@ -35,10 +34,6 @@ namespace Umbraco.Core.Models [Obsolete("This method is obsolete, use ContentType(IContentType parent, string alias) instead.", false)] public ContentType(IContentType parent) : this(parent, null) { - if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes) - { - AddContentType(parent); - } } /// @@ -50,10 +45,6 @@ namespace Umbraco.Core.Models public ContentType(IContentType parent, string alias) : base(parent, alias) { - if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes) - { - AddContentType(parent); - } _allowedTemplates = new List(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index dda6bc38b3..4b1ed8f9c1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -7,13 +7,13 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { - getAvailableCompositeContentTypes: function (contentTypeId, parentId) { + getAvailableCompositeContentTypes: function (contentTypeId) { return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentTypeApiBaseUrl", "GetAvailableCompositeContentTypes", - [{ contentTypeId: contentTypeId }, { parentId: parentId }])), + [{ contentTypeId: contentTypeId }])), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 10ead115f2..0700e08c2e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -213,7 +213,7 @@ function init(contentType) { //get available composite types - contentTypeResource.getAvailableCompositeContentTypes(contentType.id, contentType.parentId).then(function (result) { + contentTypeResource.getAvailableCompositeContentTypes(contentType.id).then(function (result) { contentType.availableCompositeContentTypes = result; // convert icons for composite content types iconHelper.formatContentTypeIcons(contentType.availableCompositeContentTypes); diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.config b/src/Umbraco.Web.UI/config/umbracoSettings.config index f20579e698..77d31fda09 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.config @@ -104,7 +104,7 @@ Textstring - false + true diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index cd9caec397..72a605b6cc 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -93,9 +93,9 @@ namespace Umbraco.Web.Editors return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); } - public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId, int parentId) + public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, parentId, UmbracoObjectTypes.DocumentType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType); } [UmbracoTreeAuthorize( diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 8e6ba92994..7962a73480 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -51,7 +51,7 @@ namespace Umbraco.Web.Editors /// Returns the available composite content types for a given content type /// /// - protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, int parentId, UmbracoObjectTypes type) + protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type) { IContentTypeComposition source = null; diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 3f2bde1dc9..d54981ff45 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -83,9 +83,9 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId, int parentId) + public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, parentId, UmbracoObjectTypes.MediaType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType); } public ContentTypeCompositionDisplay GetEmpty(int parentId) diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 72312669b2..7716f6ea00 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -79,9 +79,9 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId, int parentId) + public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, parentId, UmbracoObjectTypes.MemberType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType); } public ContentTypeCompositionDisplay GetEmpty() From d06003b24197bae7b972a49dc48dd9202eed9fdd Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 11 Jan 2016 14:12:55 +0100 Subject: [PATCH 19/42] Removing unnecessary settings check. --- src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs | 2 -- .../compositions/compositions.controller.js | 1 - src/Umbraco.Web/Editors/ContentTypeController.cs | 7 +++---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 16dc068a10..e113c9428e 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Models; namespace Umbraco.Core.Services diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js index 9aba858e7d..4188b331e3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.controller.js @@ -19,7 +19,6 @@ return true; } } - } angular.module("umbraco").controller("Umbraco.Overlays.CompositionsOverlay", CompositionsOverlay); diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 72a605b6cc..ec85142131 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -186,12 +186,11 @@ namespace Umbraco.Web.Editors /// public ContentTypeDisplay GetEmpty(int parentId) { - IContentType ct = null; - if (UmbracoConfig.For.UmbracoSettings().Content.EnableInheritedDocumentTypes && - parentId != Constants.System.Root) + IContentType ct; + if (parentId != Constants.System.Root) { var parent = Services.ContentTypeService.GetContentType(parentId); - ct = parent != null ? new ContentType(parent, String.Empty) : new ContentType(parentId); + ct = parent != null ? new ContentType(parent, string.Empty) : new ContentType(parentId); } else ct = new ContentType(parentId); From a09d576300a6088b3f7a6ac6b57ee34a207d5b38 Mon Sep 17 00:00:00 2001 From: Claus Date: Mon, 11 Jan 2016 14:13:25 +0100 Subject: [PATCH 20/42] clean using. --- src/Umbraco.Web/Editors/ContentTypeController.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index ec85142131..4f50d7ab11 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; @@ -12,7 +11,6 @@ using Umbraco.Core.Services; using Umbraco.Core.PropertyEditors; using System.Net.Http; using Umbraco.Core; -using Umbraco.Core.Configuration; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; From 32377b29b8e6ffa11f13039f92abf164d685d375 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 11 Jan 2016 15:04:51 +0100 Subject: [PATCH 21/42] Have content type filtering working on the compositions list so a dev is not allowed to create an illegal composition - next we need to pass in the already existing property types to further ensure an illegal composition cannot be created. --- .../Services/ContentTypeServiceExtensions.cs | 69 ++++++++++++++++--- .../components/umbgroupsbuilder.directive.js | 35 +++++++++- .../common/resources/contenttype.resource.js | 17 ++++- .../common/resources/mediatype.resource.js | 17 ++++- .../common/resources/membertype.resource.js | 17 ++++- .../services/contenttypehelper.service.js | 32 ++++++++- .../compositions/compositions.html | 15 ++-- .../Editors/ContentTypeController.cs | 20 +++++- .../Editors/ContentTypeControllerBase.cs | 18 ++++- .../Editors/MediaTypeController.cs | 20 +++++- .../Editors/MemberTypeController.cs | 20 +++++- 11 files changed, 249 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index d16e7946b9..e43818ae14 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -10,11 +10,43 @@ namespace Umbraco.Core.Services /// /// Returns the available composite content types for a given content type /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// /// public static IEnumerable GetAvailableCompositeContentTypes(this IContentTypeService ctService, IContentTypeComposition source, - IContentTypeComposition[] allContentTypes) + IContentTypeComposition[] allContentTypes, + string[] filterContentTypes = null, + string[] filterPropertyTypes = null) { + + if (filterContentTypes == null) filterContentTypes = new string[] { }; + if (filterPropertyTypes == null) filterContentTypes = new string[] { }; + + //ensure the source alias is added + var filterContentTypesList = filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToList(); + if (source != null && filterContentTypesList.Contains(source.Alias) == false) + filterContentTypesList.Add(source.Alias); + + //create the full list of property types to use as the filter + var filteredPropertyTypes = allContentTypes + .Where(c => filterContentTypesList.Contains(c.Alias)) + .SelectMany(c => c.PropertyTypes) + .Select(c => c.Alias) + .Union(filterPropertyTypes); + + var sourceId = source != null ? source.Id : 0; + //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic // note: there are many sanity checks missing here and there ;-(( @@ -22,15 +54,12 @@ namespace Umbraco.Core.Services //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) // throw new Exception("A parent does not belong to a composition."); - if (source != null) + // find out if any content type uses this content type + var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); + if (isUsing.Length > 0) { - // find out if any content type uses this content type - var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == source.Id)).ToArray(); - if (isUsing.Length > 0) - { - //if already in use a composition, do not allow any composited types - return new List(); - } + //if already in use a composition, do not allow any composited types + return new List(); } // if it is not used then composition is possible @@ -62,7 +91,25 @@ namespace Umbraco.Core.Services // .ToArray(); return list - .Where(x => x.Id != (source != null ? source.Id : 0)) + //not itself + .Where(x => x.Id != sourceId) + .Where(x => + { + //need to filter any content types that are included in this list + if (filterContentTypesList.Any() == false) return true; + + return filterContentTypesList.Any(c => c.InvariantEquals(x.Alias)) == false; + }) + .Where(x => + { + //need to filter any content types that have matching property aliases that are included in this list + if (filterContentTypesList.Any() == false) return true; + + //ensure that we don't return if there's any overlapping property aliases from the filtered ones specified + return filteredPropertyTypes.Intersect( + x.PropertyTypes.Select(p => p.Alias), + StringComparer.InvariantCultureIgnoreCase).Any() == false; + }) .OrderBy(x => x.Name) .ToList(); } @@ -74,6 +121,8 @@ namespace Umbraco.Core.Services /// private static IEnumerable GetDirectOrIndirect(IContentTypeComposition ctype) { + if (ctype == null) return Enumerable.Empty(); + // hashset guarantees unicity on Id var all = new HashSet(new DelegateEqualityComparer( (x, y) => x.Id == y.Id, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index c8ea32110e..2657424818 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -1,7 +1,7 @@ (function() { 'use strict'; - function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q) { + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout) { function link(scope, el, attr, ctrl) { @@ -116,6 +116,36 @@ } + function filterAvailableCompositions(selectedContentType, selecting) { + + //selecting = true if the user has check the item, false if the user has unchecked the item + + var selectedContentTypeAliases = selecting ? + //the user has selected the item so add to the current list + _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : + //the user has unselected the item so remove from the current list + _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { + return i === selectedContentType.alias; + }); + + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + + return resourceLookup(scope.model.id, selectedContentTypeAliases).then(function (filteredAvailableCompositeTypes) { + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { + //reset first + current.disallow = false; + //see if this list item is found in the response (allowed) list + var found = _.find(filteredAvailableCompositeTypes, function (f) { + return current.alias === f.alias; + }); + //disallow if the item was not found in the response (allowed) list - + // BUT do not set to dissallowed if it is currently checked + current.disallow = (selectedContentTypeAliases.indexOf(current.alias) === -1) && (found ? false : true); + }); + }); + } + function updatePropertiesSortOrder() { angular.forEach(scope.model.groups, function(group){ @@ -187,6 +217,8 @@ // or the action has been confirmed } else { + //TODO: RE-validate compositions + // make sure that all tabs has an init property if (scope.model.groups.length !== 0) { angular.forEach(scope.model.groups, function(group) { @@ -227,6 +259,7 @@ mediaTypeResource.getById(compositeContentType.id).then(function(composition){ contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index a59bc83ccb..10e969226d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -16,13 +16,26 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to retrieve count'); }, - getAvailableCompositeContentTypes: function (contentTypeId) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + var query = ""; + _.each(filterContentTypes, function (item) { + query += "filterContentTypes=" + item + "&"; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += "filterContentTypes=&"; + } + query += "contentTypeId=" + contentTypeId; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "contentTypeApiBaseUrl", "GetAvailableCompositeContentTypes", - [{ contentTypeId: contentTypeId }])), + query)), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index 39315c6074..dff3a99667 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -16,13 +16,26 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to retrieve count'); }, - getAvailableCompositeContentTypes: function (contentTypeId) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + var query = ""; + _.each(filterContentTypes, function (item) { + query += "filterContentTypes=" + item + "&"; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += "filterContentTypes=&"; + } + query += "contentTypeId=" + contentTypeId; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "mediaTypeApiBaseUrl", "GetAvailableCompositeMediaTypes", - [{ contentTypeId: contentTypeId }])), + query)), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index d949844a6c..e8253e463d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -7,13 +7,26 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { - getAvailableCompositeContentTypes: function (contentTypeId) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes) { + if (!filterContentTypes) { + filterContentTypes = []; + } + var query = ""; + _.each(filterContentTypes, function (item) { + query += "filterContentTypes=" + item + "&"; + }); + // if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterContentTypes.length === 0) { + query += "filterContentTypes=&"; + } + query += "contentTypeId=" + contentTypeId; + return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "memberTypeApiBaseUrl", "GetAvailableCompositeMemberTypes", - [{ contentTypeId: contentTypeId }])), + query)), 'Failed to retrieve data for content type id ' + contentTypeId); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index f9ffcd4a23..3230f83331 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -43,8 +43,38 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter) { return newArray; }, + validateAddingComposition: function(contentType, compositeContentType) { + + //Validate that by adding this group that we are not adding duplicate property type aliases + + var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })); + var propAliasesExisting = _.flatten(_.map(contentType.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })); + var intersec = _.intersection(propertiesAdding, propAliasesExisting); + if (intersec.length > 0) { + //return the overlapping property aliases + return intersec; + } + + //no overlapping property aliases + return []; + }, + mergeCompositeContentType: function(contentType, compositeContentType) { + //Validate that there are no overlapping aliases + var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); + if (overlappingAliases.length > 0) { + throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); + } + angular.forEach(compositeContentType.groups, function(compositionGroup) { // order composition groups based on sort order @@ -134,7 +164,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter) { // push id to array of merged composite content types compositionGroup.parentTabContentTypes.push(compositeContentType.id); - + // push group before placeholder tab contentType.groups.unshift(compositionGroup); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 2cdcef5ee6..49dab6151a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -26,13 +26,18 @@
      -
    • - -
      - + +
      + + ng-change="model.selectCompositeContentType(compositeContentType)" + ng-disabled="compositeContentType.disallow" />
      diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index b70683dd93..f91844fe07 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -95,9 +95,25 @@ namespace Umbraco.Web.Editors return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); } - public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType, filterContentTypes, filterPropertyTypes); } [UmbracoTreeAuthorize( diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 975154e6e8..2f98372e77 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -49,8 +49,22 @@ namespace Umbraco.Web.Editors /// /// Returns the available composite content types for a given content type /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// /// - protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type) + protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, + UmbracoObjectTypes type, + string[] filterContentTypes, + string[] filterPropertyTypes) { IContentTypeComposition source = null; @@ -90,7 +104,7 @@ namespace Umbraco.Web.Editors throw new ArgumentOutOfRangeException("The entity type was not a content type"); } - var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes); + var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); return filtered .Select(Mapper.Map) diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index f00f10de1e..a433bc032b 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -87,9 +87,25 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType, filterContentTypes, filterPropertyTypes); } public ContentTypeCompositionDisplay GetEmpty(int parentId) diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 7716f6ea00..de87a0fdc9 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -79,9 +79,25 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId) + /// + /// Returns the avilable compositions for this content type + /// + /// + /// + /// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out + /// along with any content types that have matching property types that are included in the filtered content types + /// + /// + /// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out. + /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot + /// be looked up via the db, they need to be passed in. + /// + /// + public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId, + [FromUri]string[] filterContentTypes, + [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType); + return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes); } public ContentTypeCompositionDisplay GetEmpty() From f7b34c76f96fb7c28f444fe77c017297732a7730 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 11 Jan 2016 15:38:07 +0100 Subject: [PATCH 22/42] got the filtering all working - this ensures that no CTs are selectable if any current property type exists in one of them in the list and re-filters on each CT selection in the list to ensure no illegal comps can be created. --- .../Services/ContentTypeServiceExtensions.cs | 38 +++++++++---------- .../components/umbgroupsbuilder.directive.js | 15 ++++++-- .../common/resources/contenttype.resource.js | 13 ++++++- .../common/resources/mediatype.resource.js | 13 ++++++- .../common/resources/membertype.resource.js | 13 ++++++- .../services/contenttypehelper.service.js | 7 +++- .../compositions/compositions.html | 4 +- 7 files changed, 72 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index e43818ae14..d9aa41c02a 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -28,22 +28,22 @@ namespace Umbraco.Core.Services IContentTypeComposition[] allContentTypes, string[] filterContentTypes = null, string[] filterPropertyTypes = null) - { - - if (filterContentTypes == null) filterContentTypes = new string[] { }; - if (filterPropertyTypes == null) filterContentTypes = new string[] { }; - - //ensure the source alias is added - var filterContentTypesList = filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToList(); - if (source != null && filterContentTypesList.Contains(source.Alias) == false) - filterContentTypesList.Add(source.Alias); + { + filterContentTypes = filterContentTypes == null + ? new string[] { } + : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); //create the full list of property types to use as the filter - var filteredPropertyTypes = allContentTypes - .Where(c => filterContentTypesList.Contains(c.Alias)) - .SelectMany(c => c.PropertyTypes) - .Select(c => c.Alias) - .Union(filterPropertyTypes); + //this is the combination of all property type aliases found in the content types passed in for the filter + //as well as the specific property types passed in for the filter + filterPropertyTypes = filterPropertyTypes == null + ? new string[] {} + : allContentTypes + .Where(c => filterContentTypes.Contains(c.Alias)) + .SelectMany(c => c.PropertyTypes) + .Select(c => c.Alias) + .Union(filterPropertyTypes) + .ToArray(); var sourceId = source != null ? source.Id : 0; @@ -96,17 +96,13 @@ namespace Umbraco.Core.Services .Where(x => { //need to filter any content types that are included in this list - if (filterContentTypesList.Any() == false) return true; - - return filterContentTypesList.Any(c => c.InvariantEquals(x.Alias)) == false; + return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false; }) .Where(x => { - //need to filter any content types that have matching property aliases that are included in this list - if (filterContentTypesList.Any() == false) return true; - + //need to filter any content types that have matching property aliases that are included in this list //ensure that we don't return if there's any overlapping property aliases from the filtered ones specified - return filteredPropertyTypes.Intersect( + return filterPropertyTypes.Intersect( x.PropertyTypes.Select(p => p.Alias), StringComparer.InvariantCultureIgnoreCase).Any() == false; }) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 2657424818..43b1ced6b6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -125,13 +125,22 @@ _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : //the user has unselected the item so remove from the current list _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { - return i === selectedContentType.alias; - }); + return i === selectedContentType && selectedContentType.alias; + }); + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function (f) { + return f !== null && f !== undefined; + }); //use a different resource lookup depending on the content type type var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; - return resourceLookup(scope.model.id, selectedContentTypeAliases).then(function (filteredAvailableCompositeTypes) { + return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { //reset first current.disallow = false; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 10e969226d..c3e1368efe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -16,10 +16,14 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to retrieve count'); }, - getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { if (!filterContentTypes) { filterContentTypes = []; } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + var query = ""; _.each(filterContentTypes, function (item) { query += "filterContentTypes=" + item + "&"; @@ -28,6 +32,13 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { if (filterContentTypes.length === 0) { query += "filterContentTypes=&"; } + _.each(filterPropertyTypes, function (item) { + query += "filterPropertyTypes=" + item + "&"; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += "filterPropertyTypes=&"; + } query += "contentTypeId=" + contentTypeId; return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index dff3a99667..17a96821fd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -16,10 +16,14 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to retrieve count'); }, - getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { if (!filterContentTypes) { filterContentTypes = []; } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + var query = ""; _.each(filterContentTypes, function (item) { query += "filterContentTypes=" + item + "&"; @@ -28,6 +32,13 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { if (filterContentTypes.length === 0) { query += "filterContentTypes=&"; } + _.each(filterPropertyTypes, function (item) { + query += "filterPropertyTypes=" + item + "&"; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += "filterPropertyTypes=&"; + } query += "contentTypeId=" + contentTypeId; return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index e8253e463d..0649277c54 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -7,10 +7,14 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { - getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes) { + getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) { if (!filterContentTypes) { filterContentTypes = []; } + if (!filterPropertyTypes) { + filterPropertyTypes = []; + } + var query = ""; _.each(filterContentTypes, function (item) { query += "filterContentTypes=" + item + "&"; @@ -19,6 +23,13 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { if (filterContentTypes.length === 0) { query += "filterContentTypes=&"; } + _.each(filterPropertyTypes, function (item) { + query += "filterPropertyTypes=" + item + "&"; + }); + // if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error + if (filterPropertyTypes.length === 0) { + query += "filterPropertyTypes=&"; + } query += "contentTypeId=" + contentTypeId; return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index 3230f83331..abfa58bc72 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -52,11 +52,14 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter) { return p.alias; }); })); - var propAliasesExisting = _.flatten(_.map(contentType.groups, function(g) { + var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) { return _.map(g.properties, function(p) { return p.alias; }); - })); + })), function(f) { + return f !== null && f !== undefined; + }); + var intersec = _.intersection(propertiesAdding, propAliasesExisting); if (intersec.length > 0) { //return the overlapping property aliases diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 49dab6151a..34e3a3d2aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -27,7 +27,7 @@
      • @@ -46,4 +46,4 @@
    • -
    \ No newline at end of file +
From 7389d343e4d2c6cf2a1de794a54b21204b579681 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 11 Jan 2016 15:53:20 +0100 Subject: [PATCH 23/42] adds more unit tests --- .../Services/ContentTypeServiceExtensions.cs | 8 +- .../ContentTypeServiceExtensionsTests.cs | 113 ++++++++++++++++++ 2 files changed, 117 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index d9aa41c02a..7f7c74fb37 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -33,13 +33,13 @@ namespace Umbraco.Core.Services ? new string[] { } : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); + if (filterPropertyTypes == null) filterPropertyTypes = new string[] {}; + //create the full list of property types to use as the filter //this is the combination of all property type aliases found in the content types passed in for the filter //as well as the specific property types passed in for the filter - filterPropertyTypes = filterPropertyTypes == null - ? new string[] {} - : allContentTypes - .Where(c => filterContentTypes.Contains(c.Alias)) + filterPropertyTypes = allContentTypes + .Where(c => filterContentTypes.InvariantContains(c.Alias)) .SelectMany(c => c.PropertyTypes) .Select(c => c.Alias) .Union(filterPropertyTypes) diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs index fbdb45114e..b4054e1669 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs @@ -1,6 +1,9 @@ +using System; using System.Linq; using Moq; using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; @@ -10,6 +13,116 @@ namespace Umbraco.Tests.Services [TestFixture] public class ContentTypeServiceExtensionsTests : BaseUmbracoApplicationTest { + [Test] + public void GetAvailableCompositeContentTypes_No_Overlap_By_Content_Type_And_Property_Type_Alias() + { + Action addPropType = (alias, ct) => + { + var contentCollection = new PropertyTypeCollection + { + new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = alias, Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88} + }; + var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 }; + ct.PropertyGroups.Add(pg); + }; + + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null); + addPropType("title", ct2); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null); + addPropType("title", ct3); + var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null); + var ct5 = MockedContentTypes.CreateBasicContentType("ct5", "CT5", null); + addPropType("blah", ct5); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + ct4.Id = 4; + ct5.Id = 4; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3, ct4, ct5 }, + new[] { ct2.Alias }, + new[] { "blah" }); + + Assert.AreEqual(1, availableTypes.Count()); + Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); + } + + [Test] + public void GetAvailableCompositeContentTypes_No_Overlap_By_Property_Type_Alias() + { + Action addPropType = ct => + { + var contentCollection = new PropertyTypeCollection + { + new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88} + }; + var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 }; + ct.PropertyGroups.Add(pg); + }; + + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null); + addPropType(ct2); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null); + addPropType(ct3); + var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + ct4.Id = 4; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3, ct4 }, + new string[] { }, + new[] { "title" }); + + Assert.AreEqual(1, availableTypes.Count()); + Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); + } + + [Test] + public void GetAvailableCompositeContentTypes_No_Overlap_By_Content_Type() + { + Action addPropType = ct => + { + var contentCollection = new PropertyTypeCollection + { + new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88} + }; + var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 }; + ct.PropertyGroups.Add(pg); + }; + + var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null); + var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null); + addPropType(ct2); + var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null); + addPropType(ct3); + var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null); + ct1.Id = 1; + ct2.Id = 2; + ct3.Id = 3; + ct4.Id = 4; + + var service = new Mock(); + + var availableTypes = service.Object.GetAvailableCompositeContentTypes( + ct1, + new[] { ct1, ct2, ct3, ct4 }, + new [] {ct2.Alias}); + + Assert.AreEqual(1, availableTypes.Count()); + Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); + } + [Test] public void GetAvailableCompositeContentTypes_Not_Itself() { From ee8405ccd01577ab5302987f2c2e06471450f265 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 11 Jan 2016 17:29:41 +0100 Subject: [PATCH 24/42] fixes rebase/merge --- .../components/umbgroupsbuilder.directive.js | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 43b1ced6b6..c984f182a1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -225,9 +225,7 @@ // submit overlay if no compositions has been removed // or the action has been confirmed } else { - - //TODO: RE-validate compositions - + // make sure that all tabs has an init property if (scope.model.groups.length !== 0) { angular.forEach(scope.model.groups, function(group) { @@ -252,32 +250,43 @@ scope.compositionsDialogModel = null; }, - selectCompositeContentType: function(compositeContentType) { + selectCompositeContentType: function (selectedContentType) { - if (scope.model.compositeContentTypes.indexOf(compositeContentType.alias) === -1) { - //merge composition with content type + //first check if this is a new selection - we need to store this value here before any further digests/async + // because after that the scope.model.compositeContentTypes will be populated with the selected value. + var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; - if(scope.contentType === "documentType") { + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { - contentTypeResource.getById(compositeContentType.id).then(function(composition){ - contentTypeHelper.mergeCompositeContentType(scope.model, composition); - }); + if (newSelection) { + //merge composition with content type + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; - } else if(scope.contentType === "mediaType") { - - mediaTypeResource.getById(compositeContentType.id).then(function(composition){ - contentTypeHelper.mergeCompositeContentType(scope.model, composition); + resourceLookup(selectedContentType.id).then(function (composition) { + //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and + // double check here. + var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); + if (overlappingAliases.length > 0) { + //this will create an invalid composition, need to uncheck it + scope.compositionsDialogModel.compositeContentTypes.splice( + scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); + //dissallow this until something else is unchecked + selectedContentType.disallow = true; } - }); + else { + contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } + }); + } + else { + // split composition from content type + contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); + } - } - - - } else { - // split composition from content type - contentTypeHelper.splitCompositeContentType(scope.model, compositeContentType); - } + }); } }; From 67994c630f943e8d4306c22f6159b5103db7683d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 12 Jan 2016 13:46:18 +0100 Subject: [PATCH 25/42] fixes null checking and ensures that the current property type aliases are passed up when first launching the compositions dialog. --- .../Services/ContentTypeServiceExtensions.cs | 4 +++- .../components/umbgroupsbuilder.directive.js | 23 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 7f7c74fb37..5fbf2a2c74 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -33,7 +33,9 @@ namespace Umbraco.Core.Services ? new string[] { } : filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); - if (filterPropertyTypes == null) filterPropertyTypes = new string[] {}; + filterPropertyTypes = filterPropertyTypes == null + ? new string[] {} + : filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray(); //create the full list of property types to use as the filter //this is the combination of all property type aliases found in the content types passed in for the filter diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index c984f182a1..1a9367cf5a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -293,21 +293,30 @@ var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount; - $q.all([ + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { + return _.map(g.properties, function(p) { + return p.alias; + }); + })), function(f) { + return f !== null && f !== undefined; + }); + $q.all([ //get available composite types - availableContentTypeResource(scope.model.id).then(function (result) { + availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { scope.compositionsDialogModel.availableCompositeContentTypes = result; // convert icons for composite content types iconHelper.formatContentTypeIcons(scope.compositionsDialogModel.availableCompositeContentTypes); }), //get content type count - countContentTypeResource().then(function (result) { + countContentTypeResource().then(function(result) { scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); }) - ]).then(function () { - //resolves when both other promises are done, now show it - scope.compositionsDialogModel.show = true; - }); + ]).then(function() { + //resolves when both other promises are done, now show it + scope.compositionsDialogModel.show = true; + }); }; From 85178bbad8aad69b75823b27826c0aa894fb229a Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 12 Jan 2016 15:44:37 +0100 Subject: [PATCH 26/42] Changes GetAvailableCompositeContentTypes to return a list of composite candidates and then whether they are actually allowed or not based on the filters. Updates the controllers response to be a list of which content types are allowed or not, updates the view accordingly, changes when the filtering happens so that it happens after the content type has been updated with new properties. --- .../Services/ContentTypeServiceExtensions.cs | 38 ++++------ .../ContentTypeServiceExtensionsTests.cs | 21 +++-- .../components/umbgroupsbuilder.directive.js | 76 +++++++++++-------- .../compositions/compositions.html | 10 +-- .../Editors/ContentTypeController.cs | 10 ++- .../Editors/ContentTypeControllerBase.cs | 6 +- .../Editors/MediaTypeController.cs | 10 ++- .../Editors/MemberTypeController.cs | 10 ++- 8 files changed, 105 insertions(+), 76 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 5fbf2a2c74..97fa199e97 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Services /// be looked up via the db, they need to be passed in. /// /// - public static IEnumerable GetAvailableCompositeContentTypes(this IContentTypeService ctService, + public static IEnumerable> GetAvailableCompositeContentTypes(this IContentTypeService ctService, IContentTypeComposition source, IContentTypeComposition[] allContentTypes, string[] filterContentTypes = null, @@ -48,20 +48,13 @@ namespace Umbraco.Core.Services .ToArray(); var sourceId = source != null ? source.Id : 0; - - //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic - - // note: there are many sanity checks missing here and there ;-(( - // make sure once and for all - //if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false)) - // throw new Exception("A parent does not belong to a composition."); - + // find out if any content type uses this content type var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray(); if (isUsing.Length > 0) { //if already in use a composition, do not allow any composited types - return new List(); + return new List>(); } // if it is not used then composition is possible @@ -81,18 +74,10 @@ namespace Umbraco.Core.Services foreach (var x in indirectContentTypes) list.Add(x); - //// directContentTypes are those we use directly - //// they are already in indirectContentTypes, no need to add to the list - //var directContentTypes = source.ContentTypeComposition.ToArray(); - - //var enabled = usableContentTypes.Select(x => x.Id) // those we can use - // .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used - // .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used - // .Where(x => x != source.ParentId) // but not the parent - // .Distinct() - // .ToArray(); - - return list + //At this point we have a list of content types that 'could' be compositions + + //now we'll filter this list based on the filters requested + var filtered = list //not itself .Where(x => x.Id != sourceId) .Where(x => @@ -110,6 +95,15 @@ namespace Umbraco.Core.Services }) .OrderBy(x => x.Name) .ToList(); + + //now we can create our result based on what is still available + var result = list + .OrderBy(x => x.Name) + .Select(composition => filtered.Contains(composition) + ? new Tuple(composition, true) + : new Tuple(composition, false)).ToList(); + + return result; } /// diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs index b4054e1669..6057443fa0 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs @@ -46,7 +46,8 @@ namespace Umbraco.Tests.Services ct1, new[] { ct1, ct2, ct3, ct4, ct5 }, new[] { ct2.Alias }, - new[] { "blah" }); + new[] { "blah" }) + .Where(x => x.Item2).Select(x => x.Item1).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); @@ -82,7 +83,8 @@ namespace Umbraco.Tests.Services ct1, new[] { ct1, ct2, ct3, ct4 }, new string[] { }, - new[] { "title" }); + new[] { "title" }) + .Where(x => x.Item2).Select(x => x.Item1).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); @@ -117,7 +119,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, new[] { ct1, ct2, ct3, ct4 }, - new [] {ct2.Alias}); + new [] {ct2.Alias}) + .Where(x => x.Item2).Select(x => x.Item1).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); @@ -137,7 +140,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] {ct1, ct2, ct3}); + new[] {ct1, ct2, ct3}) + .Where(x => x.Item2).Select(x => x.Item1).ToArray(); Assert.AreEqual(2, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); @@ -202,7 +206,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }) + .Where(x => x.Item2).Select(x => x.Item1).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct3.Id, availableTypes.Single().Id); @@ -224,7 +229,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }) + .Where(x => x.Item2).Select(x => x.Item1).ToArray(); Assert.AreEqual(2, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); @@ -250,7 +256,8 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }) + .Where(x => x.Item2).Select(x => x.Item1).ToArray(); Assert.AreEqual(3, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 1a9367cf5a..7dadee5e09 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -125,7 +125,7 @@ _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : //the user has unselected the item so remove from the current list _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { - return i === selectedContentType && selectedContentType.alias; + return i === selectedContentType.alias; }); //get the currently assigned property type aliases - ensure we pass these to the server side filer @@ -143,14 +143,16 @@ return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { //reset first - current.disallow = false; + current.allowed = true; //see if this list item is found in the response (allowed) list var found = _.find(filteredAvailableCompositeTypes, function (f) { - return current.alias === f.alias; + return current.contentType.alias === f.contentType.alias; }); - //disallow if the item was not found in the response (allowed) list - - // BUT do not set to dissallowed if it is currently checked - current.disallow = (selectedContentTypeAliases.indexOf(current.alias) === -1) && (found ? false : true); + //allow if the item was found in the response (allowed) list - + // and ensure its set to allowed if it is currently checked + current.allowed = (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || + ((found !== null && found !== undefined) ? found.allowed : false); + }); }); } @@ -256,37 +258,42 @@ // because after that the scope.model.compositeContentTypes will be populated with the selected value. var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; - //based on the selection, we need to filter the available composite types list - filterAvailableCompositions(selectedContentType, newSelection).then(function () { + if (newSelection) { + //merge composition with content type - if (newSelection) { - //merge composition with content type + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; - //use a different resource lookup depending on the content type type - var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; + resourceLookup(selectedContentType.id).then(function (composition) { + //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and + // double check here. + var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); + if (overlappingAliases.length > 0) { + //this will create an invalid composition, need to uncheck it + scope.compositionsDialogModel.compositeContentTypes.splice( + scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); + //dissallow this until something else is unchecked + selectedContentType.allowed = false; + } + else { + contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } - resourceLookup(selectedContentType.id).then(function (composition) { - //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and - // double check here. - var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); - if (overlappingAliases.length > 0) { - //this will create an invalid composition, need to uncheck it - scope.compositionsDialogModel.compositeContentTypes.splice( - scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); - //dissallow this until something else is unchecked - selectedContentType.disallow = true; - } - else { - contentTypeHelper.mergeCompositeContentType(scope.model, composition); - } + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something }); - } - else { - // split composition from content type - contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); - } + }); + } + else { + // split composition from content type + contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); - }); + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + //TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); + } } }; @@ -306,8 +313,11 @@ //get available composite types availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) { scope.compositionsDialogModel.availableCompositeContentTypes = result; + var contentTypes = _.map(scope.compositionsDialogModel.availableCompositeContentTypes, function(c) { + return c.contentType; + }); // convert icons for composite content types - iconHelper.formatContentTypeIcons(scope.compositionsDialogModel.availableCompositeContentTypes); + iconHelper.formatContentTypeIcons(contentTypes); }), //get content type count countContentTypeResource().then(function(result) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 34e3a3d2aa..b8719f029f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -35,14 +35,14 @@ ng-class="{ '-selected': model.compositeContentTypes.indexOf(compositeContentType.alias)+1 }"> + checklist-value="compositeContentType.contentType.alias" + ng-change="model.selectCompositeContentType(compositeContentType.contentType)" + ng-disabled="compositeContentType.allowed===false" />
- - {{ compositeContentType.name }} + + {{ compositeContentType.contentType.name }}
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index f91844fe07..45fc01e55c 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -109,11 +109,17 @@ namespace Umbraco.Web.Editors /// be looked up via the db, they need to be passed in. /// /// - public IEnumerable GetAvailableCompositeContentTypes(int contentTypeId, + public HttpResponseMessage GetAvailableCompositeContentTypes(int contentTypeId, [FromUri]string[] filterContentTypes, [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType, filterContentTypes, filterPropertyTypes); + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } [UmbracoTreeAuthorize( diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 2f98372e77..17a3b01666 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -61,7 +61,7 @@ namespace Umbraco.Web.Editors /// /// /// - protected IEnumerable PerformGetAvailableCompositeContentTypes(int contentTypeId, + protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type, string[] filterContentTypes, string[] filterPropertyTypes) @@ -107,10 +107,10 @@ namespace Umbraco.Web.Editors var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); return filtered - .Select(Mapper.Map) + .Select(x => new Tuple(Mapper.Map(x.Item1), x.Item2)) .Select(x => { - x.Name = TranslateItem(x.Name); + x.Item1.Name = TranslateItem(x.Item1.Name); return x; }) .ToList(); diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index a433bc032b..8b88d99a33 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -101,11 +101,17 @@ namespace Umbraco.Web.Editors /// be looked up via the db, they need to be passed in. /// /// - public IEnumerable GetAvailableCompositeMediaTypes(int contentTypeId, + public HttpResponseMessage GetAvailableCompositeMediaTypes(int contentTypeId, [FromUri]string[] filterContentTypes, [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType, filterContentTypes, filterPropertyTypes); + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } public ContentTypeCompositionDisplay GetEmpty(int parentId) diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index de87a0fdc9..27941a38df 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -93,11 +93,17 @@ namespace Umbraco.Web.Editors /// be looked up via the db, they need to be passed in. /// /// - public IEnumerable GetAvailableCompositeMemberTypes(int contentTypeId, + public HttpResponseMessage GetAvailableCompositeMemberTypes(int contentTypeId, [FromUri]string[] filterContentTypes, [FromUri]string[] filterPropertyTypes) { - return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes); + var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes) + .Select(x => new + { + contentType = x.Item1, + allowed = x.Item2 + }); + return Request.CreateResponse(result); } public ContentTypeCompositionDisplay GetEmpty() From ff83d2e1f470636d2353ba872b331455e65f6ee3 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 12 Jan 2016 17:41:53 +0100 Subject: [PATCH 27/42] U4-7561 Backoffice Media Section should provide information on physical location of files --- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 1 + .../umbraco/config/lang/en_us.xml | 1 + .../Models/Mapping/MediaModelMapper.cs | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 7c89e7f746..a3136328a1 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -168,6 +168,7 @@ Click to upload Drop your files here... + Link to media Create a new member 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 2a86fa458b..4d5c853cdb 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -168,6 +168,7 @@ Click to upload Drop your files here... + Link to media Create a new member diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 9a71686c28..05701e7e6c 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -136,6 +136,29 @@ namespace Umbraco.Web.Models.Mapping } }; + if (media.Properties.FirstOrDefault(x => x.Alias == "umbracoFile") != null) + { + var helper = new UmbracoHelper(UmbracoContext.Current); + var mediaItem = helper.TypedMedia(media.Id); + if (mediaItem != null) + { + var crop = mediaItem.GetCropUrl("umbracoFile", string.Empty); + if (string.IsNullOrWhiteSpace(crop) == false) + { + var link = new ContentPropertyDisplay + { + Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("media/urls"), + // don't add the querystring, split on the "?" will also work if there is no "?" + Value = crop.Split('?')[0], + View = "urllist" + }; + + genericProperties.Add(link); + } + } + } + TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties); } From 79cc4cd2fa2e2c3dbee23df864c7752d131e17a4 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 12 Jan 2016 18:16:55 +0100 Subject: [PATCH 28/42] Change "umbracoFile" to the constant for this value --- src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 05701e7e6c..f1f2f5bbba 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -136,13 +136,13 @@ namespace Umbraco.Web.Models.Mapping } }; - if (media.Properties.FirstOrDefault(x => x.Alias == "umbracoFile") != null) + if (media.Properties.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File) != null) { var helper = new UmbracoHelper(UmbracoContext.Current); var mediaItem = helper.TypedMedia(media.Id); if (mediaItem != null) { - var crop = mediaItem.GetCropUrl("umbracoFile", string.Empty); + var crop = mediaItem.GetCropUrl(Constants.Conventions.Media.File, string.Empty); if (string.IsNullOrWhiteSpace(crop) == false) { var link = new ContentPropertyDisplay From 43227d45105cf99c19371bc89e3a9c1364ea1cbc Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 12 Jan 2016 19:46:05 +0100 Subject: [PATCH 29/42] Support for multiple properties spawning a url - adding more uploadfields to umbracoSettings.config (autoFillImageProperties) --- .../Models/Mapping/MediaModelMapper.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index f1f2f5bbba..2d2e878d4b 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -9,6 +9,7 @@ using System.Web.Routing; using AutoMapper; using umbraco; using Umbraco.Core; +using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; using Umbraco.Core.PropertyEditors; @@ -142,15 +143,23 @@ namespace Umbraco.Web.Models.Mapping var mediaItem = helper.TypedMedia(media.Id); if (mediaItem != null) { - var crop = mediaItem.GetCropUrl(Constants.Conventions.Media.File, string.Empty); - if (string.IsNullOrWhiteSpace(crop) == false) + var crops = new List(); + var autoFillProperties = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties; + foreach (var field in autoFillProperties) + { + var crop = mediaItem.GetCropUrl(field.Alias, string.Empty); + if (string.IsNullOrWhiteSpace(crop) == false) + crops.Add(crop.Split('?')[0]); + } + + if (crops.Any()) { var link = new ContentPropertyDisplay { Alias = string.Format("{0}urls", Constants.PropertyEditors.InternalGenericPropertiesPrefix), Label = localizedText.Localize("media/urls"), // don't add the querystring, split on the "?" will also work if there is no "?" - Value = crop.Split('?')[0], + Value = string.Join(",", crops), View = "urllist" }; From 8e024fccff36a0303bba9d36a111576d3a971cc0 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 12 Jan 2016 20:05:22 +0100 Subject: [PATCH 30/42] Don't hardcode the umbracoFile constant --- src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 2d2e878d4b..94961d9793 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -137,21 +137,21 @@ namespace Umbraco.Web.Models.Mapping } }; - if (media.Properties.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File) != null) + var helper = new UmbracoHelper(UmbracoContext.Current); + var mediaItem = helper.TypedMedia(media.Id); + if (mediaItem != null) { - var helper = new UmbracoHelper(UmbracoContext.Current); - var mediaItem = helper.TypedMedia(media.Id); - if (mediaItem != null) + var crops = new List(); + var autoFillProperties = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties.ToArray(); + if (autoFillProperties.Any()) { - var crops = new List(); - var autoFillProperties = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties; foreach (var field in autoFillProperties) { var crop = mediaItem.GetCropUrl(field.Alias, string.Empty); if (string.IsNullOrWhiteSpace(crop) == false) crops.Add(crop.Split('?')[0]); } - + if (crops.Any()) { var link = new ContentPropertyDisplay From d6a49f275f8a7442d2f3826e624976146ab3eb51 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 10:29:26 +0100 Subject: [PATCH 31/42] adds test that fails - need to fix it now --- .../Repositories/ContentRepositoryTest.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 146ec49a50..d3ff6307cf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -57,6 +57,58 @@ namespace Umbraco.Tests.Persistence.Repositories return repository; } + [Test] + public void Rebuild_Xml_Structures_With_Non_Latest_Version() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + ContentTypeRepository contentTypeRepository; + using (var repository = CreateRepository(unitOfWork, out contentTypeRepository)) + { + var contentType1 = MockedContentTypes.CreateSimpleContentType("Textpage1", "Textpage1"); + contentTypeRepository.AddOrUpdate(contentType1); + + var allCreated = new List(); + + //create 100 non published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + //create 100 published + for (var i = 0; i < 100; i++) + { + var c1 = MockedContent.CreateSimpleContent(contentType1); + c1.ChangePublishedState(PublishedState.Published); + repository.AddOrUpdate(c1); + allCreated.Add(c1); + } + unitOfWork.Commit(); + + //now create some versions of this content - this shouldn't affect the xml structures saved + for (int i = 0; i < allCreated.Count; i++) + { + allCreated[i].Name = "blah" + i; + //IMPORTANT testing note here: We need to changed the published state here so that + // it doesn't automatically think this is simply publishing again - this forces the latest + // version to be Saved and not published + allCreated[i].ChangePublishedState(PublishedState.Saved); + repository.AddOrUpdate(allCreated[i]); + } + unitOfWork.Commit(); + + //delete all xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); + Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(100, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + [Test] public void Rebuild_All_Xml_Structures() { From 5e461b0cec9a0b5094292b94fba7b4b3060a2e07 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 10:29:44 +0100 Subject: [PATCH 32/42] adds packaging test --- .../Services/Importing/PackageImportTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs index 215f24e375..81679c98f7 100644 --- a/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs +++ b/src/Umbraco.Tests/Services/Importing/PackageImportTests.cs @@ -74,6 +74,31 @@ namespace Umbraco.Tests.Services.Importing Assert.That(uBlogsyLanding.CompositionPropertyGroups.Count(), Is.EqualTo(5)); } + [Test] + public void PackagingService_Can_Import_Inherited_ContentTypes_And_Verify_PropertyTypes_UniqueIds() + { + // Arrange + string strXml = ImportResources.InheritedDocTypes_Package; + var xml = XElement.Parse(strXml); + var dataTypeElement = xml.Descendants("DataTypes").First(); + var templateElement = xml.Descendants("Templates").First(); + var docTypeElement = xml.Descendants("DocumentTypes").First(); + var packagingService = ServiceContext.PackagingService; + + // Act + var dataTypes = packagingService.ImportDataTypeDefinitions(dataTypeElement); + var templates = packagingService.ImportTemplates(templateElement); + var contentTypes = packagingService.ImportContentTypes(docTypeElement); + + // Assert + var mRBasePage = contentTypes.First(x => x.Alias == "MRBasePage"); + foreach (var propertyType in mRBasePage.PropertyTypes) + { + var propertyTypeDto = this.DatabaseContext.Database.First("WHERE id = @id", new { id = propertyType.Id }); + Assert.AreEqual(propertyTypeDto.UniqueId, propertyType.Key); + } + } + [Test] public void PackagingService_Can_Import_Inherited_ContentTypes_And_Verify_PropertyGroups_And_PropertyTypes() { From 3131096ffb236f47706741c7bd71d0a5b06f95ef Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 11:33:46 +0100 Subject: [PATCH 33/42] Ensures that itself is never returned in the list, not even disabled (since you can never select yourself), ensures that any comp already selected is enabled for selection in the UI. --- .../Services/ContentTypeServiceExtensions.cs | 10 +++++----- .../components/umbgroupsbuilder.directive.js | 2 +- src/Umbraco.Web/Editors/ContentTypeControllerBase.cs | 11 +++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 97fa199e97..0a5cff6d4b 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -77,9 +77,7 @@ namespace Umbraco.Core.Services //At this point we have a list of content types that 'could' be compositions //now we'll filter this list based on the filters requested - var filtered = list - //not itself - .Where(x => x.Id != sourceId) + var filtered = list .Where(x => { //need to filter any content types that are included in this list @@ -95,9 +93,11 @@ namespace Umbraco.Core.Services }) .OrderBy(x => x.Name) .ToList(); - - //now we can create our result based on what is still available + + //now we can create our result based on what is still available var result = list + //not itself + .Where(x => x.Id != sourceId) .OrderBy(x => x.Name) .Select(composition => filtered.Contains(composition) ? new Tuple(composition, true) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 7dadee5e09..df92ae1c90 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -317,7 +317,7 @@ return c.contentType; }); // convert icons for composite content types - iconHelper.formatContentTypeIcons(contentTypes); + iconHelper.formatContentTypeIcons(contentTypes); }), //get content type count countContentTypeResource().then(function(result) { diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 17a3b01666..a1082639e8 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -106,11 +106,22 @@ namespace Umbraco.Web.Editors var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes); + var currCompositions = source == null ? new string[] { } : source.ContentTypeComposition.Select(x => x.Alias).ToArray(); + return filtered .Select(x => new Tuple(Mapper.Map(x.Item1), x.Item2)) .Select(x => { + //translate the name x.Item1.Name = TranslateItem(x.Item1.Name); + + //we need to ensure that the item is enabled if it is already selected + if (currCompositions.Contains(x.Item1.Alias)) + { + //re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!) + x = new Tuple(x.Item1, true); + } + return x; }) .ToList(); From 074d89427149a5b96c98f2b90015888530c945ef Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 12:10:04 +0100 Subject: [PATCH 34/42] Fixes: U4-7706 Property Type UniqueIds do not stay consistent during package installation --- .../Persistence/Factories/PropertyGroupFactory.cs | 6 +----- .../Persistence/Repositories/ContentTypeBaseRepository.cs | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs index 443457b0d9..487932f4b0 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs @@ -129,11 +129,7 @@ namespace Umbraco.Core.Persistence.Factories Name = propertyType.Name, SortOrder = propertyType.SortOrder, ValidationRegExp = propertyType.ValidationRegExp, - UniqueId = propertyType.HasIdentity - ? propertyType.Key == Guid.Empty - ? Guid.NewGuid() - : propertyType.Key - : Guid.NewGuid() + UniqueId = propertyType.Key == Guid.Empty ? Guid.NewGuid() : propertyType.Key }; if (tabId != default(int)) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 631bb7e1fb..fbc3069e5c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -454,6 +454,7 @@ AND umbracoNode.id <> @id", propType.DataTypeDefinitionId = dto.DataTypeId; propType.Description = dto.Description; propType.Id = dto.Id; + propType.Key = dto.UniqueId; propType.Name = dto.Name; propType.Mandatory = dto.Mandatory; propType.SortOrder = dto.SortOrder; From 68ddea2776d8219ed7a233c2581edcf72b01bf38 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 12:33:57 +0100 Subject: [PATCH 35/42] refactors a bit of code from the PR, adds more translations to the other english file. --- .../Repositories/ContentRepository.cs | 31 ++++++------------- .../umbraco/config/lang/en_us.xml | 3 ++ 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 2eab12a6a0..85491b9f36 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -234,8 +234,15 @@ namespace Umbraco.Core.Persistence.Repositories var processed = 0; do { - var descendants = GetPagedResultsByQueryNoFilter(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - + //NOTE: This is an important call, we cannot simply make a call to: + // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); + // because that method is used to query 'latest' content items where in this case we don't necessarily + // want latest content items because a pulished content item might not actually be the latest. + // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 + var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, + new Tuple("cmsDocument", "nodeId"), + ProcessQuery, "Path", Direction.Ascending); + var xmlItems = (from descendant in descendants let xml = serializer(descendant) select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); @@ -787,26 +794,6 @@ namespace Umbraco.Core.Persistence.Repositories } - /// - /// Gets paged content results - /// - /// Query to excute - /// Page number - /// Page size - /// Total records query would return without paging - /// Field to order by - /// Direction to order by - /// An Enumerable list of objects - public IEnumerable GetPagedResultsByQueryNoFilter(IQuery query, long pageIndex, int pageSize, out long totalRecords, - string orderBy, Direction orderDirection) - { - - return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, - new Tuple("cmsDocument", "nodeId"), - ProcessQuery, orderBy, orderDirection); - - } - #endregion #region IRecycleBinRepository members 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 2a86fa458b..c7480ac22a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -696,6 +696,9 @@ To manage your website, simply open the Umbraco back office and start adding con %0% could not be published because the item is scheduled for release. ]]> + From 17902712ac7cf6c86ee26ca251117f4c2120a8e4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 14:44:17 +0100 Subject: [PATCH 36/42] fixes merge/build --- src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index fd8106707d..0a5cff6d4b 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core.Models; From 1f245ec553e16f1be664d0847264654bc2b1b8f7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 14:48:19 +0100 Subject: [PATCH 37/42] fixes alias validation when saving an existing contenttype --- src/Umbraco.Web/Editors/ContentTypeControllerBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 753e882772..24d945c1be 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -191,7 +191,7 @@ namespace Umbraco.Web.Editors //Validate that there's no other ct with the same name var exists = getContentTypeByAlias(contentTypeSave.Alias); - if (exists != null) + if (exists != null && exists.Id.ToInvariantString() != contentTypeSave.Id.ToString()) { ModelState.AddModelError("Alias", "A content type with this alias already exists"); } From 54669ee2bc6572893ee83aacc0ce6badc0966330 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 13 Jan 2016 16:25:35 +0100 Subject: [PATCH 38/42] Ensures you cannot move a child doc type, changes the result of GetAvailableCompositions to be a model instead of a tuple, the result also includes the current ancestor list which we can use to disable items in the list, makes these models/methods internal since they'll most likely be changing, --- .../ContentTypeAvailableCompositionsResult.cs | 17 ++++++ ...ContentTypeAvailableCompositionsResults.cs | 26 ++++++++++ .../Models/ContentTypeCompositionBase.cs | 12 ++--- .../Services/ContentTypeServiceExtensions.cs | 52 +++++++++++++------ src/Umbraco.Core/Umbraco.Core.csproj | 4 +- .../ContentTypeServiceExtensionsTests.cs | 18 +++---- .../compositions/compositions.html | 10 ++-- .../Editors/ContentTypeControllerBase.cs | 13 +++-- .../Trees/ContentTypeTreeController.cs | 16 +++++- 9 files changed, 126 insertions(+), 42 deletions(-) create mode 100644 src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs create mode 100644 src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs new file mode 100644 index 0000000000..b1d2b45dc5 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResult.cs @@ -0,0 +1,17 @@ +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResult + { + public ContentTypeAvailableCompositionsResult(IContentTypeComposition composition, bool allowed) + { + Composition = composition; + Allowed = allowed; + } + + public IContentTypeComposition Composition { get; private set; } + public bool Allowed { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs new file mode 100644 index 0000000000..653d7a10a9 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentTypeAvailableCompositionsResults.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + /// + /// Used when determining available compositions for a given content type + /// + internal class ContentTypeAvailableCompositionsResults + { + public ContentTypeAvailableCompositionsResults() + { + Ancestors = Enumerable.Empty(); + Results = Enumerable.Empty(); + } + + public ContentTypeAvailableCompositionsResults(IEnumerable ancestors, IEnumerable results) + { + Ancestors = ancestors; + Results = results; + } + + public IEnumerable Ancestors { get; private set; } + public IEnumerable Results { get; private set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index 5ac21885d7..a6f8a2ef37 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -23,8 +23,8 @@ namespace Umbraco.Core.Models protected ContentTypeCompositionBase(IContentTypeComposition parent) : this(parent, null) - { - } + { + } protected ContentTypeCompositionBase(IContentTypeComposition parent, string alias) : base(parent, alias) @@ -122,10 +122,10 @@ namespace Umbraco.Core.Models return false; RemovedContentTypeKeyTracker.Add(contentTypeComposition.Id); - + //If the ContentType we are removing has Compositions of its own these needs to be removed as well var compositionIdsToRemove = contentTypeComposition.CompositionIds().ToList(); - if(compositionIdsToRemove.Any()) + if (compositionIdsToRemove.Any()) RemovedContentTypeKeyTracker.AddRange(compositionIdsToRemove); OnPropertyChanged(ContentTypeCompositionSelector); @@ -218,8 +218,8 @@ namespace Umbraco.Core.Models return false; // get and ensure a group local to this content type - var group = PropertyGroups.Contains(propertyGroupName) - ? PropertyGroups[propertyGroupName] + var group = PropertyGroups.Contains(propertyGroupName) + ? PropertyGroups[propertyGroupName] : AddAndReturnPropertyGroup(propertyGroupName); if (group == null) return false; diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index 0a5cff6d4b..ed04edc6bf 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -23,7 +23,7 @@ namespace Umbraco.Core.Services /// be looked up via the db, they need to be passed in. /// /// - public static IEnumerable> GetAvailableCompositeContentTypes(this IContentTypeService ctService, + internal static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, IContentTypeComposition source, IContentTypeComposition[] allContentTypes, string[] filterContentTypes = null, @@ -54,7 +54,7 @@ namespace Umbraco.Core.Services if (isUsing.Length > 0) { //if already in use a composition, do not allow any composited types - return new List>(); + return new ContentTypeAvailableCompositionsResults(); } // if it is not used then composition is possible @@ -68,7 +68,7 @@ namespace Umbraco.Core.Services .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); foreach (var x in usableContentTypes) list.Add(x); - + // indirect types are those that we use, directly or indirectly var indirectContentTypes = GetDirectOrIndirect(source).ToArray(); foreach (var x in indirectContentTypes) @@ -93,21 +93,46 @@ namespace Umbraco.Core.Services }) .OrderBy(x => x.Name) .ToList(); - - //now we can create our result based on what is still available + + //get ancestor ids - we will filter all ancestors + var ancestors = GetAncestors(source, allContentTypes); + var ancestorIds = ancestors.Select(x => x.Id).ToArray(); + + //now we can create our result based on what is still available and the ancestors var result = list //not itself .Where(x => x.Id != sourceId) .OrderBy(x => x.Name) .Select(composition => filtered.Contains(composition) - ? new Tuple(composition, true) - : new Tuple(composition, false)).ToList(); + ? new ContentTypeAvailableCompositionsResult(composition, ancestorIds.Contains(composition.Id) == false) + : new ContentTypeAvailableCompositionsResult(composition, false)).ToList(); - return result; + return new ContentTypeAvailableCompositionsResults(ancestors, result); + } + + private static IContentTypeComposition[] GetAncestors(IContentTypeComposition ctype, IContentTypeComposition[] allContentTypes) + { + if (ctype == null) return new IContentTypeComposition[] {}; + var ancestors = new List(); + var parentId = ctype.ParentId; + while (parentId > 0) + { + var parent = allContentTypes.FirstOrDefault(x => x.Id == parentId); + if (parent != null) + { + ancestors.Add(parent); + parentId = parent.ParentId; + } + else + { + parentId = -1; + } + } + return ancestors.ToArray(); } /// - /// Get those that we use directly or indirectly + /// Get those that we use directly /// /// /// @@ -121,12 +146,9 @@ namespace Umbraco.Core.Services x => x.Id)); var stack = new Stack(); - - if (ctype != null) - { - foreach (var x in ctype.ContentTypeComposition) - stack.Push(x); - } + + foreach (var x in ctype.ContentTypeComposition) + stack.Push(x); while (stack.Count > 0) { diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 5b404470b7..00ca6863ed 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -181,7 +181,7 @@ - + @@ -370,6 +370,8 @@ + + diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs index 6057443fa0..ed29cb2108 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceExtensionsTests.cs @@ -47,7 +47,7 @@ namespace Umbraco.Tests.Services new[] { ct1, ct2, ct3, ct4, ct5 }, new[] { ct2.Alias }, new[] { "blah" }) - .Where(x => x.Item2).Select(x => x.Item1).ToArray(); + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); @@ -84,7 +84,7 @@ namespace Umbraco.Tests.Services new[] { ct1, ct2, ct3, ct4 }, new string[] { }, new[] { "title" }) - .Where(x => x.Item2).Select(x => x.Item1).ToArray(); + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); @@ -120,7 +120,7 @@ namespace Umbraco.Tests.Services ct1, new[] { ct1, ct2, ct3, ct4 }, new [] {ct2.Alias}) - .Where(x => x.Item2).Select(x => x.Item1).ToArray(); + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id); @@ -141,7 +141,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, new[] {ct1, ct2, ct3}) - .Where(x => x.Item2).Select(x => x.Item1).ToArray(); + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(2, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); @@ -163,7 +163,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }).Results; Assert.AreEqual(0, availableTypes.Count()); } @@ -185,7 +185,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, - new[] { ct1, ct2, ct3 }); + new[] { ct1, ct2, ct3 }).Results; Assert.AreEqual(0, availableTypes.Count()); } @@ -207,7 +207,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, new[] { ct1, ct2, ct3 }) - .Where(x => x.Item2).Select(x => x.Item1).ToArray(); + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(1, availableTypes.Count()); Assert.AreEqual(ct3.Id, availableTypes.Single().Id); @@ -230,7 +230,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, new[] { ct1, ct2, ct3 }) - .Where(x => x.Item2).Select(x => x.Item1).ToArray(); + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(2, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); @@ -257,7 +257,7 @@ namespace Umbraco.Tests.Services var availableTypes = service.Object.GetAvailableCompositeContentTypes( ct1, new[] { ct1, ct2, ct3 }) - .Where(x => x.Item2).Select(x => x.Item1).ToArray(); + .Results.Where(x => x.Allowed).Select(x => x.Composition).ToArray(); Assert.AreEqual(3, availableTypes.Count()); Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index d936bfa679..a113e8309c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -31,22 +31,22 @@
  • + ng-class="{'-disabled': vm.isDisabled(compositeContentType.contentType.alias), '-selected': vm.isSelected(compositeContentType.contentType.alias)}">
    + ng-class="{ '-selected': model.compositeContentTypes.indexOf(compositeContentType.contentType.alias)+1 }">
    -