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..6b8534a88f --- /dev/null +++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +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 +{ + // contentApps: [ + // { + // name: 'App Name', // required + // alias: 'appAlias', // required + // weight: 0, // optional, default is 0, use values between -99 and +99 + // icon: 'icon.app', // required + // view: 'path/view.htm', // required + // show: [ // optional, default is always show + // '-content/foo', // hide for content type 'foo' + // '+content/*', // show for all other content types + // '+media/*' // show for all media types + // ] + // }, + // ... + // ] + + /// + /// Represents a content app definition, parsed from a manifest. + /// + [DataContract(Name = "appdef", Namespace = "")] + public class ManifestContentAppDefinition : IContentAppDefinition + { + private string _view; + private ContentApp _app; + private ShowRule[] _showRules; + + /// + /// 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 weight of the content app. + /// + [DataMember(Name = "weight")] + public int Weight { 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 partA, partB; + + switch (o) + { + case IContent content: + partA = "content"; + partB = content.ContentType.Alias; + break; + + case IMedia media: + partA = "media"; + partB = media.ContentType.Alias; + break; + + default: + return null; + } + + var rules = _showRules ?? (_showRules = ShowRule.Parse(Show).ToArray()); + + // if no 'show' is specified, then always display the content app + if (rules.Length > 0) + { + var ok = false; + + // else iterate over each entry + foreach (var rule in rules) + { + // if the entry does not apply, skip it + if (!rule.Matches(partA, partB)) + continue; + + // if the entry applies, + // if it's an exclude entry, exit, do not display the content app + if (!rule.Show) + 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, + Weight = Weight + }); + } + + private class ShowRule + { + private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + + public bool Show { get; private set; } + public string PartA { get; private set; } + public string PartB { get; private set; } + + public bool Matches(string partA, string partB) + { + return (PartA == "*" || PartA.InvariantEquals(partA)) && (PartB == "*" || PartB.InvariantEquals(partB)); + } + + public static IEnumerable Parse(string[] rules) + { + foreach (var rule in rules) + { + var match = ShowRegex.Match(rule); + if (!match.Success) + throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest."); + + yield return new ShowRule + { + Show = match.Groups[1].Value != "-", + PartA = match.Groups[2].Value, + PartB = match.Groups[3].Value + }; + } + } + } + } +} 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..bf28c28c9e --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs @@ -0,0 +1,72 @@ +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 or sets the weight of the content app. + /// + /// + /// Content apps are ordered by weight, from left (lowest values) to right (highest values). + /// Some built-in apps have special weights: listview is -666, content is -100 and infos is +100. + /// The default weight is 0, meaning somewhere in-between content and infos, but weight could + /// be used for ordering between user-level apps, or anything really. + /// + [DataMember(Name = "weight")] + public int Weight { 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 + /// + [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.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index ee521a7119..c9bb14c527 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -37,6 +37,7 @@ using Umbraco.Web.Services; using Umbraco.Examine; using Umbraco.Tests.Testing.Objects.Accessors; using Umbraco.Web.Composing.CompositionRoots; +using Umbraco.Web.ContentApps; using Umbraco.Web._Legacy.Actions; using Current = Umbraco.Core.Composing.Current; using Umbraco.Web.Routing; @@ -206,6 +207,9 @@ namespace Umbraco.Tests.Testing Container.RegisterSingleton(); Container.RegisterSingleton(); + + // register empty content apps collection + Container.RegisterCollectionBuilder(); } protected virtual void ComposeCacheHelper() diff --git a/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollection.cs b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollection.cs new file mode 100644 index 0000000000..7dda00e62c --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollection.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.Logging; + +namespace Umbraco.Web.ContentApps +{ + public class ContentAppDefinitionCollection : BuilderCollectionBase + { + private readonly ILogger _logger; + + public ContentAppDefinitionCollection(IEnumerable items, ILogger logger) + : base(items) + { + _logger = logger; + } + + public IEnumerable GetContentAppsFor(object o) + { + var apps = this.Select(x => x.GetContentAppFor(o)).WhereNotNull().OrderBy(x => x.Weight).ToList(); + + var aliases = new HashSet(); + List dups = null; + + foreach (var app in apps) + { + if (aliases.Contains(app.Alias)) + (dups ?? (dups = new List())).Add(app.Alias); + else + aliases.Add(app.Alias); + } + + if (dups != null) + { + // dying is not user-friendly, so let's write to log instead, and wish people read logs... + + //throw new InvalidOperationException($"Duplicate content app aliases found: {string.Join(",", dups)}"); + _logger.Warn($"Duplicate content app aliases found: {string.Join(",", dups)}"); + } + + return apps; + } + } +} diff --git a/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollectionBuilder.cs b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollectionBuilder.cs new file mode 100644 index 0000000000..267dd2d0e7 --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentAppDefinitionCollectionBuilder.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using LightInject; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; +using Umbraco.Core.Manifest; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + public class ContentAppDefinitionCollectionBuilder : OrderedCollectionBuilderBase + { + public ContentAppDefinitionCollectionBuilder(IServiceContainer container) + : base(container) + { } + + protected override ContentAppDefinitionCollectionBuilder This => this; + + // need to inject dependencies in the collection, so override creation + public override ContentAppDefinitionCollection CreateCollection() + { + // get the logger just-in-time - see note below for manifest parser + var logger = Container.GetInstance(); + + return new ContentAppDefinitionCollection(CreateItems(), logger); + } + + protected override IEnumerable CreateItems(params object[] args) + { + // get the manifest parser just-in-time - injecting it in the ctor would mean that + // simply getting the builder in order to configure the collection, would require + // its dependencies too, and that can create cycles or other oddities + var manifestParser = Container.GetInstance(); + + 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..c2d6341e87 --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentEditorContentAppDefinition.cs @@ -0,0 +1,47 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + internal class ContentEditorContentAppDefinition : IContentAppDefinition + { + // see note on ContentApp + private const int Weight = -100; + + 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", + Weight = Weight + }); + + 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", + Weight = Weight + }); + + 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..be7a40f007 --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ContentInfoContentAppDefinition.cs @@ -0,0 +1,44 @@ +using System; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; + +namespace Umbraco.Web.ContentApps +{ + public class ContentInfoContentAppDefinition : IContentAppDefinition + { + // see note on ContentApp + private const int Weight = +100; + + 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", + Weight = Weight + }); + + case IMedia _: + return _mediaApp ?? (_mediaApp = new ContentApp + { + Alias = "umbInfo", + Name = "Info", + Icon = "icon-info", + View = "views/media/apps/info/info.html", + Weight = Weight + }); + + 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..5c73b2fa8c --- /dev/null +++ b/src/Umbraco.Web/ContentApps/ListViewContentAppDefinition.cs @@ -0,0 +1,107 @@ +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 + { + // see note on ContentApp + private const int Weight = -666; + + 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", + Weight = Weight + }; + + 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 @@ -