diff --git a/VERSION b/VERSION index 13175fdc..c9929e36 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.1 \ No newline at end of file +1.4.2 \ No newline at end of file diff --git a/src/Umbraco.Community.Contentment/Composing/CompositionExtensions.cs b/src/Umbraco.Community.Contentment/Composing/CompositionExtensions.cs index d69669d8..db5074ae 100644 --- a/src/Umbraco.Community.Contentment/Composing/CompositionExtensions.cs +++ b/src/Umbraco.Community.Contentment/Composing/CompositionExtensions.cs @@ -25,6 +25,7 @@ public static Composition UnlockContentment(this Composition composition) // Data List - Data Sources .Add() .Add() + .Add() .Add() .Add() .Add() @@ -32,6 +33,7 @@ public static Composition UnlockContentment(this Composition composition) .Add() .Add() .Add() + .Add() .Add() .Add() ; diff --git a/src/Umbraco.Community.Contentment/Constants.cs b/src/Umbraco.Community.Contentment/Constants.cs index 14405c5d..88724dac 100644 --- a/src/Umbraco.Community.Contentment/Constants.cs +++ b/src/Umbraco.Community.Contentment/Constants.cs @@ -81,6 +81,11 @@ internal static partial class PropertyGroups } } + internal static partial class Icons + { + public const string ContentTemplate = "icon-blueprint"; + } + internal static partial class Package { public const string Author = "Lee Kelleher"; diff --git a/src/Umbraco.Community.Contentment/DataEditors/CodeEditor/CodeEditorConfigurationEditor.cs b/src/Umbraco.Community.Contentment/DataEditors/CodeEditor/CodeEditorConfigurationEditor.cs index 3885be21..fc4c791c 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/CodeEditor/CodeEditorConfigurationEditor.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/CodeEditor/CodeEditorConfigurationEditor.cs @@ -17,6 +17,8 @@ internal sealed class CodeEditorConfigurationEditor : ConfigurationEditor internal const string Mode = "mode"; internal const string Theme = "theme"; internal const string UseWrapMode = "useWrapMode"; + internal const string MinLines = "minLines"; + internal const string MaxLines = "maxLines"; public CodeEditorConfigurationEditor() : base() @@ -131,11 +133,11 @@ public CodeEditorConfigurationEditor() //Fields.Add("enableLiveAutocompletion", "enableLiveAutocompletion", "[A friendly description]", "boolean");// enableLiveAutocompletion: 0, //Fields.Add("readonly", "readonly", "[A friendly description]", "boolean");// readonly: 0, - DefaultConfiguration.Add("minLines", 12); - Fields.Add("minLines", "Minimum lines", "Set the minimum number of lines that the editor will be. The default is 12 lines.", IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath)); + DefaultConfiguration.Add(MinLines, 12); + Fields.Add(MinLines, "Minimum lines", "Set the minimum number of lines that the editor will be. The default is 12 lines.", IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath)); - DefaultConfiguration.Add("maxLines", 30); - Fields.Add("maxLines", "Maximum lines", "Set the maximum number of lines that the editor can be. If left empty, the editor will not auto-scale.", IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath)); + DefaultConfiguration.Add(MaxLines, 30); + Fields.Add(MaxLines, "Maximum lines", "Set the maximum number of lines that the editor can be. If left empty, the editor will not auto-scale.", IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath)); } } } diff --git a/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.js b/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.js index 61160f8d..80058fcb 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.js +++ b/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.js @@ -52,6 +52,7 @@ angular.module("umbraco").controller("Umbraco.Community.Contentment.DataEditors. config.allowEdit = {}; config.nameTemplates = {}; config.descriptionTemplates = {}; + config.missingItem = {}; config.items.forEach(function (item) { config.itemLookup[item.key] = item; @@ -67,6 +68,12 @@ angular.module("umbraco").controller("Umbraco.Community.Contentment.DataEditors. } }); + localizationService.localizeMany(["contentment_missingItemName", "contentment_missingItemDescription"]).then(function (data) { + config.missingItem["name"] = data[0]; + config.missingItem["description"] = data[1]; + config.missingItem["icon"] = "icon-alert"; + }); + vm.allowAdd = (config.maxItems === 0 || config.maxItems === "0") || $scope.model.value.length < config.maxItems; vm.allowEdit = function (item, $index) { return config.allowEdit[item.key]; }; vm.allowRemove = Object.toBoolean(config.allowRemove); @@ -178,7 +185,12 @@ angular.module("umbraco").controller("Umbraco.Community.Contentment.DataEditors. function populate(item, $index, propertyName) { var label = ""; - if (propertyName === 'name' && config.nameTemplates.hasOwnProperty(item.key) === true) { + // check that the configuration editor exists, if not then return a default label. + if (config.itemLookup.hasOwnProperty(item.key) === false && config.missingItem) { + return config.missingItem[propertyName] || propertyName; + } + + if (propertyName === "name" && config.nameTemplates.hasOwnProperty(item.key) === true) { var expression = config.nameTemplates[item.key]; if (expression) { item.value.$index = $index + 1; @@ -187,7 +199,7 @@ angular.module("umbraco").controller("Umbraco.Community.Contentment.DataEditors. } } - if (propertyName === 'description' && config.descriptionTemplates.hasOwnProperty(item.key) === true) { + if (propertyName === "description" && config.descriptionTemplates.hasOwnProperty(item.key) === true) { var expression = config.descriptionTemplates[item.key]; if (expression) { item.value.$index = $index + 1; @@ -212,6 +224,7 @@ angular.module("umbraco").controller("Umbraco.Community.Contentment.DataEditors. $scope.model.value.splice($index, 1); + // TODO: [LK] Add the `maxItem` numeric code. if ((config.maxItems === 0 || config.maxItems === "0") || $scope.model.value.length < config.maxItems) { vm.allowAdd = true; } @@ -234,6 +247,7 @@ angular.module("umbraco").controller("Umbraco.Community.Contentment.DataEditors. } else { vm.allowAdd = true; } + emit(); }; function setDirty() { diff --git a/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.overlay.html b/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.overlay.html index 412cdb6d..42b61af2 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.overlay.html +++ b/src/Umbraco.Community.Contentment/DataEditors/ConfigurationEditor/configuration-editor.overlay.html @@ -30,7 +30,7 @@ no-dirty-check />
    -
  • +
  • +
  • + No items found for ''. +
