Add Multi Url Picker to core (#2323)
This commit is contained in:
committed by
Sebastiaan Janssen
parent
76bece07ef
commit
e1c9b1818e
@@ -447,6 +447,11 @@ namespace Umbraco.Core
|
||||
/// </summary>
|
||||
public const string NestedContentAlias = "Umbraco.NestedContent";
|
||||
|
||||
/// <summary>
|
||||
/// Alias for the multi url picker editor.
|
||||
/// </summary>
|
||||
public const string MultiUrlPickerAlias = "Umbraco.MultiUrlPicker";
|
||||
|
||||
public static class PreValueKeys
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<div ng-controller="Umbraco.PropertyEditors.MultiUrlPickerController" class="umb-editor umb-contentpicker">
|
||||
<p ng-if="(renderModel|filter:{trashed:true}).length == 1"><localize key="contentPicker_pickedTrashedItem"></localize></p>
|
||||
<p ng-if="(renderModel|filter:{trashed:true}).length > 1"><localize key="contentPicker_pickedTrashedItems"></localize></p>
|
||||
|
||||
<ng-form name="multiUrlPickerForm">
|
||||
<div ui-sortable="sortableOptions" ng-model="renderModel">
|
||||
<umb-node-preview
|
||||
ng-repeat="link in renderModel"
|
||||
icon="link.icon"
|
||||
name="link.name"
|
||||
published="link.published"
|
||||
description="link.url + (link.queryString ? link.queryString : '')"
|
||||
sortable="!sortableOptions.disabled"
|
||||
allow-remove="true"
|
||||
allow-edit="true"
|
||||
on-remove="remove($index)"
|
||||
on-edit="openLinkPicker(link, $index)">
|
||||
</umb-node-preview>
|
||||
</div>
|
||||
|
||||
<a ng-show="!model.config.maxNumber || renderModel.length < model.config.maxNumber"
|
||||
class="umb-node-preview-add"
|
||||
href
|
||||
ng-click="openLinkPicker()"
|
||||
prevent-default>
|
||||
<localize key="general_add">Add</localize>
|
||||
</a>
|
||||
|
||||
<div class="umb-contentpicker__min-max-help">
|
||||
|
||||
<!-- Both min and max items -->
|
||||
<span ng-if="model.config.minNumber && model.config.maxNumber && model.config.minNumber !== model.config.maxNumber">
|
||||
<span ng-if="renderModel.length < model.config.maxNumber">Add between {{model.config.minNumber}} and {{model.config.maxNumber}} items</span>
|
||||
<span ng-if="renderModel.length > model.config.maxNumber">
|
||||
<localize key="validation_maxCount">You can only have</localize> {{model.config.maxNumber}} <localize key="validation_itemsSelected"> items selected</localize>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Equal min and max -->
|
||||
<span ng-if="model.config.minNumber && model.config.maxNumber && model.config.minNumber === model.config.maxNumber">
|
||||
<span ng-if="renderModel.length < model.config.maxNumber">Add {{model.config.minNumber - renderModel.length}} item(s)</span>
|
||||
<span ng-if="renderModel.length > model.config.maxNumber">
|
||||
<localize key="validation_maxCount">You can only have</localize> {{model.config.maxNumber}} <localize key="validation_itemsSelected"> items selected</localize>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Only max -->
|
||||
<span ng-if="!model.config.minNumber && model.config.maxNumber">
|
||||
<span ng-if="renderModel.length < model.config.maxNumber">Add up to {{model.config.maxNumber}} items</span>
|
||||
<span ng-if="renderModel.length > model.config.maxNumber">
|
||||
<localize key="validation_maxCount">You can only have</localize> {{model.config.maxNumber}} <localize key="validation_itemsSelected">items selected</localize>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<!-- Only min -->
|
||||
<span ng-if="model.config.minNumber && !model.config.maxNumber && renderModel.length < model.config.minNumber">
|
||||
Add at least {{model.config.minNumber}} item(s)
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<!--These are here because we need ng-form fields to validate against-->
|
||||
<input type="hidden" name="minCount" ng-model="renderModel" />
|
||||
<input type="hidden" name="maxCount" ng-model="renderModel" />
|
||||
|
||||
<div class="help-inline" val-msg-for="minCount" val-toggle-msg="minCount">
|
||||
<localize key="validation_minCount">You need to add at least</localize> {{model.config.minNumber}} <localize key="validation_items">items</localize>
|
||||
</div>
|
||||
|
||||
<div class="help-inline" val-msg-for="maxCount" val-toggle-msg="maxCount">
|
||||
<localize key="validation_maxCount">You can only have</localize> {{model.config.maxNumber}} <localize key="validation_itemsSelected">items selected</localize>
|
||||
</div>
|
||||
</ng-form>
|
||||
<umb-overlay ng-if="linkPickerOverlay.show"
|
||||
model="linkPickerOverlay"
|
||||
view="linkPickerOverlay.view"
|
||||
position="right">
|
||||
</umb-overlay>
|
||||
</div>
|
||||
@@ -117,6 +117,7 @@ namespace Umbraco.Web.Cache
|
||||
LegacyMediaPickerPropertyConverter.ClearCaches();
|
||||
SliderValueConverter.ClearCaches();
|
||||
MediaPickerPropertyConverter.ClearCaches();
|
||||
MultiUrlPickerPropertyConverter.ClearCaches();
|
||||
|
||||
|
||||
base.Refresh(jsonPayload);
|
||||
|
||||
36
src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs
Normal file
36
src/Umbraco.Web/Models/ContentEditing/LinkDisplay.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
13
src/Umbraco.Web/Models/Link.cs
Normal file
13
src/Umbraco.Web/Models/Link.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
9
src/Umbraco.Web/Models/LinkType.cs
Normal file
9
src/Umbraco.Web/Models/LinkType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Web.Models
|
||||
{
|
||||
public enum LinkType
|
||||
{
|
||||
Content,
|
||||
Media,
|
||||
External
|
||||
}
|
||||
}
|
||||
249
src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs
Normal file
249
src/Umbraco.Web/PropertyEditors/MultiUrlPickerPropertyEditor.cs
Normal file
@@ -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<object>();
|
||||
|
||||
var value = property.Value.ToString();
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
return Enumerable.Empty<object>();
|
||||
|
||||
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<List<LinkDto>>(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<IUmbracoEntity>();
|
||||
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<LinkDisplay>();
|
||||
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<MultiUrlPickerPropertyValueEditor>($"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<List<LinkDisplay>>(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<MultiUrlPickerPropertyValueEditor>($"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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MultiUrlPickerPropertyConverter>("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<Link>()
|
||||
: 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<Link>();
|
||||
var dtos = ((JArray) source).ToObject<IEnumerable<MultiUrlPickerPropertyEditor.LinkDto>>();
|
||||
|
||||
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<Link>)
|
||||
: 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<int>().Result
|
||||
: 0;
|
||||
});
|
||||
|
||||
return maxNumber != 1;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<int, int> Storages = new ConcurrentDictionary<int, int>();
|
||||
|
||||
internal static void ClearCaches()
|
||||
{
|
||||
Storages.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,6 +419,7 @@
|
||||
<Compile Include="Models\ContentEditing\ContentTypeSave.cs" />
|
||||
<Compile Include="Models\ContentEditing\DocumentTypeSave.cs" />
|
||||
<Compile Include="Models\ContentEditing\InstalledPackageModel.cs" />
|
||||
<Compile Include="Models\ContentEditing\LinkDisplay.cs" />
|
||||
<Compile Include="Models\ContentEditing\MediaTypeDisplay.cs" />
|
||||
<Compile Include="Models\ContentEditing\MediaTypeSave.cs" />
|
||||
<Compile Include="Models\ContentEditing\MemberPropertyTypeBasic.cs" />
|
||||
@@ -444,6 +445,8 @@
|
||||
<Compile Include="Models\DetachedPublishedProperty.cs" />
|
||||
<Compile Include="Models\ContentEditing\UserInvite.cs" />
|
||||
<Compile Include="Models\ContentEditing\UserSave.cs" />
|
||||
<Compile Include="Models\Link.cs" />
|
||||
<Compile Include="Models\LinkType.cs" />
|
||||
<Compile Include="Models\LocalPackageInstallModel.cs" />
|
||||
<Compile Include="Models\Mapping\CodeFileDisplayMapper.cs" />
|
||||
<Compile Include="Models\Mapping\ContentTypeModelMapperExtensions.cs" />
|
||||
@@ -485,6 +488,7 @@
|
||||
<Compile Include="PropertyEditors\MediaPicker2PropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\MemberPicker2PropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\MultiNodeTreePicker2PropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\MultiUrlPickerPropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\NestedContentController.cs" />
|
||||
<Compile Include="PropertyEditors\NestedContentHelper.cs" />
|
||||
<Compile Include="PropertyEditors\NestedContentPropertyEditor.cs" />
|
||||
@@ -499,6 +503,7 @@
|
||||
<Compile Include="PropertyEditors\ValueConverters\MemberPickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\MultiNodeTreePickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\LegacyMediaPickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\MultiUrlPickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\NestedContentSingleValueConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\NestedContentManyValueConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\NestedContentPublishedPropertyTypeExtensions.cs" />
|
||||
|
||||
Reference in New Issue
Block a user