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 @@
-