From e1c9b1818ee639bf2136d06654121299d6473d49 Mon Sep 17 00:00:00 2001 From: Rasmus John Pedersen Date: Sun, 20 Jan 2019 21:12:00 +0100 Subject: [PATCH] Add Multi Url Picker to core (#2323) --- src/Umbraco.Core/Constants-PropertyEditors.cs | 5 + .../PropertyEditors/PropertyEditorResolver.cs | 6 + .../multiurlpicker.controller.js | 133 ++++++++++ .../multiurlpicker/multiurlpicker.html | 79 ++++++ .../Cache/DataTypeCacheRefresher.cs | 1 + .../Models/ContentEditing/LinkDisplay.cs | 36 +++ src/Umbraco.Web/Models/Link.cs | 13 + src/Umbraco.Web/Models/LinkType.cs | 9 + .../MultiUrlPickerPropertyEditor.cs | 249 ++++++++++++++++++ .../RelatedLinks2PropertyEditor.cs | 9 +- .../MultiUrlPickerPropertyConverter.cs | 165 ++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 5 + 12 files changed, 707 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html create mode 100644 src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs create mode 100644 src/Umbraco.Web/Models/Link.cs create mode 100644 src/Umbraco.Web/Models/LinkType.cs create mode 100644 src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerPropertyConverter.cs diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 43779ea44a..80976b33ca 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -447,6 +447,11 @@ namespace Umbraco.Core /// public const string NestedContentAlias = "Umbraco.NestedContent"; + /// + /// Alias for the multi url picker editor. + /// + public const string MultiUrlPickerAlias = "Umbraco.MultiUrlPicker"; + public static class PreValueKeys { /// diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs index 4db0d4f444..46091e4834 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditorResolver.cs @@ -53,6 +53,12 @@ namespace Umbraco.Core.PropertyEditors nestedContentEditorFromPackage.Name = "(Obsolete) " + nestedContentEditorFromPackage.Name; nestedContentEditorFromPackage.IsDeprecated = true; } + var multiUrlPickerEditorFromPackage = editors.FirstOrDefault(x => x.Alias == "RJP.MultiUrlPicker"); + if (multiUrlPickerEditorFromPackage != null) + { + multiUrlPickerEditorFromPackage.Name = "(Obsolete) " + multiUrlPickerEditorFromPackage.Name; + multiUrlPickerEditorFromPackage.IsDeprecated = true; + } return editors; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js new file mode 100644 index 0000000000..1795622aed --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -0,0 +1,133 @@ +function multiUrlPickerController($scope, angularHelper, localizationService, entityResource, iconHelper) { + + $scope.renderModel = []; + + if ($scope.preview) { + return; + } + + if (!Array.isArray($scope.model.value)) { + $scope.model.value = []; + } + + var currentForm = angularHelper.getCurrentForm($scope); + + $scope.sortableOptions = { + distance: 10, + tolerance: "pointer", + scroll: true, + zIndex: 6000, + update: function () { + currentForm.$setDirty(); + } + }; + + $scope.model.value.forEach(function (link) { + link.icon = iconHelper.convertFromLegacyIcon(link.icon); + $scope.renderModel.push(link); + }); + + $scope.$on("formSubmitting", function () { + $scope.model.value = $scope.renderModel; + }); + + $scope.$watch( + function () { + return $scope.renderModel.length; + }, + function () { + if ($scope.model.config && $scope.model.config.minNumber) { + $scope.multiUrlPickerForm.minCount.$setValidity( + "minCount", + +$scope.model.config.minNumber <= $scope.renderModel.length + ); + } + if ($scope.model.config && $scope.model.config.maxNumber) { + $scope.multiUrlPickerForm.maxCount.$setValidity( + "maxCount", + +$scope.model.config.maxNumber >= $scope.renderModel.length + ); + } + $scope.sortableOptions.disabled = $scope.renderModel.length === 1; + } + ); + + $scope.remove = function ($index) { + $scope.renderModel.splice($index, 1); + + currentForm.$setDirty(); + }; + + $scope.openLinkPicker = function (link, $index) { + var target = link ? { + name: link.name, + anchor: link.queryString, + // the linkPicker breaks if it get an udi for media + udi: link.isMedia ? null : link.udi, + url: link.url, + target: link.target + } : null; + + $scope.linkPickerOverlay = { + view: "linkpicker", + currentTarget: target, + show: true, + submit: function (model) { + if (model.target.url) { + // if an anchor exists, check that it is appropriately prefixed + if (model.target.anchor && model.target.anchor[0] !== '?' && model.target.anchor[0] !== '#') { + model.target.anchor = (model.target.anchor.indexOf('=') === -1 ? '#' : '?') + model.target.anchor; + } + if (link) { + if (link.isMedia && link.url === model.target.url) { + // we can assume the existing media item is changed and no new file has been selected + // so we don't need to update the udi and isMedia fields + } else { + link.udi = model.target.udi; + link.isMedia = model.target.isMedia; + } + + link.name = model.target.name || model.target.url; + link.queryString = model.target.anchor; + link.target = model.target.target; + link.url = model.target.url; + } else { + link = { + isMedia: model.target.isMedia, + name: model.target.name || model.target.url, + queryString: model.target.anchor, + target: model.target.target, + udi: model.target.udi, + url: model.target.url + }; + $scope.renderModel.push(link); + } + + if (link.udi) { + var entityType = link.isMedia ? "media" : "document"; + + entityResource.getById(link.udi, entityType).then(function (data) { + link.icon = iconHelper.convertFromLegacyIcon(data.icon); + link.published = (data.metaData && data.metaData.IsPublished === false && entityType === "Document") ? false : true; + link.trashed = data.trashed; + if (link.trashed) { + item.url = localizationService.dictionary.general_recycleBin; + } + }); + } else { + link.icon = "icon-link"; + link.published = true; + } + + currentForm.$setDirty(); + } + + $scope.linkPickerOverlay.show = false; + $scope.linkPickerOverlay = null; + } + }; + }; +} + +angular.module("umbraco").controller("Umbraco.PropertyEditors.MultiUrlPickerController", multiUrlPickerController); + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html new file mode 100644 index 0000000000..9f542ba1a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html @@ -0,0 +1,79 @@ +
+

+

+ + +
+ + +
+ + + Add + + +
+ + + + Add between {{model.config.minNumber}} and {{model.config.maxNumber}} items + + You can only have {{model.config.maxNumber}} items selected + + + + + + Add {{model.config.minNumber - renderModel.length}} item(s) + + You can only have {{model.config.maxNumber}} items selected + + + + + + Add up to {{model.config.maxNumber}} items + + You can only have {{model.config.maxNumber}} items selected + + + + + + Add at least {{model.config.minNumber}} item(s) + + +
+ + + + + +
+ You need to add at least {{model.config.minNumber}} items +
+ +
+ You can only have {{model.config.maxNumber}} items selected +
+
+ + +
diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index 65d366e2ad..fe85263f3b 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -117,6 +117,7 @@ namespace Umbraco.Web.Cache LegacyMediaPickerPropertyConverter.ClearCaches(); SliderValueConverter.ClearCaches(); MediaPickerPropertyConverter.ClearCaches(); + MultiUrlPickerPropertyConverter.ClearCaches(); base.Refresh(jsonPayload); diff --git a/src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs new file mode 100644 index 0000000000..857ae2c318 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs @@ -0,0 +1,36 @@ +using System.Runtime.Serialization; +using Umbraco.Core; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "link", Namespace = "")] + internal class LinkDisplay + { + [DataMember(Name = "icon")] + public string Icon { get; set; } + + [DataMember(Name = "isMedia")] + public bool IsMedia { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "published")] + public bool Published { get; set; } + + [DataMember(Name = "queryString")] + public string QueryString { get; set; } + + [DataMember(Name = "target")] + public string Target { get; set; } + + [DataMember(Name = "trashed")] + public bool Trashed { get; set; } + + [DataMember(Name = "udi")] + public GuidUdi Udi { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/Link.cs b/src/Umbraco.Web/Models/Link.cs new file mode 100644 index 0000000000..74ad4ad2af --- /dev/null +++ b/src/Umbraco.Web/Models/Link.cs @@ -0,0 +1,13 @@ +using Umbraco.Core; + +namespace Umbraco.Web.Models +{ + public class Link + { + public string Name { get; set; } + public string Target { get; set; } + public LinkType Type { get; set; } + public Udi Udi { get; set; } + public string Url { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/LinkType.cs b/src/Umbraco.Web/Models/LinkType.cs new file mode 100644 index 0000000000..3db3165d7f --- /dev/null +++ b/src/Umbraco.Web/Models/LinkType.cs @@ -0,0 +1,9 @@ +namespace Umbraco.Web.Models +{ + public enum LinkType + { + Content, + Media, + External + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs new file mode 100644 index 0000000000..84523f05c4 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Routing; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.MultiUrlPickerAlias, "Multi Url Picker", PropertyEditorValueTypes.Json, "multiurlpicker", Group = "pickers", Icon = "icon-link", IsParameterEditor = true)] + public class MultiUrlPickerPropertyEditor : PropertyEditor + { + protected override PreValueEditor CreatePreValueEditor() + { + return new MultiUrlPickerPreValueEditor(); + } + + protected override PropertyValueEditor CreateValueEditor() + { + return new MultiUrlPickerPropertyValueEditor(base.CreateValueEditor()); + } + + private class MultiUrlPickerPreValueEditor : PreValueEditor + { + public MultiUrlPickerPreValueEditor() + { + Fields.Add(new PreValueField + { + Key = "minNumber", + View = "number", + Name = "Minimum number of items" + }); + Fields.Add(new PreValueField + { + Key = "maxNumber", + View = "number", + Name = "Maximum number of items" + }); + } + } + + private class MultiUrlPickerPropertyValueEditor : PropertyValueEditorWrapper + { + public MultiUrlPickerPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) + { + } + + public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService) + { + if (property.Value == null) + return Enumerable.Empty(); + + var value = property.Value.ToString(); + + if (string.IsNullOrEmpty(value)) + return Enumerable.Empty(); + + try + { + var umbHelper = new UmbracoHelper(UmbracoContext.Current); + var services = ApplicationContext.Current.Services; + var entityService = services.EntityService; + var contentTypeService = services.ContentTypeService; + string deletedLocalization = null; + string recycleBinLocalization = null; + + var dtos = JsonConvert.DeserializeObject>(value); + + var documentLinks = dtos.FindAll(link => + link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Document + ); + + var mediaLinks = dtos.FindAll(link => + link.Udi != null && link.Udi.EntityType == Constants.UdiEntityType.Media + ); + + var entities = new List(); + if (documentLinks.Count > 0) + { + entities.AddRange( + entityService.GetAll(UmbracoObjectTypes.Document, + documentLinks.Select(link => link.Udi.Guid).ToArray()) + ); + } + + if (mediaLinks.Count > 0) + { + entities.AddRange( + entityService.GetAll(UmbracoObjectTypes.Media, + mediaLinks.Select(link => link.Udi.Guid).ToArray()) + ); + } + + var links = new List(); + foreach (var dto in dtos) + { + var link = new LinkDisplay + { + Icon = "icon-link", + IsMedia = false, + Name = dto.Name, + Published = true, + QueryString = dto.QueryString, + Target = dto.Target, + Trashed = false, + Udi = dto.Udi, + Url = dto.Url, + }; + + links.Add(link); + + if (dto.Udi == null) + continue; + + var entity = entities.Find(e => e.Key == dto.Udi.Guid); + if (entity == null) + { + if (deletedLocalization == null) + deletedLocalization = services.TextService.Localize("general/deleted"); + + link.Published = false; + link.Trashed = true; + link.Url = deletedLocalization; + } + else + { + var entityType = + Equals(entity.AdditionalData["NodeObjectTypeId"], Constants.ObjectTypes.MediaGuid) + ? Constants.UdiEntityType.Media + : Constants.UdiEntityType.Document; + + var udi = new GuidUdi(entityType, entity.Key); + + var contentTypeAlias = (string)entity.AdditionalData["ContentTypeAlias"]; + if (entity.Trashed) + { + if (recycleBinLocalization == null) + recycleBinLocalization = services.TextService.Localize("general/recycleBin"); + + link.Trashed = true; + link.Url = recycleBinLocalization; + } + + if (udi.EntityType == Constants.UdiEntityType.Document) + { + var contentType = contentTypeService.GetContentType(contentTypeAlias); + + if (contentType == null) + continue; + + link.Icon = contentType.Icon; + link.Published = Equals(entity.AdditionalData["IsPublished"], true); + + if (link.Trashed == false) + link.Url = umbHelper.Url(entity.Id, UrlProviderMode.Relative); + } + else + { + link.IsMedia = true; + + var mediaType = contentTypeService.GetMediaType(contentTypeAlias); + + if (mediaType == null) + continue; + + link.Icon = mediaType.Icon; + + if (link.Trashed) + continue; + + var media = umbHelper.TypedMedia(entity.Id); + if (media != null) + link.Url = media.Url; + } + } + } + return links; + } + catch (Exception ex) + { + ApplicationContext.Current.ProfilingLogger.Logger.Error($"Error getting links.\r\n{property.Value}", ex); + } + + return base.ConvertDbToEditor(property, propertyType, dataTypeService); + } + + public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) + { + if (editorValue.Value == null) + return null; + + var value = editorValue.Value.ToString(); + + if (string.IsNullOrEmpty(value)) + return null; + + try + { + return JsonConvert.SerializeObject( + from link in JsonConvert.DeserializeObject>(value) + select new LinkDto + { + Name = link.Name, + QueryString = link.QueryString, + Target = link.Target, + Udi = link.Udi, + Url = link.Udi == null ? link.Url : null, // only save the url for external links + }, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + } + catch (Exception ex) + { + ApplicationContext.Current.ProfilingLogger.Logger.Error($"Error saving links.\r\n{editorValue.Value}", ex); + } + return base.ConvertEditorToDb(editorValue, currentValue); + } + } + + [DataContract] + internal class LinkDto + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "queryString")] + public string QueryString { get; set; } + + [DataMember(Name = "target")] + public string Target { get; set; } + + [DataMember(Name = "udi")] + public GuidUdi Udi { get; set; } + + [DataMember(Name = "url")] + public string Url { get; set; } + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs index 3aaf191fd2..541dccaa4e 100644 --- a/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RelatedLinks2PropertyEditor.cs @@ -1,10 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { - [PropertyEditor(Constants.PropertyEditors.RelatedLinks2Alias, "Related links", "relatedlinks", ValueType = PropertyEditorValueTypes.Json, Icon = "icon-thumbnail-list", Group = "pickers")] + // TODO: Remove in V8 + [Obsolete("This editor is obsolete, use MultiUrlPickerPropertyEditor instead")] + [PropertyEditor(Constants.PropertyEditors.RelatedLinks2Alias, "Related links", "relatedlinks", ValueType = PropertyEditorValueTypes.Json, Icon = "icon-thumbnail-list", Group = "pickers", IsDeprecated = true)] public class RelatedLinks2PropertyEditor : PropertyEditor { public RelatedLinks2PropertyEditor() @@ -33,4 +36,4 @@ namespace Umbraco.Web.PropertyEditors public int Maximum { get; set; } } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerPropertyConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerPropertyConverter.cs new file mode 100644 index 0000000000..397448983a --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MultiUrlPickerPropertyConverter.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; +using Umbraco.Web.Models; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter(typeof(JsonValueConverter))] + public class MultiUrlPickerPropertyConverter : PropertyValueConverterBase, IPropertyValueConverterMeta + { + private readonly IDataTypeService _dataTypeService; + + public MultiUrlPickerPropertyConverter(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + //TODO: Remove this ctor in v8 since the other one will use IoC + public MultiUrlPickerPropertyConverter() : this(ApplicationContext.Current.Services.DataTypeService) + { + } + + public override bool IsConverter(PublishedPropertyType propertyType) + { + return propertyType.PropertyEditorAlias.Equals(Constants.PropertyEditors.MultiUrlPickerAlias); + } + + public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview) + { + if (source == null) + return null; + + if (source.ToString().Trim().StartsWith("[") == false) + return null; + + try + { + return JArray.Parse(source.ToString()); + } + catch (Exception ex) + { + LogHelper.Error("Error parsing JSON", ex); + } + return null; + } + + public override object ConvertSourceToObject(PublishedPropertyType propertyType, object source, bool preview) + { + var isMultiple = IsMultipleDataType(propertyType.DataTypeId, out var maxNumber); + if (source == null) + return isMultiple + ? Enumerable.Empty() + : null; + + //TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton + if (UmbracoContext.Current == null) + return source; + + var umbHelper = new UmbracoHelper(UmbracoContext.Current); + + var links = new List(); + var dtos = ((JArray) source).ToObject>(); + + foreach (var dto in dtos) + { + var type = LinkType.External; + var url = dto.Url; + if (dto.Udi != null) + { + type = dto.Udi.EntityType == Constants.UdiEntityType.Media + ? LinkType.Media + : LinkType.Content; + + if (type == LinkType.Media) + { + var media = umbHelper.TypedMedia(dto.Udi); + if (media == null) + continue; + url = media.Url; + } + else + { + var content = umbHelper.TypedContent(dto.Udi); + if (content == null) + continue; + url = content.Url; + } + } + + var link = new Link + { + Name = dto.Name, + Target = dto.Target, + Type = type, + Udi = dto.Udi, + Url = url + dto.QueryString, + }; + + links.Add(link); + } + + if (isMultiple == false) + return links.FirstOrDefault(); + if (maxNumber > 0) + return links.Take(maxNumber); + + return links; + } + + public Type GetPropertyValueType(PublishedPropertyType propertyType) + { + return IsMultipleDataType(propertyType.DataTypeId, out var maxNumber) + ? typeof(IEnumerable) + : typeof(Link); + } + + public PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue) + { + switch (cacheValue) + { + case PropertyCacheValue.Source: + return PropertyCacheLevel.Content; + case PropertyCacheValue.Object: + case PropertyCacheValue.XPath: + return PropertyCacheLevel.ContentCache; + } + + return PropertyCacheLevel.None; + } + + private bool IsMultipleDataType(int dataTypeId, out int maxNumber) + { + // GetPreValuesCollectionByDataTypeId is cached at repository level; + // still, the collection is deep-cloned so this is kinda expensive, + // better to cache here + trigger refresh in DataTypeCacheRefresher + + maxNumber = Storages.GetOrAdd(dataTypeId, id => + { + var preValues = _dataTypeService.GetPreValuesCollectionByDataTypeId(id).PreValuesAsDictionary; + + return preValues.TryGetValue("maxNumber", out var maxNumberPreValue) + ? maxNumberPreValue.Value.TryConvertTo().Result + : 0; + }); + + return maxNumber != 1; + } + + private static readonly ConcurrentDictionary Storages = new ConcurrentDictionary(); + + internal static void ClearCaches() + { + Storages.Clear(); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e021de0011..1b668b06c8 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -419,6 +419,7 @@ + @@ -444,6 +445,8 @@ + + @@ -485,6 +488,7 @@ + @@ -499,6 +503,7 @@ +