diff --git a/src/Umbraco.Community.Contentment/DataEditors/ContentBlocks/content-blocks.overlay.html b/src/Umbraco.Community.Contentment/DataEditors/ContentBlocks/content-blocks.overlay.html index 0c2a1b88..3e7068c4 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/ContentBlocks/content-blocks.overlay.html +++ b/src/Umbraco.Community.Contentment/DataEditors/ContentBlocks/content-blocks.overlay.html @@ -41,7 +41,7 @@
    -
  • +
  • +
  • + No items found for ''. +
    @@ -92,7 +95,7 @@
      -
    • +
    • +
    • + No items found for ''. +
    diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/EnumDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/EnumDataListSource.cs index 614d2f3f..59361b15 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/EnumDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/EnumDataListSource.cs @@ -43,7 +43,7 @@ public EnumDataListSource(ILogger logger) Key = "enumType", Name = "Enumeration type", Description = "Select the enumeration from an assembly type.", - View = IOHelper.ResolveUrl(CascadingDropdownListDataEditor.DataEditorViewPath), + View = CascadingDropdownListDataEditor.DataEditorViewPath, Config = new Dictionary { { CascadingDropdownListDataEditor.APIs, new[] @@ -65,15 +65,16 @@ public EnumDataListSource(ILogger logger) public IEnumerable GetItems(Dictionary config) { - var items = new List(); - var type = GetValueType(config); if (type == null) { - return items; + return Enumerable.Empty(); } + var items = new List(); + var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static); + foreach (var field in fields) { var attr = field.GetCustomAttribute(false); @@ -83,6 +84,7 @@ public IEnumerable GetItems(Dictionary config) } var attr2 = field.GetCustomAttribute(false); + items.Add(new DataListItem { Description = attr?.Description ?? attr2?.Description, @@ -93,7 +95,7 @@ public IEnumerable GetItems(Dictionary config) }); } - if (config.TryGetValueAs("sortAlphabetically", out string boolean) == true && boolean == "1") + if (config.TryGetValueAs("sortAlphabetically", out bool boolean) == true && boolean == true) { return items.OrderBy(x => x.Name, StringComparer.InvariantCultureIgnoreCase); } diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/ExamineDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/ExamineDataListSource.cs new file mode 100644 index 00000000..3c713290 --- /dev/null +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/ExamineDataListSource.cs @@ -0,0 +1,149 @@ +/* Copyright © 2021 Lee Kelleher. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System.Collections.Generic; +using System.Linq; +using Examine; +using Examine.Search; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.PropertyEditors; +using UmbConstants = Umbraco.Core.Constants; + +namespace Umbraco.Community.Contentment.DataEditors +{ + [Core.Composing.HideFromTypeFinder] + public sealed class ExamineDataListSource : IDataListSource + { + private readonly IExamineManager _examineManager; + + private const string _defaultNameField = "nodeName"; + private const string _defaultValueField = "__Key"; + private const string _defaultIconField = "__Icon"; + + public ExamineDataListSource(IExamineManager examineManager) + { + _examineManager = examineManager; + } + + public string Name => "Examine Query"; + + public string Description => "Populate the data source from an Examine query."; + + public string Icon => "icon-search"; + + public OverlaySize OverlaySize => OverlaySize.Small; + + public IEnumerable Fields => new[] + { + new ConfigurationField + { + Key = "examineIndex", + Name = "Examine Index", + Description = "Select the Examine index.", + View = DropdownListDataListEditor.DataEditorViewPath, + Config = new Dictionary + { + { DropdownListDataListEditor.AllowEmpty, Constants.Values.False }, + { Constants.Conventions.ConfigurationFieldAliases.Items, _examineManager.Indexes.OrderBy(x => x.Name).Select(x => new DataListItem { Name = x.Name.SplitPascalCasing(), Value = x.Name }) }, + } + }, + new NotesConfigurationField(@"
    +Do you need help with Lucene query? +
    +

    If you need assistance with Lucene query syntax, please refer to this resource on our.umbraco.com.

    +
    +
    ", true), + new ConfigurationField + { + Key = "luceneQuery", + Name = "Lucene query", + Description = "Enter your raw Lucene expression to query Examine with.", + View = CodeEditorDataEditor.DataEditorViewPath, + Config = new Dictionary + { + { CodeEditorConfigurationEditor.Mode, "text" }, + { CodeEditorConfigurationEditor.MinLines, 1 }, + { CodeEditorConfigurationEditor.MaxLines, 5 }, + } + }, + new ConfigurationField + { + Key = "nameField", + Name = "Name Field", + Description = "Enter the field name to select the name from the Examine record.", + View = "textstring", + }, + new ConfigurationField + { + Key = "valueField", + Name = "Value Field", + Description = "Enter the field name to select the value (key) from the Examine record.", + View = "textstring", + }, + new ConfigurationField + { + Key = "iconField", + Name = "Icon Field", + Description = "(optional) Enter the field name to select the icon from the Examine record.", + View = "textstring", + }, + new ConfigurationField + { + Key = "descriptionField", + Name = "Description Field", + Description = "(optional) Enter the field name to select the description from the Examine record.", + View = "textstring", + }, + }; + + public Dictionary DefaultValues => new Dictionary + { + { "examineIndex", UmbConstants.UmbracoIndexes.ExternalIndexName }, + { "luceneQuery", "+__IndexType:content" }, + { "nameField", _defaultNameField }, + { "valueField", _defaultValueField }, + { "iconField", _defaultIconField }, + { "descriptionField", string.Empty }, + }; + + public IEnumerable GetItems(Dictionary config) + { + var examineIndex = config.GetValueAs("examineIndex", UmbConstants.UmbracoIndexes.ExternalIndexName); + if (_examineManager.TryGetIndex(examineIndex, out var index) == true) + { + var luceneQuery = config.GetValueAs("luceneQuery", string.Empty); + if (string.IsNullOrWhiteSpace(luceneQuery) == false) + { + var nameField = config.GetValueAs("nameField", _defaultNameField); + var valueField = config.GetValueAs("valueField", _defaultValueField); + var iconField = config.GetValueAs("iconField", _defaultIconField); + var descriptionField = config.GetValueAs("descriptionField", string.Empty); + + var results = index + .GetSearcher() + .CreateQuery() + .NativeQuery(luceneQuery) + // NOTE: For any `OrderBy` complaints, refer to: https://github.com/Shazwazza/Examine/issues/126 + .OrderBy(new SortableField(nameField, SortType.String)) + .Execute(); + + if (results?.TotalItemCount > 0) + { + return results.Select(x => new DataListItem + { + Name = x.Values.ContainsKey(nameField) == true ? x.Values[nameField] : x.Values[_defaultNameField], + Value = x.Values.ContainsKey(valueField) == true ? x.Values[valueField] : x.Values[_defaultValueField], + Icon = x.Values.ContainsKey(iconField) == true ? x.Values[iconField] : x.Values[_defaultIconField], + Description = x.Values.ContainsKey(descriptionField) == true ? x.Values[descriptionField] : null, + }); + } + } + } + + return Enumerable.Empty(); + } + } +} diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/JsonDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/JsonDataListSource.cs index 4d38a1ae..0ed1dd0f 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/JsonDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/JsonDataListSource.cs @@ -100,27 +100,25 @@ public JsonDataListSource(ILogger logger) public IEnumerable GetItems(Dictionary config) { - var items = new List(); - var url = config.GetValueAs("url", string.Empty); if (string.IsNullOrWhiteSpace(url) == true) { - return items; + return Enumerable.Empty(); } var json = GetJson(url); if (json == null) { - return items; + return Enumerable.Empty(); } var itemsJsonPath = config.GetValueAs("itemsJsonPath", string.Empty); if (string.IsNullOrWhiteSpace(itemsJsonPath) == true) { - return items; + return Enumerable.Empty(); } try @@ -130,7 +128,7 @@ public IEnumerable GetItems(Dictionary config) if (tokens.Any() == false) { _logger.Warn($"The JSONPath '{itemsJsonPath}' did not match any items in the JSON."); - return items; + return Enumerable.Empty(); } // TODO: [UP-FOR-GRABS] How would you get the string-value from a "key"? @@ -142,6 +140,8 @@ public IEnumerable GetItems(Dictionary config) var iconJsonPath = config.GetValueAs("iconJsonPath", string.Empty); var descriptionJsonPath = config.GetValueAs("descriptionJsonPath", string.Empty); + var items = new List(); + foreach (var token in tokens) { var name = token.SelectToken(nameJsonPath); @@ -176,13 +176,15 @@ public IEnumerable GetItems(Dictionary config) Description = description?.ToString() ?? string.Empty }); } + + return items; } catch (Exception ex) { _logger.Error(ex, "Error finding items in the JSON. Please check the syntax of your JSONPath expressions."); } - return items; + return Enumerable.Empty(); } private JToken GetJson(string url) diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/SqlDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/SqlDataListSource.cs index 7e800f4e..21bcb141 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/SqlDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/SqlDataListSource.cs @@ -44,7 +44,7 @@ public SqlDataListSource() public string Icon => "icon-server-alt"; - public OverlaySize OverlaySize => OverlaySize.Small; + public OverlaySize OverlaySize => OverlaySize.Large; public IEnumerable Fields => new ConfigurationField[] { @@ -68,10 +68,12 @@ public SqlDataListSource() Key = "query", Name = "SQL query", Description = "Enter your SQL query.", - View = IOHelper.ResolveUrl(CodeEditorDataEditor.DataEditorViewPath), + View = CodeEditorDataEditor.DataEditorViewPath, Config = new Dictionary { { CodeEditorConfigurationEditor.Mode, _codeEditorMode }, + { CodeEditorConfigurationEditor.MinLines, 20 }, + { CodeEditorConfigurationEditor.MaxLines, 40 }, } }, new ConfigurationField @@ -79,13 +81,13 @@ public SqlDataListSource() Key = "connectionString", Name = "Connection string", Description = "Select the connection string.", - View = IOHelper.ResolveUrl(DropdownListDataListEditor.DataEditorViewPath), + View = DropdownListDataListEditor.DataEditorViewPath, Config = new Dictionary { { DropdownListDataListEditor.AllowEmpty, Constants.Values.False }, { Constants.Conventions.ConfigurationFieldAliases.Items, _connectionStrings }, } - } + }, }; public Dictionary DefaultValues => new Dictionary diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/TextDelimitedDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/TextDelimitedDataListSource.cs index 17d6d40d..24f540e6 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/TextDelimitedDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/TextDelimitedDataListSource.cs @@ -66,28 +66,28 @@ public TextDelimitedDataListSource(ILogger logger) Key = "nameIndex", Name = "Name Index", Description = "Enter the index position of the name field from the delimited line.
    The default index position is 0.", - View = IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath) + View = NumberInputDataEditor.DataEditorViewPath }, new ConfigurationField { Key = "valueIndex", Name = "Value Index", Description = "Enter the index position of the value (key) field from the delimited line.
    The default index position is 1.", - View = IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath) + View = NumberInputDataEditor.DataEditorViewPath }, new ConfigurationField { Key = "iconIndex", Name = "Icon Index", Description = "(optional) Enter the index position of the icon field from the delimited line. To ignore this option, set the value to -1.", - View = IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath) + View = NumberInputDataEditor.DataEditorViewPath }, new ConfigurationField { Key = "descriptionIndex", Name = "Description Index", Description = "(optional) Enter the index position of the description field from the delimited line. To ignore this option, set the value to -1.", - View = IOHelper.ResolveUrl(NumberInputDataEditor.DataEditorViewPath) + View = NumberInputDataEditor.DataEditorViewPath } }; diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentDataListSource.cs index 9b05fcaf..ca427663 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentDataListSource.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Xml; using Umbraco.Web; +using UmbConstants = Umbraco.Core.Constants; namespace Umbraco.Community.Contentment.DataEditors { @@ -50,21 +51,23 @@ public UmbracoContentDataListSource(IContentTypeService contentTypeService, IUmb public IEnumerable GetItems(Dictionary config) { var preview = true; - - var startNode = default(IPublishedContent); - var parentNode = config.GetValueAs("parentNode", string.Empty); + var startNode = default(IPublishedContent); if (parentNode.InvariantStartsWith("umb://document/") == false) { + var nodeContextId = default(int?); var umbracoContext = _umbracoContextAccessor.UmbracoContext; // NOTE: First we check for "id" (if on a content page), then "parentId" (if editing an element). - var nodeContextId = int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("id"), out var currentId) == true - ? currentId - : int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("parentId"), out var parentId) == true - ? parentId - : default(int?); + if (int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("id"), out var currentId) == true) + { + nodeContextId = currentId; + } + else if (int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("parentId"), out var parentId) == true) + { + nodeContextId = parentId; + } if (nodeContextId == -20) { @@ -86,29 +89,29 @@ public IEnumerable GetItems(Dictionary config) startNode = _umbracoContextAccessor.UmbracoContext.Content.GetById(preview, udi.Guid); } - return startNode == null - ? Enumerable.Empty() - : startNode.Children.Select(x => new DataListItem + if (startNode != null) + { + return startNode.Children.Select(x => new DataListItem { // TODO: [LK:2020-12-03] If multi-lingual is enabled, should the `.Name` take the culture into account? Name = x.Name, - Value = Udi.Create(Core.Constants.UdiEntityType.Document, x.Key).ToString(), - Icon = ContentTypeCacheHelper.TryGetIcon(x.ContentType.Alias, out var icon, _contentTypeService) == true ? icon : Core.Constants.Icons.Content, + Value = Udi.Create(UmbConstants.UdiEntityType.Document, x.Key).ToString(), + Icon = ContentTypeCacheHelper.TryGetIcon(x.ContentType.Alias, out var icon, _contentTypeService) == true ? icon : UmbConstants.Icons.Content, Description = x.TemplateId > 0 ? x.Url : string.Empty, Disabled = x.IsPublished() == false, }); + } + + return Enumerable.Empty(); } public Type GetValueType(Dictionary config) => typeof(IPublishedContent); public object ConvertValue(Type type, string value) { - if (type == typeof(IPublishedContent) && Udi.TryParse(value, out var udi) == true) - { - return _umbracoContextAccessor.UmbracoContext.Content.GetById(udi); - } - - return value.TryConvertTo(type).Result; + return Udi.TryParse(value, out var udi) == true + ? _umbracoContextAccessor.UmbracoContext.Content.GetById(udi) + : default; } } } diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentPropertiesDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentPropertiesDataListSource.cs index f3a3f914..7c5423cc 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentPropertiesDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentPropertiesDataListSource.cs @@ -76,8 +76,7 @@ public IEnumerable Fields public IEnumerable GetItems(Dictionary config) { - if (config.TryGetValue("contentType", out var obj) == true && - obj is JArray array && + if (config.TryGetValueAs("contentType", out JArray array) == true && array.Count > 0 && array[0].Value() is string str && string.IsNullOrWhiteSpace(str) == false && diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentXPathDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentXPathDataListSource.cs index f8806c61..5c3c83a9 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentXPathDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoContentXPathDataListSource.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Services; using Umbraco.Core.Xml; using Umbraco.Web; +using UmbConstants = Umbraco.Core.Constants; namespace Umbraco.Community.Contentment.DataEditors { @@ -73,21 +74,23 @@ public UmbracoContentXPathDataListSource(IContentTypeService contentTypeService, public IEnumerable GetItems(Dictionary config) { - var items = new List(); - var xpath = config.GetValueAs("xpath", string.Empty); if (string.IsNullOrWhiteSpace(xpath) == false) { + var nodeContextId = default(int?); var preview = true; var umbracoContext = _umbracoContextAccessor.UmbracoContext; // NOTE: First we check for "id" (if on a content page), then "parentId" (if editing an element). - var nodeContextId = int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("id"), out var currentId) == true - ? currentId - : int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("parentId"), out var parentId) == true - ? parentId - : default(int?); + if (int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("id"), out var currentId) == true) + { + nodeContextId = currentId; + } + else if (int.TryParse(umbracoContext.HttpContext.Request.QueryString.Get("parentId"), out var parentId) == true) + { + nodeContextId = parentId; + } IEnumerable getPath(int id) => umbracoContext.Content.GetById(preview, id)?.Path.ToDelimitedList().Reverse(); bool publishedContentExists(int id) => umbracoContext.Content.GetById(preview, id) != null; @@ -96,30 +99,28 @@ public IEnumerable GetItems(Dictionary config) if (string.IsNullOrWhiteSpace(parsed) == false && parsed.StartsWith("$") == false) { - items.AddRange(umbracoContext.Content.GetByXPath(preview, parsed)); + return umbracoContext.Content.GetByXPath(preview, parsed) + .Select(x => new DataListItem + { + Name = x.Name, + Value = Udi.Create(UmbConstants.UdiEntityType.Document, x.Key).ToString(), + Icon = ContentTypeCacheHelper.TryGetIcon(x.ContentType.Alias, out var icon, _contentTypeService) == true ? icon : UmbConstants.Icons.Content, + Description = x.TemplateId > 0 ? x.Url : string.Empty, + Disabled = x.IsPublished() == false, + }); } } - return items.Select(x => new DataListItem - { - Name = x.Name, - Value = Udi.Create(Core.Constants.UdiEntityType.Document, x.Key).ToString(), - Icon = ContentTypeCacheHelper.TryGetIcon(x.ContentType.Alias, out var icon, _contentTypeService) == true ? icon : Core.Constants.Icons.Content, - Description = x.TemplateId > 0 ? x.Url : string.Empty, - Disabled = x.IsPublished() == false, - }); + return Enumerable.Empty(); } public Type GetValueType(Dictionary config) => typeof(IPublishedContent); public object ConvertValue(Type type, string value) { - if (type == typeof(IPublishedContent) && Udi.TryParse(value, out var udi)) - { - return _umbracoContextAccessor.UmbracoContext.Content.GetById(udi); - } - - return value.TryConvertTo(type).Result; + return Udi.TryParse(value, out var udi) == true + ? _umbracoContextAccessor.UmbracoContext.Content.GetById(udi) + : default; } } } diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoDictionaryDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoDictionaryDataListSource.cs index 595d3dd5..4eb3ee4f 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoDictionaryDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoDictionaryDataListSource.cs @@ -37,7 +37,7 @@ public UmbracoDictionaryDataListSource(ILocalizationService localizationService) Key = "item", Name = "Dictionary item", Description = "Select a parent dictionary item to display the child items.", - View = IOHelper.ResolveUrl(DictionaryPickerDataEditor.DataEditorViewPath), + View = DictionaryPickerDataEditor.DataEditorViewPath, Config = new Dictionary { { MaxItemsConfigurationField.MaxItems, 1 } diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoEntityDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoEntityDataListSource.cs index 9c7e73f8..a372e8bb 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoEntityDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoEntityDataListSource.cs @@ -22,6 +22,7 @@ public sealed class UmbracoEntityDataListSource : IDataListSource, IDataListSour { { nameof(UmbracoObjectTypes.DataType), UmbracoObjectTypes.DataType }, { nameof(UmbracoObjectTypes.Document), UmbracoObjectTypes.Document }, + { nameof(UmbracoObjectTypes.DocumentBlueprint), UmbracoObjectTypes.DocumentBlueprint }, { nameof(UmbracoObjectTypes.DocumentType), UmbracoObjectTypes.DocumentType }, { nameof(UmbracoObjectTypes.Media), UmbracoObjectTypes.Media }, { nameof(UmbracoObjectTypes.MediaType), UmbracoObjectTypes.MediaType }, @@ -33,6 +34,7 @@ public sealed class UmbracoEntityDataListSource : IDataListSource, IDataListSour { { nameof(UmbracoObjectTypes.DataType), UmbConstants.Icons.DataType }, { nameof(UmbracoObjectTypes.Document), UmbConstants.Icons.Content }, + { nameof(UmbracoObjectTypes.DocumentBlueprint), Constants.Icons.ContentTemplate }, { nameof(UmbracoObjectTypes.DocumentType), UmbConstants.Icons.ContentType }, { nameof(UmbracoObjectTypes.Media), UmbConstants.Icons.MediaImage }, { nameof(UmbracoObjectTypes.MediaType), UmbConstants.Icons.MediaType }, @@ -58,7 +60,7 @@ public UmbracoEntityDataListSource(IEntityService entityService) new NotesConfigurationField(@"
    A note about supported Umbraco entity types.
    -

    Umbraco's `EntityService` API has limited support for querying entity types by GUID or UDI.

    +

    Umbraco's EntityService API (currently) has limited support for querying entity types by GUID or UDI.

    Supported entity types are available in the list below.

    ", true), @@ -82,13 +84,9 @@ public UmbracoEntityDataListSource(IEntityService entityService) public IEnumerable GetItems(Dictionary config) { - var entityType = config.TryGetValue("entityType", out var obj) == true && obj is string value - ? value - : string.Empty; - - if (SupportedEntityTypes.TryGetValue(entityType, out var objectType) == true) + if (config.TryGetValueAs("entityType", out string entityType) == true && SupportedEntityTypes.TryGetValue(entityType, out var objectType) == true) { - var icon = EntityTypeIcons.ContainsKey(entityType) == true ? EntityTypeIcons[entityType] : UmbConstants.Icons.DefaultIcon; + var icon = EntityTypeIcons.GetValueAs(entityType, UmbConstants.Icons.DefaultIcon); return _entityService .GetAll(objectType) diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoImageCropDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoImageCropDataListSource.cs index ff2c6e13..d61c46a0 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoImageCropDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoImageCropDataListSource.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json.Linq; using Umbraco.Core; -using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -43,8 +41,7 @@ public IEnumerable Fields Description = x.EditorAlias, Name = x.Name, Value = Udi.Create(UmbConstants.UdiEntityType.DataType, x.Key).ToString(), - }) - .ToList(); + }); return new ConfigurationField[] { @@ -53,30 +50,27 @@ public IEnumerable Fields Key = "imageCropper", Name = "Image Cropper", Description = "Select a Data Type that uses the Image Cropper.", - View = ItemPickerDataListEditor.DataEditorViewPath, + View = RadioButtonListDataListEditor.DataEditorViewPath, Config = new Dictionary { - { "enableFilter", items.Count > 5 ? Constants.Values.True : Constants.Values.False }, - { "items", items }, - { "listType", "list" }, - { "overlayView", IOHelper.ResolveUrl(ItemPickerDataListEditor.DataEditorOverlayViewPath) }, - { "maxItems", 1 }, + { Constants.Conventions.ConfigurationFieldAliases.Items, items }, + { ShowDescriptionsConfigurationField.ShowDescriptions, Constants.Values.True }, + { ShowIconsConfigurationField.ShowIcons, Constants.Values.True }, + { Constants.Conventions.ConfigurationFieldAliases.DefaultValue, items.FirstOrDefault()?.Value } } } }; } } - public Dictionary DefaultValues => null; + public Dictionary DefaultValues => default; public OverlaySize OverlaySize => OverlaySize.Small; public IEnumerable GetItems(Dictionary config) { if (config.TryGetValue("imageCropper", out var obj) == true && - obj is JArray array && - array.Count > 0 && - array[0].Value() is string str && + obj is string str && string.IsNullOrWhiteSpace(str) == false && GuidUdi.TryParse(str, out var udi) == true) { diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoMembersDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoMembersDataListSource.cs new file mode 100644 index 00000000..6ae0cfc9 --- /dev/null +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UmbracoMembersDataListSource.cs @@ -0,0 +1,136 @@ +/* Copyright © 2021 Lee Kelleher. + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.PublishedCache; +using UmbConstants = Umbraco.Core.Constants; + +namespace Umbraco.Community.Contentment.DataEditors +{ + [Core.Composing.HideFromTypeFinder] + public sealed class UmbracoMembersDataListSource : IDataListSource, IDataListSourceValueConverter + { + private readonly IMemberTypeService _memberTypeService; + private readonly IMemberService _memberService; + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public UmbracoMembersDataListSource(IMemberTypeService memberTypeService, IMemberService memberService, IPublishedSnapshotAccessor publishedSnapshotAccessor) + { + _memberTypeService = memberTypeService; + _memberService = memberService; + _publishedSnapshotAccessor = publishedSnapshotAccessor; + } + + public string Name => "Umbraco Members"; + + public string Description => "Populate a data source with Umbraco members."; + + public string Icon => UmbConstants.Icons.Member; + + public IEnumerable Fields + { + get + { + var items = _memberTypeService + .GetAll() + .Select(x => new DataListItem + { + Icon = x.Icon, + Description = x.Description, + Name = x.Name, + Value = Udi.Create(UmbConstants.UdiEntityType.MemberType, x.Key).ToString(), + }) + .ToList(); + + return new[] + { + new NotesConfigurationField($@"
    +Important note about Umbraco Members. +
    +

    This data source is ideal for smaller number of members, e.g. under 50. Upwards of that, you will notice an unpleasant editor experience and rapidly diminished performance.

    +

    Remember...

    +
    +

    “With great power comes great responsibility!”

    +
    +

    —Benjamin Franklin Parker

    +
    +
    ", true), + new ConfigurationField + { + Key = "memberType", + Name = "Member Type", + Description = "Select a member type to filter the members by. If left empty, all members will be used.", + View = ItemPickerDataListEditor.DataEditorViewPath, + Config = new Dictionary + { + { "addButtonLabelKey", "defaultdialogs_selectMemberType" }, + { "enableFilter", items.Count > 5 ? Constants.Values.True : Constants.Values.False }, + { "items", items }, + { "listType", "list" }, + { "overlayView", IOHelper.ResolveUrl(ItemPickerDataListEditor.DataEditorOverlayViewPath) }, + { "maxItems", 1 }, + } + } + }; + } + } + + public Dictionary DefaultValues => default; + + public OverlaySize OverlaySize => OverlaySize.Small; + + public IEnumerable GetItems(Dictionary config) + { + DataListItem mapMember(IMember member) + { + var guidUdi = Udi.Create(UmbConstants.UdiEntityType.Member, member.Key).ToString(); + return new DataListItem + { + Name = member.Name, + Value = guidUdi, + Icon = member.ContentType.Icon ?? UmbConstants.Icons.Member, + Description = guidUdi, + }; + }; + + if (config.TryGetValueAs("memberType", out JArray array) == true && + array.Count > 0 && + array[0].Value() is string str && + string.IsNullOrWhiteSpace(str) == false && + GuidUdi.TryParse(str, out var udi) == true) + { + var memberType = _memberTypeService.Get(udi.Guid); + if (memberType != null) + { + return _memberService.GetMembersByMemberType(memberType.Id).Select(mapMember); + } + } + else + { + return _memberService.GetAllMembers().Select(mapMember); + } + + return Enumerable.Empty(); + } + + public Type GetValueType(Dictionary config) => typeof(IPublishedContent); + + public object ConvertValue(Type type, string value) + { + return GuidUdi.TryParse(value, out var udi) == true && udi.Guid.Equals(Guid.Empty) == false + ? _publishedSnapshotAccessor.PublishedSnapshot.Members.GetByProviderKey(udi.Guid) + : default; + } + } +} diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UserDefinedDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UserDefinedDataListSource.cs index 9d384d9c..8e6e5c78 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UserDefinedDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/UserDefinedDataListSource.cs @@ -7,7 +7,6 @@ using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; -using Umbraco.Core.IO; using Umbraco.Core.PropertyEditors; namespace Umbraco.Community.Contentment.DataEditors @@ -28,7 +27,7 @@ public sealed class UserDefinedDataListSource : IDataListSource Key = "items", Name = "Options", Description = "Configure the option items for the data list.

    Please try to avoid using duplicate values, as this may cause adverse issues with list editors.", - View = IOHelper.ResolveUrl(DataListDataEditor.DataEditorListEditorViewPath), + View = DataListDataEditor.DataEditorListEditorViewPath, Config = new Dictionary() { { "confirmRemoval", Constants.Values.True }, diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/XmlDataListSource.cs b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/XmlDataListSource.cs index 10250209..109a2e66 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/XmlDataListSource.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/DataSources/XmlDataListSource.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Xml; using System.Xml.XPath; @@ -99,12 +100,12 @@ public XmlDataListSource(ILogger logger) public IEnumerable GetItems(Dictionary config) { - var items = new List(); - var url = config.GetValueAs("url", string.Empty); if (string.IsNullOrWhiteSpace(url) == true) - return items; + { + return Enumerable.Empty(); + } var path = url.InvariantStartsWith("http") == false ? IOHelper.MapPath(url) @@ -119,7 +120,6 @@ public IEnumerable GetItems(Dictionary config) catch (WebException ex) { _logger.Error(ex, $"Unable to retrieve data from '{path}'."); - return items; } catch (XmlException ex) { @@ -128,14 +128,14 @@ public IEnumerable GetItems(Dictionary config) if (doc == null) { - return items; + return Enumerable.Empty(); } var itemsXPath = config.GetValueAs("itemsXPath", string.Empty); if (string.IsNullOrWhiteSpace(itemsXPath) == true) { - return items; + return Enumerable.Empty(); } var nav = doc.CreateNavigator(); @@ -158,7 +158,7 @@ public IEnumerable GetItems(Dictionary config) if (nodes.Count == 0) { _logger.Warn($"The XPath '{itemsXPath}' did not match any items in the XML: {nav.OuterXml.Substring(0, Math.Min(300, nav.OuterXml.Length))}"); - return items; + return Enumerable.Empty(); } var nameXPath = config.GetValueAs("nameXPath", "text()"); @@ -166,6 +166,8 @@ public IEnumerable GetItems(Dictionary config) var iconXPath = config.GetValueAs("iconXPath", string.Empty); var descriptionXPath = config.GetValueAs("descriptionXPath", string.Empty); + var items = new List(); + foreach (XPathNavigator node in nodes) { var name = node.SelectSingleNode(nameXPath, nsmgr); diff --git a/src/Umbraco.Community.Contentment/DataEditors/DataList/data-list.preview.js b/src/Umbraco.Community.Contentment/DataEditors/DataList/data-list.preview.js index 27f85923..3799fff5 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/DataList/data-list.preview.js +++ b/src/Umbraco.Community.Contentment/DataEditors/DataList/data-list.preview.js @@ -71,7 +71,7 @@ angular.module("umbraco").controller("Umbraco.Community.Contentment.DataEditors. .then(function (result) { vm.property = result; - vm.state = result.config.items.length > 0 ? "loaded" : "noItems"; + vm.state = result.config.items && result.config.items.length > 0 ? "loaded" : "noItems"; }); } diff --git a/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/ItemPickerDataListEditor.cs b/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/ItemPickerDataListEditor.cs index db877b91..9215ee26 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/ItemPickerDataListEditor.cs +++ b/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/ItemPickerDataListEditor.cs @@ -65,6 +65,7 @@ public sealed class ItemPickerDataListEditor : IDataListEditor { ShowDescriptionsConfigurationField.ShowDescriptions, Constants.Values.True }, } }, + new EnableFilterConfigurationField(), new MaxItemsConfigurationField(), new AllowClearConfigurationField(), new ConfigurationField @@ -95,6 +96,7 @@ public sealed class ItemPickerDataListEditor : IDataListEditor { { "listType", "list" }, { "defaultIcon", Core.Constants.Icons.DefaultIcon }, + { EnableFilterConfigurationField.EnableFilter, Constants.Values.True }, { MaxItemsConfigurationField.MaxItems, "0" }, }; diff --git a/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.css b/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.css index e3718896..8f54cc57 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.css +++ b/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.css @@ -47,3 +47,11 @@ .contentment.lk-overlay .umb-action:not(.-disabled) .umb-checkmark:not(.umb-checkmark--checked).icon-check:before { content: none; } + +.contentment .umb-card-grid .alert { + flex: none; + font-size: inherit; + text-align: inherit; + width: 100%; + max-width: 100%; +} diff --git a/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.html b/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.html index 6fee3c83..a15a94ec 100644 --- a/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.html +++ b/src/Umbraco.Community.Contentment/DataEditors/ItemPicker/item-picker.overlay.html @@ -39,16 +39,19 @@
      -
    • +
    • +
    • + No items found for ''. +
      -
    • +
    • +
    • + No items found for ''. +
    diff --git a/src/Umbraco.Community.Contentment/Properties/VersionInfo.cs b/src/Umbraco.Community.Contentment/Properties/VersionInfo.cs index ec5d5515..f9de6c1a 100644 --- a/src/Umbraco.Community.Contentment/Properties/VersionInfo.cs +++ b/src/Umbraco.Community.Contentment/Properties/VersionInfo.cs @@ -1,5 +1,5 @@ using System.Reflection; [assembly: AssemblyVersion("1.4")] -[assembly: AssemblyFileVersion("1.4.1")] -[assembly: AssemblyInformationalVersion("1.4.1")] +[assembly: AssemblyFileVersion("1.4.2")] +[assembly: AssemblyInformationalVersion("1.4.2")] diff --git a/src/Umbraco.Community.Contentment/Umbraco.Community.Contentment.csproj b/src/Umbraco.Community.Contentment/Umbraco.Community.Contentment.csproj index c069bd18..b322f58c 100644 --- a/src/Umbraco.Community.Contentment/Umbraco.Community.Contentment.csproj +++ b/src/Umbraco.Community.Contentment/Umbraco.Community.Contentment.csproj @@ -306,8 +306,10 @@ + + diff --git a/src/Umbraco.Community.Contentment/Web/UI/App_Plugins/Contentment/lang/en.xml b/src/Umbraco.Community.Contentment/Web/UI/App_Plugins/Contentment/lang/en.xml index 08152da5..ea715c49 100644 --- a/src/Umbraco.Community.Contentment/Web/UI/App_Plugins/Contentment/lang/en.xml +++ b/src/Umbraco.Community.Contentment/Web/UI/App_Plugins/Contentment/lang/en.xml @@ -11,6 +11,10 @@ Are you sure you want to remove this item? Yes, remove + + This item is no longer available + Please remove this configuration and select another item. + Select and configure a display mode Select and configure an element type