From d6775aa79abee84d7ea3cafeb8ea622796b7c765 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 20 Sep 2018 17:22:39 +0200 Subject: [PATCH] Implement content apps code config and manifest --- .../Manifest/ContentAppDefinitionConverter.cs | 16 +++ .../Manifest/ManifestContentAppDefinition.cs | 127 ++++++++++++++++++ src/Umbraco.Core/Manifest/ManifestParser.cs | 9 +- src/Umbraco.Core/Manifest/PackageManifest.cs | 4 + .../Models/ContentEditing/ContentApp.cs | 61 +++++++++ .../ContentEditing/IContentAppDefinition.cs | 20 +++ src/Umbraco.Core/Umbraco.Core.csproj | 4 + .../Manifest/ManifestParserTests.cs | 38 ++++++ .../ContentAppDefinitionCollection.cs | 20 +++ .../ContentAppDefinitionCollectionBuilder.cs | 27 ++++ .../ContentEditorContentAppDefinition.cs | 42 ++++++ .../ContentInfoContentAppDefinition.cs | 39 ++++++ .../ListViewContentAppDefinition.cs | 103 ++++++++++++++ src/Umbraco.Web/Editors/ContentController.cs | 4 +- src/Umbraco.Web/Editors/MediaController.cs | 4 +- src/Umbraco.Web/Editors/MemberController.cs | 4 +- .../Models/ContentEditing/ContentApp.cs | 36 ----- .../ContentEditing/ContentItemDisplay.cs | 1 + .../Models/ContentEditing/MediaItemDisplay.cs | 1 + .../ContentEditing/MemberListDisplay.cs | 1 + .../Models/Mapping/ContentAppResolver.cs | 50 ++----- .../Mapping/ContentAppResolverExtensions.cs | 77 ----------- .../Models/Mapping/MediaAppResolver.cs | 49 ++----- .../Runtime/WebRuntimeComponent.cs | 8 ++ src/Umbraco.Web/Umbraco.Web.csproj | 7 +- 25 files changed, 552 insertions(+), 200 deletions(-) create mode 100644 src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs create mode 100644 src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs create mode 100644 src/Umbraco.Core/Models/ContentEditing/ContentApp.cs create mode 100644 src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs create mode 100644 src/Umbraco.Web/ContentApps/ContentAppDefinitionCollection.cs create mode 100644 src/Umbraco.Web/ContentApps/ContentAppDefinitionCollectionBuilder.cs create mode 100644 src/Umbraco.Web/ContentApps/ContentEditorContentAppDefinition.cs create mode 100644 src/Umbraco.Web/ContentApps/ContentInfoContentAppDefinition.cs create mode 100644 src/Umbraco.Web/ContentApps/ListViewContentAppDefinition.cs delete mode 100644 src/Umbraco.Web/Models/ContentEditing/ContentApp.cs delete mode 100644 src/Umbraco.Web/Models/Mapping/ContentAppResolverExtensions.cs diff --git a/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs b/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs new file mode 100644 index 0000000000..87f104d90e --- /dev/null +++ b/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs @@ -0,0 +1,16 @@ +using System; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.Serialization; + +namespace Umbraco.Core.Manifest +{ + /// + /// Implements a json read converter for . + /// + internal class ContentAppDefinitionConverter : JsonReadConverter + { + protected override IContentAppDefinition Create(Type objectType, string path, JObject jObject) + => new ManifestContentAppDefinition(); + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs new file mode 100644 index 0000000000..9536335013 --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs @@ -0,0 +1,127 @@ +using System; +using System.Runtime.Serialization; +using System.Text.RegularExpressions; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Core.Manifest +{ + /// + /// Represents a content app definition, parsed from a manifest. + /// + [DataContract(Name = "appdef", Namespace = "")] + public class ManifestContentAppDefinition : IContentAppDefinition + { + private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + private string _view; + private ContentApp _app; + + /// + /// Gets or sets the name of the content app. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// Gets or sets the unique alias of the content app. + /// + /// + /// Must be a valid javascript identifier, ie no spaces etc. + /// + [DataMember(Name = "alias")] + public string Alias { get; set; } + + /// + /// Gets or sets the icon of the content app. + /// + /// + /// Must be a valid helveticons class name (see http://hlvticons.ch/). + /// + [DataMember(Name = "icon")] + public string Icon { get; set; } + + /// + /// Gets or sets the view for rendering the content app. + /// + [DataMember(Name = "view")] + public string View + { + get => _view; + set => _view = IOHelper.ResolveVirtualUrl(value); + } + + /// + /// Gets or sets the list of 'show' conditions for the content app. + /// + [DataMember(Name = "show")] + public string[] Show { get; set; } = Array.Empty(); + + /// + public ContentApp GetContentAppFor(object o) + { + string entityType, contentType; + + switch (o) + { + case IContent content: + entityType = "content"; + contentType = content.ContentType.Alias; + break; + + case IMedia media: + entityType = "media"; + contentType = media.ContentType.Alias; + break; + + default: + return null; + } + + // if no 'show' is specified, then always display the content app + if (Show.Length > 0) + { + var ok = false; + + // else iterate over each entry + foreach (var show in Show) + { + var match = ShowRegex.Match(show); + if (!match.Success) + throw new FormatException($"Illegal 'show' entry \"{show}\" in manifest."); + + var e = match.Groups[2].Value; + var c = match.Groups[3].Value; + + // if the entry does not apply, skip it + // support wildcards for entity & content types + if ((e != "*" && !e.InvariantEquals(entityType)) || (c != "*" && !c.InvariantEquals(contentType))) + continue; + + // if the entry applies, + // if it's an exclude entry, exit, do not display the content app + if (match.Groups[1].Value == "-") + return null; + + // else break - ok to display + ok = true; + break; + } + + // when 'show' is specified, default is to *not* show the content app + if (!ok) + return null; + } + + // content app can be displayed + return _app ?? (_app = new ContentApp + { + Alias = Alias, + Name = Name, + Icon = Icon, + View = View + }); + } + } +} diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs index e2363e314f..125dee5c05 100644 --- a/src/Umbraco.Core/Manifest/ManifestParser.cs +++ b/src/Umbraco.Core/Manifest/ManifestParser.cs @@ -8,6 +8,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Manifest @@ -98,6 +99,7 @@ namespace Umbraco.Core.Manifest var propertyEditors = new List(); var parameterEditors = new List(); var gridEditors = new List(); + var contentApps = new List(); foreach (var manifest in manifests) { @@ -106,6 +108,7 @@ namespace Umbraco.Core.Manifest if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors); if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors); if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors); + if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps); } return new PackageManifest @@ -114,7 +117,8 @@ namespace Umbraco.Core.Manifest Stylesheets = stylesheets.ToArray(), PropertyEditors = propertyEditors.ToArray(), ParameterEditors = parameterEditors.ToArray(), - GridEditors = gridEditors.ToArray() + GridEditors = gridEditors.ToArray(), + ContentApps = contentApps.ToArray() }; } @@ -146,7 +150,8 @@ namespace Umbraco.Core.Manifest var manifest = JsonConvert.DeserializeObject(text, new DataEditorConverter(_logger), - new ValueValidatorConverter(_validators)); + new ValueValidatorConverter(_validators), + new ContentAppDefinitionConverter()); // scripts and stylesheets are raw string, must process here for (var i = 0; i < manifest.Scripts.Length; i++) diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs index a1702cc58b..32dae46a9a 100644 --- a/src/Umbraco.Core/Manifest/PackageManifest.cs +++ b/src/Umbraco.Core/Manifest/PackageManifest.cs @@ -1,5 +1,6 @@ using System; using Newtonsoft.Json; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.PropertyEditors; namespace Umbraco.Core.Manifest @@ -23,5 +24,8 @@ namespace Umbraco.Core.Manifest [JsonProperty("gridEditors")] public GridEditor[] GridEditors { get; set; } = Array.Empty(); + + [JsonProperty("contentApps")] + public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty(); } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs new file mode 100644 index 0000000000..17bfadbba5 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs @@ -0,0 +1,61 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models.ContentEditing +{ + /// + /// Represents a content app. + /// + /// + /// Content apps are editor extensions. + /// + [DataContract(Name = "app", Namespace = "")] + public class ContentApp + { + /// + /// Gets the name of the content app. + /// + [DataMember(Name = "name")] + public string Name { get; set; } + + /// + /// Gets the unique alias of the content app. + /// + /// + /// Must be a valid javascript identifier, ie no spaces etc. + /// + [DataMember(Name = "alias")] + public string Alias { get; set; } + + /// + /// Gets the icon of the content app. + /// + /// + /// Must be a valid helveticons class name (see http://hlvticons.ch/). + /// + [DataMember(Name = "icon")] + public string Icon { get; set; } + + /// + /// Gets the view for rendering the content app. + /// + [DataMember(Name = "view")] + public string View { get; set; } + + /// + /// The view model specific to this app + /// fixme how/where is this used? + /// + [DataMember(Name = "viewModel")] + public object ViewModel { get; set; } + + /// + /// Gets a value indicating whether the app is active. + /// + /// + /// Normally reserved for Angular to deal with but in some cases this can be set on the server side. + /// + [DataMember(Name = "active")] + public bool Active { get; set; } + } +} + diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs new file mode 100644 index 0000000000..5e0c421742 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core.Models.ContentEditing +{ + /// + /// Represents a content app definition. + /// + public interface IContentAppDefinition + { + /// + /// Gets the content app for an object. + /// + /// The source object. + /// The content app for the object, or null. + /// + /// The definition must determine, based on , whether + /// the content app should be displayed or not, and return either a + /// instance, or null. + /// + ContentApp GetContentAppFor(object source); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b33669a631..76ed089dec 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -334,6 +334,8 @@ + + @@ -365,6 +367,8 @@ + + diff --git a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs index 2bbd70e367..5145b848ed 100644 --- a/src/Umbraco.Tests/Manifest/ManifestParserTests.cs +++ b/src/Umbraco.Tests/Manifest/ManifestParserTests.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Manifest; using Umbraco.Core.PropertyEditors; using Umbraco.Core.PropertyEditors.Validators; using Umbraco.Core.Services; +using Umbraco.Web.ContentApps; namespace Umbraco.Tests.Manifest { @@ -345,5 +346,42 @@ javascript: ['~/test.js',/*** some note about stuff asd09823-4**09234*/ '~/test2 // fixme - should we resolveUrl in configs? } + + [Test] + public void CanParseManifest_ContentApps() + { + const string json = @"{'contentApps': [ + { + alias: 'myPackageApp1', + name: 'My App1', + icon: 'icon-foo', + view: '~/App_Plugins/MyPackage/ContentApps/MyApp1.html' + }, + { + alias: 'myPackageApp2', + name: 'My App2', + config: { key1: 'some config val' }, + icon: 'icon-bar', + view: '~/App_Plugins/MyPackage/ContentApps/MyApp2.html' + } +]}"; + + var manifest = _parser.ParseManifest(json); + Assert.AreEqual(2, manifest.ContentApps.Length); + + Assert.IsInstanceOf(manifest.ContentApps[0]); + var app0 = (ManifestContentAppDefinition) manifest.ContentApps[0]; + Assert.AreEqual("myPackageApp1", app0.Alias); + Assert.AreEqual("My App1", app0.Name); + Assert.AreEqual("icon-foo", app0.Icon); + Assert.AreEqual("/App_Plugins/MyPackage/ContentApps/MyApp1.html", app0.View); + + Assert.IsInstanceOf(manifest.ContentApps[1]); + var app1 = (ManifestContentAppDefinition)manifest.ContentApps[1]; + Assert.AreEqual("myPackageApp2", app1.Alias); + Assert.AreEqual("My App2", app1.Name); + Assert.AreEqual("icon-bar", app1.Icon); + Assert.AreEqual("/App_Plugins/MyPackage/ContentApps/MyApp2.html", app1.View); + } } } diff --git a/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollection.cs b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollection.cs new file mode 100644 index 0000000000..3c84d9cbdb --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollection.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + public class ContentAppDefinitionCollection : BuilderCollectionBase + { + public ContentAppDefinitionCollection(IEnumerable items) + : base(items) + { } + + public IEnumerable GetContentAppsFor(object o) + { + return this.Select(x => x.GetContentAppFor(o)).WhereNotNull(); + } + } +} diff --git a/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollectionBuilder.cs b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollectionBuilder.cs new file mode 100644 index 0000000000..ae4fdf4fe0 --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollectionBuilder.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using System.Linq; +using LightInject; +using Umbraco.Core.Composing; +using Umbraco.Core.Manifest; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + public class ContentAppDefinitionCollectionBuilder : OrderedCollectionBuilderBase + { + private readonly ManifestParser _manifestParser; + + public ContentAppDefinitionCollectionBuilder(IServiceContainer container, ManifestParser manifestParser) + : base(container) + { + _manifestParser = manifestParser; + } + + protected override ContentAppDefinitionCollectionBuilder This => this; + + protected override IEnumerable CreateItems(params object[] args) + { + return base.CreateItems(args).Concat(_manifestParser.Manifest.ContentApps); + } + } +} diff --git a/src/Umbraco.Web/ContentApps/ContentEditorContentAppDefinition.cs b/src/Umbraco.Web/ContentApps/ContentEditorContentAppDefinition.cs new file mode 100644 index 0000000000..ce9e869e69 --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentEditorContentAppDefinition.cs @@ -0,0 +1,42 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + internal class ContentEditorContentAppDefinition : IContentAppDefinition + { + private ContentApp _contentApp; + private ContentApp _mediaApp; + + public ContentApp GetContentAppFor(object o) + { + switch (o) + { + case IContent _: + return _contentApp ?? (_contentApp = new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = "icon-document", + View = "views/content/apps/content/content.html" + }); + + case IMedia media when !media.ContentType.IsContainer && media.ContentType.Alias != Core.Constants.Conventions.MediaTypes.Folder: + return _mediaApp ?? (_mediaApp = new ContentApp + { + Alias = "umbContent", + Name = "Content", + Icon = "icon-document", + View = "views/media/apps/content/content.html" + }); + + case IMedia _: + return null; + + default: + throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); + } + } + } +} diff --git a/src/Umbraco.Web/ContentApps/ContentInfoContentAppDefinition.cs b/src/Umbraco.Web/ContentApps/ContentInfoContentAppDefinition.cs new file mode 100644 index 0000000000..02e368d438 --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentInfoContentAppDefinition.cs @@ -0,0 +1,39 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + public class ContentInfoContentAppDefinition : IContentAppDefinition + { + private ContentApp _contentApp; + private ContentApp _mediaApp; + + public ContentApp GetContentAppFor(object o) + { + switch (o) + { + case IContent _: + return _contentApp ?? (_contentApp = new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/content/apps/info/info.html" + }); + + case IMedia _: + return _mediaApp ?? (_mediaApp = new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/media/apps/info/info.html" + }); + + default: + throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); + } + } + } +} diff --git a/src/Umbraco.Web/ContentApps/ListViewContentAppDefinition.cs b/src/Umbraco.Web/ContentApps/ListViewContentAppDefinition.cs new file mode 100644 index 0000000000..8694434342 --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ListViewContentAppDefinition.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + internal class ListViewContentAppDefinition : IContentAppDefinition + { + private readonly IDataTypeService _dataTypeService; + private readonly PropertyEditorCollection _propertyEditors; + + public ListViewContentAppDefinition(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors) + { + _dataTypeService = dataTypeService; + _propertyEditors = propertyEditors; + } + + public ContentApp GetContentAppFor(object o) + { + string contentTypeAlias, entityType; + + switch (o) + { + case IContent content when !content.ContentType.IsContainer: + return null; + case IContent content: + contentTypeAlias = content.ContentType.Alias; + entityType = "content"; + break; + case IMedia media when !media.ContentType.IsContainer && media.ContentType.Alias != Core.Constants.Conventions.MediaTypes.Folder: + return null; + case IMedia media: + contentTypeAlias = media.ContentType.Alias; + entityType = "media"; + break; + default: + throw new NotSupportedException($"Object type {o.GetType()} is not supported here."); + } + + return CreateContentApp(_dataTypeService, _propertyEditors, entityType, contentTypeAlias); + } + + public static ContentApp CreateContentApp(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors, string entityType, string contentTypeAlias) + { + var contentApp = new ContentApp + { + Alias = "umbListView", + Name = "Child items", + Icon = "icon-list", + View = "views/content/apps/listview/listview.html" + }; + + var customDtdName = Core.Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias; + var dtdId = Core.Constants.DataTypes.DefaultContentListView; + //first try to get the custom one if there is one + var dt = dataTypeService.GetDataType(customDtdName) + ?? dataTypeService.GetDataType(dtdId); + + if (dt == null) + { + throw new InvalidOperationException("No list view data type was found for this document type, ensure that the default list view data types exists and/or that your custom list view data type exists"); + } + + var editor = propertyEditors[dt.EditorAlias]; + if (editor == null) + { + throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); + } + + var listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditor(dt.Configuration); + //add the entity type to the config + listViewConfig["entityType"] = entityType; + + //Override Tab Label if tabName is provided + if (listViewConfig.ContainsKey("tabName")) + { + var configTabName = listViewConfig["tabName"]; + if (configTabName != null && String.IsNullOrWhiteSpace(configTabName.ToString()) == false) + contentApp.Name = configTabName.ToString(); + } + + //This is the view model used for the list view app + contentApp.ViewModel = new List + { + new ContentPropertyDisplay + { + Alias = $"{Core.Constants.PropertyEditors.InternalGenericPropertiesPrefix}containerView", + Label = "", + Value = null, + View = editor.GetValueEditor().View, + HideLabel = true, + Config = listViewConfig + } + }; + + return contentApp; + } + } +} diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index f1fde55030..fbff9bdaa1 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -24,6 +24,7 @@ using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Persistence.Querying; using Umbraco.Web.PublishedCache; using Umbraco.Core.Events; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Validation; using Umbraco.Web.Composing; using Umbraco.Web.Models; @@ -32,6 +33,7 @@ using Umbraco.Web._Legacy.Actions; using Constants = Umbraco.Core.Constants; using Language = Umbraco.Web.Models.ContentEditing.Language; using Umbraco.Core.PropertyEditors; +using Umbraco.Web.ContentApps; using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; @@ -227,7 +229,7 @@ namespace Umbraco.Web.Editors public ContentItemDisplay GetRecycleBin() { var apps = new List(); - apps.AppendListViewApp(Services.DataTypeService, _propertyEditors, "recycleBin", "content"); + apps.Add(ListViewContentAppDefinition.CreateContentApp(Services.DataTypeService, _propertyEditors, "recycleBin", "content")); apps[0].Active = true; var display = new ContentItemDisplay { diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index e44228a022..40f62c9cfd 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -31,9 +31,11 @@ using Umbraco.Web.UI; using Notification = Umbraco.Web.Models.ContentEditing.Notification; using Umbraco.Core.Persistence; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Models.Editors; using Umbraco.Core.Models.Validation; using Umbraco.Core.PropertyEditors; +using Umbraco.Web.ContentApps; using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; @@ -97,7 +99,7 @@ namespace Umbraco.Web.Editors public MediaItemDisplay GetRecycleBin() { var apps = new List(); - apps.AppendListViewApp(Services.DataTypeService, _propertyEditors, "recycleBin", "media"); + apps.Add(ListViewContentAppDefinition.CreateContentApp(Services.DataTypeService, _propertyEditors, "recycleBin", "media")); apps[0].Active = true; var display = new MediaItemDisplay { diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index aa03628632..9f70c3c33b 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -26,7 +26,9 @@ using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; using System.Collections.Generic; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.PropertyEditors; +using Umbraco.Web.ContentApps; using Umbraco.Web.Editors.Binders; using Umbraco.Web.Editors.Filters; @@ -137,7 +139,7 @@ namespace Umbraco.Web.Editors var name = foundType != null ? foundType.Name : listName; var apps = new List(); - apps.AppendListViewApp(Services.DataTypeService, _propertyEditors, listName, "member"); + apps.Add(ListViewContentAppDefinition.CreateContentApp(Services.DataTypeService, _propertyEditors, listName, "member")); apps[0].Active = true; var display = new MemberListDisplay diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Web/Models/ContentEditing/ContentApp.cs deleted file mode 100644 index f95d6ac6fd..0000000000 --- a/src/Umbraco.Web/Models/ContentEditing/ContentApp.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models.ContentEditing -{ - /// - /// Defines a "Content App" which are editor extensions - /// - [DataContract(Name = "app", Namespace = "")] - public class ContentApp - { - [DataMember(Name = "name")] - public string Name { get; set; } - - [DataMember(Name = "alias")] - public string Alias { get; set; } - - [DataMember(Name = "icon")] - public string Icon { get; set; } - - [DataMember(Name = "view")] - public string View { get; set; } - - /// - /// The view model specific to this app - /// - [DataMember(Name = "viewModel")] - public object ViewModel { get; set; } - - /// - /// Normally reserved for Angular to deal with but in some cases this can be set on the server side - /// - [DataMember(Name = "active")] - public bool Active { get; set; } - } -} - diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index 750fdf5925..a729d51d13 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; using Umbraco.Core.Serialization; using Umbraco.Web.Routing; diff --git a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs index d979ffbf4e..0118645b60 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MediaItemDisplay.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; namespace Umbraco.Web.Models.ContentEditing { diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberListDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberListDisplay.cs index ae9469989a..592bd14df5 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberListDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberListDisplay.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; namespace Umbraco.Web.Models.ContentEditing { diff --git a/src/Umbraco.Web/Models/Mapping/ContentAppResolver.cs b/src/Umbraco.Web/Models/Mapping/ContentAppResolver.cs index cebbe81500..a199c7e60e 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentAppResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentAppResolver.cs @@ -1,56 +1,26 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using AutoMapper; using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Web.ContentApps; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { - + // injected into ContentMapperProfile, + // maps ContentApps when mapping IContent to ContentItemDisplay internal class ContentAppResolver : IValueResolver> { - private readonly ContentApp _contentApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = "icon-document", - View = "views/content/apps/content/content.html" - }; + private readonly ContentAppDefinitionCollection _contentAppDefinitions; - private readonly ContentApp _infoApp = new ContentApp + public ContentAppResolver(ContentAppDefinitionCollection contentAppDefinitions) { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/content/apps/info/info.html" - }; - - private readonly IDataTypeService _dataTypeService; - private readonly PropertyEditorCollection _propertyEditorCollection; - - public ContentAppResolver(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditorCollection) - { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - _propertyEditorCollection = propertyEditorCollection ?? throw new ArgumentNullException(nameof(propertyEditorCollection)); + _contentAppDefinitions = contentAppDefinitions; } public IEnumerable Resolve(IContent source, ContentItemDisplay destination, IEnumerable destMember, ResolutionContext context) { - var apps = new List(); - - if (source.ContentType.IsContainer) - { - //If it's a container then add the list view app and view model - apps.AppendListViewApp(_dataTypeService, _propertyEditorCollection, source.ContentType.Alias, "content"); - } - - apps.Add(_contentApp); - apps.Add(_infoApp); - - return apps; + return _contentAppDefinitions.GetContentAppsFor(source); } -} - + } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentAppResolverExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentAppResolverExtensions.cs deleted file mode 100644 index ad3fe041c7..0000000000 --- a/src/Umbraco.Web/Models/Mapping/ContentAppResolverExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using AutoMapper; -using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Web.Models.ContentEditing; - -namespace Umbraco.Web.Models.Mapping -{ - internal static class ContentAppResolverExtensions - { - /// - /// Helper method to append a list view app to the content app collection - /// - /// - public static void AppendListViewApp( - this ICollection list, - IDataTypeService dataTypeService, PropertyEditorCollection propertyEditors, - string contentTypeAlias, string entityType) - { - var listViewApp = new ContentApp - { - Alias = "umbListView", - Name = "Child items", - Icon = "icon-list", - View = "views/content/apps/listview/listview.html" - }; - - var customDtdName = Core.Constants.Conventions.DataTypes.ListViewPrefix + contentTypeAlias; - var dtdId = Core.Constants.DataTypes.DefaultContentListView; - //first try to get the custom one if there is one - var dt = dataTypeService.GetDataType(customDtdName) - ?? dataTypeService.GetDataType(dtdId); - - if (dt == null) - { - throw new InvalidOperationException("No list view data type was found for this document type, ensure that the default list view data types exists and/or that your custom list view data type exists"); - } - - var editor = propertyEditors[dt.EditorAlias]; - if (editor == null) - { - throw new NullReferenceException("The property editor with alias " + dt.EditorAlias + " does not exist"); - } - - var listViewConfig = editor.GetConfigurationEditor().ToConfigurationEditor(dt.Configuration); - //add the entity type to the config - listViewConfig["entityType"] = entityType; - - //Override Tab Label if tabName is provided - if (listViewConfig.ContainsKey("tabName")) - { - var configTabName = listViewConfig["tabName"]; - if (configTabName != null && string.IsNullOrWhiteSpace(configTabName.ToString()) == false) - listViewApp.Name = configTabName.ToString(); - } - - //This is the view model used for the list view app - listViewApp.ViewModel = new List - { - new ContentPropertyDisplay - { - Alias = $"{Core.Constants.PropertyEditors.InternalGenericPropertiesPrefix}containerView", - Label = "", - Value = null, - View = editor.GetValueEditor().View, - HideLabel = true, - Config = listViewConfig - } - }; - - list.Add(listViewApp); - } - } - -} diff --git a/src/Umbraco.Web/Models/Mapping/MediaAppResolver.cs b/src/Umbraco.Web/Models/Mapping/MediaAppResolver.cs index 9cbc6bfeea..caaaacc5f2 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaAppResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaAppResolver.cs @@ -1,57 +1,26 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using AutoMapper; using Umbraco.Core.Models; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Web.ContentApps; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { + // injected into ContentMapperProfile, + // maps ContentApps when mapping IMedia to MediaItemDisplay internal class MediaAppResolver : IValueResolver> { - private static readonly ContentApp _contentApp = new ContentApp - { - Alias = "umbContent", - Name = "Content", - Icon = "icon-document", - View = "views/media/apps/content/content.html" - }; + private readonly ContentAppDefinitionCollection _contentAppDefinitions; - private static readonly ContentApp _infoApp = new ContentApp + public MediaAppResolver(ContentAppDefinitionCollection contentAppDefinitions) { - Alias = "umbInfo", - Name = "Info", - Icon = "icon-info", - View = "views/media/apps/info/info.html" - }; - - private readonly IDataTypeService _dataTypeService; - private readonly PropertyEditorCollection _propertyEditorCollection; - - public MediaAppResolver(IDataTypeService dataTypeService, PropertyEditorCollection propertyEditorCollection) - { - _dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService)); - _propertyEditorCollection = propertyEditorCollection ?? throw new ArgumentNullException(nameof(propertyEditorCollection)); + _contentAppDefinitions = contentAppDefinitions; } public IEnumerable Resolve(IMedia source, MediaItemDisplay destination, IEnumerable destMember, ResolutionContext context) { - var apps = new List(); - - if (source.ContentType.IsContainer || source.ContentType.Alias == Core.Constants.Conventions.MediaTypes.Folder) - { - apps.AppendListViewApp(_dataTypeService, _propertyEditorCollection, source.ContentType.Alias, "media"); - } - else - { - apps.Add(_contentApp); - } - - apps.Add(_infoApp); - - return apps; + return _contentAppDefinitions.GetContentAppsFor(source); } } - } diff --git a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs index 03ba763527..028f1bf728 100644 --- a/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs +++ b/src/Umbraco.Web/Runtime/WebRuntimeComponent.cs @@ -33,12 +33,14 @@ using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Web.Cache; using Umbraco.Web.Composing.CompositionRoots; +using Umbraco.Web.ContentApps; using Umbraco.Web.Dictionary; using Umbraco.Web.Editors; using Umbraco.Web.Features; using Umbraco.Web.HealthCheck; using Umbraco.Web.Install; using Umbraco.Web.Media; +using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.Mvc; using Umbraco.Web.PublishedCache; @@ -200,6 +202,12 @@ namespace Umbraco.Web.Runtime // register properties fallback composition.Container.RegisterSingleton(); + + // register known content apps + composition.Container.RegisterCollectionBuilder() + .Append() + .Append() + .Append(); } internal void Initialize( diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 45e5018ae0..6d1b7ae162 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -144,6 +144,11 @@ + + + + + @@ -261,7 +266,6 @@ - @@ -300,7 +304,6 @@ -