diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs
index 8b637fe90e..b9f20fb449 100644
--- a/src/Umbraco.Core/Constants-PropertyEditors.cs
+++ b/src/Umbraco.Core/Constants-PropertyEditors.cs
@@ -118,7 +118,12 @@ namespace Umbraco.Core
/// RadioButton list.
///
public const string RadioButtonList = "Umbraco.RadioButtonList";
-
+
+ ///
+ /// Related Links.
+ ///
+ public const string RelatedLinks = "Umbraco.RelatedLinks";
+
///
/// Slider.
///
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
index 4e0b5dfa0f..f32ea1cb6f 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
@@ -135,7 +135,7 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
_database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
_database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
- _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
+ _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Related Links", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now });
}
private void CreateLockData()
@@ -301,7 +301,7 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1048, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext" });
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1049, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext",
Configuration = "{\"multiPicker\":1}" });
- _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" });
+ _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Constants.PropertyEditors.Aliases.RelatedLinks, DbType = "Ntext" });
}
private void CreateRelationTypeData()
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
index 064ffc7228..ee439088be 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
@@ -16,6 +16,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
RenameDataType(Constants.PropertyEditors.Aliases.MediaPicker + "2", Constants.PropertyEditors.Aliases.MediaPicker);
RenameDataType(Constants.PropertyEditors.Aliases.MemberPicker + "2", Constants.PropertyEditors.Aliases.MemberPicker);
RenameDataType(Constants.PropertyEditors.Aliases.MultiNodeTreePicker + "2", Constants.PropertyEditors.Aliases.MultiNodeTreePicker);
+ RenameDataType(Constants.PropertyEditors.Aliases.RelatedLinks + "2", Constants.PropertyEditors.Aliases.RelatedLinks);
RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea, false);
RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox, false);
}
diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs
index add3424599..6b3ce3eb4e 100644
--- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs
+++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs
@@ -279,7 +279,7 @@ AnotherContentFinder
public void GetDataEditors()
{
var types = _typeLoader.GetDataEditors();
- Assert.AreEqual(39, types.Count());
+ Assert.AreEqual(40, types.Count());
}
///
diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs
index 9490213d62..039bcaed24 100644
--- a/src/Umbraco.Tests/Services/ContentServiceTests.cs
+++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs
@@ -2215,7 +2215,7 @@ namespace Umbraco.Tests.Services
Assert.That(sut.GetValue("contentPicker"), Is.EqualTo(Udi.Create(Constants.UdiEntityType.Document, new Guid("74ECA1D4-934E-436A-A7C7-36CC16D4095C"))));
Assert.That(sut.GetValue("mediaPicker"), Is.EqualTo(Udi.Create(Constants.UdiEntityType.Media, new Guid("44CB39C8-01E5-45EB-9CF8-E70AAF2D1691"))));
Assert.That(sut.GetValue("memberPicker"), Is.EqualTo(Udi.Create(Constants.UdiEntityType.Member, new Guid("9A50A448-59C0-4D42-8F93-4F1D55B0F47D"))));
- Assert.That(sut.GetValue("multiUrlPicker"), Is.EqualTo("[{\"name\":\"https://test.com\",\"url\":\"https://test.com\"}]"));
+ Assert.That(sut.GetValue("relatedLinks"), Is.EqualTo(""));
Assert.That(sut.GetValue("tags"), Is.EqualTo("this,is,tags"));
}
diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs
index 19a57d7775..faf4acf8a4 100644
--- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs
+++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContent.cs
@@ -132,7 +132,7 @@ namespace Umbraco.Tests.TestHelpers.Entities
content.SetValue("contentPicker", Udi.Create(Constants.UdiEntityType.Document, new Guid("74ECA1D4-934E-436A-A7C7-36CC16D4095C")).ToString());
content.SetValue("mediaPicker", Udi.Create(Constants.UdiEntityType.Media, new Guid("44CB39C8-01E5-45EB-9CF8-E70AAF2D1691")).ToString());
content.SetValue("memberPicker", Udi.Create(Constants.UdiEntityType.Member, new Guid("9A50A448-59C0-4D42-8F93-4F1D55B0F47D")).ToString());
- content.SetValue("multiUrlPicker", "[{\"name\":\"https://test.com\",\"url\":\"https://test.com\"}]");
+ content.SetValue("relatedLinks", "");
content.SetValue("tags", "this,is,tags");
return content;
diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs
index d7dcf8e79a..14b967b1c9 100644
--- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs
+++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs
@@ -370,7 +370,7 @@ namespace Umbraco.Tests.TestHelpers.Entities
contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.ContentPicker, ValueStorageType.Integer) { Alias = "contentPicker", Name = "Content Picker", Mandatory = false, SortOrder = 16, DataTypeId = 1046 });
contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MediaPicker, ValueStorageType.Integer) { Alias = "mediaPicker", Name = "Media Picker", Mandatory = false, SortOrder = 17, DataTypeId = 1048 });
contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MemberPicker, ValueStorageType.Integer) { Alias = "memberPicker", Name = "Member Picker", Mandatory = false, SortOrder = 18, DataTypeId = 1047 });
- contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.MultiUrlPicker, ValueStorageType.Nvarchar) { Alias = "multiUrlPicker", Name = "Multi URL Picker", Mandatory = false, SortOrder = 21, DataTypeId = 1050 });
+ contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.RelatedLinks, ValueStorageType.Ntext) { Alias = "relatedLinks", Name = "Related Links", Mandatory = false, SortOrder = 21, DataTypeId = 1050 });
contentCollection.Add(new PropertyType(Constants.PropertyEditors.Aliases.Tags, ValueStorageType.Ntext) { Alias = "tags", Name = "Tags", Mandatory = false, SortOrder = 22, DataTypeId = 1041 });
contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 });
diff --git a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs b/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs
index ba19f41e74..cc83dcb1c9 100644
--- a/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs
+++ b/src/Umbraco.Tests/Web/Mvc/HtmlHelperExtensionMethodsTests.cs
@@ -29,5 +29,30 @@ namespace Umbraco.Tests.Web.Mvc
var output = _htmlHelper.Wrap("div", "hello world", new {style = "color:red;", onclick = "void();"});
Assert.AreEqual("hello world
", output.ToHtmlString());
}
+
+ [Test]
+ public void GetRelatedLinkHtml_Simple()
+ {
+ var relatedLink = new Umbraco.Web.Models.RelatedLink {
+ Caption = "Link Caption",
+ NewWindow = true,
+ Link = "https://www.google.com/"
+ };
+ var output = _htmlHelper.GetRelatedLinkHtml(relatedLink);
+ Assert.AreEqual("Link Caption", output.ToHtmlString());
+ }
+
+ [Test]
+ public void GetRelatedLinkHtml_HtmlAttributes()
+ {
+ var relatedLink = new Umbraco.Web.Models.RelatedLink
+ {
+ Caption = "Link Caption",
+ NewWindow = true,
+ Link = "https://www.google.com/"
+ };
+ var output = _htmlHelper.GetRelatedLinkHtml(relatedLink, new { @class = "test-class"});
+ Assert.AreEqual("Link Caption", output.ToHtmlString());
+ }
}
}
diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs
index 1186102bc8..626a19a369 100644
--- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs
+++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs
@@ -830,5 +830,39 @@ namespace Umbraco.Web
}
#endregion
+
+
+ #region RelatedLink
+
+ ///
+ /// Renders an anchor element for a RelatedLink instance.
+ /// Format: <a href="relatedLink.Link" target="_blank/_self">relatedLink.Caption</a>
+ ///
+ /// The HTML helper instance that this method extends.
+ /// The RelatedLink instance
+ /// An anchor element
+ public static MvcHtmlString GetRelatedLinkHtml(this HtmlHelper htmlHelper, RelatedLink relatedLink)
+ {
+ return htmlHelper.GetRelatedLinkHtml(relatedLink, null);
+ }
+
+ ///
+ /// Renders an anchor element for a RelatedLink instance, accepting htmlAttributes.
+ /// Format: <a href="relatedLink.Link" target="_blank/_self" htmlAttributes>relatedLink.Caption</a>
+ ///
+ /// The HTML helper instance that this method extends.
+ /// The RelatedLink instance
+ /// An object that contains the HTML attributes to set for the element.
+ ///
+ public static MvcHtmlString GetRelatedLinkHtml(this HtmlHelper htmlHelper, RelatedLink relatedLink, object htmlAttributes)
+ {
+ var tagBuilder = new TagBuilder("a");
+ tagBuilder.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
+ tagBuilder.MergeAttribute("href", relatedLink.Link);
+ tagBuilder.MergeAttribute("target", relatedLink.NewWindow ? "_blank" : "_self");
+ tagBuilder.InnerHtml = HttpUtility.HtmlEncode(relatedLink.Caption);
+ return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.Normal));
+ }
+ #endregion
}
}
diff --git a/src/Umbraco.Web/Models/RelatedLink.cs b/src/Umbraco.Web/Models/RelatedLink.cs
new file mode 100644
index 0000000000..1e1d7636ad
--- /dev/null
+++ b/src/Umbraco.Web/Models/RelatedLink.cs
@@ -0,0 +1,11 @@
+using Umbraco.Core.Models.PublishedContent;
+
+namespace Umbraco.Web.Models
+{
+ public class RelatedLink : RelatedLinkBase
+ {
+ public int? Id { get; internal set; }
+ internal bool IsDeleted { get; set; }
+ public IPublishedContent Content { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/Models/RelatedLinkBase.cs b/src/Umbraco.Web/Models/RelatedLinkBase.cs
new file mode 100644
index 0000000000..c2077ce4a9
--- /dev/null
+++ b/src/Umbraco.Web/Models/RelatedLinkBase.cs
@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+
+namespace Umbraco.Web.Models
+{
+ public abstract class RelatedLinkBase
+ {
+ [JsonProperty("caption")]
+ public string Caption { get; set; }
+ [JsonProperty("link")]
+ public string Link { get; set; }
+ [JsonProperty("newWindow")]
+ public bool NewWindow { get; set; }
+ [JsonProperty("isInternal")]
+ public bool IsInternal { get; set; }
+ [JsonProperty("type")]
+ public RelatedLinkType Type { get; set; }
+ }
+}
diff --git a/src/Umbraco.Web/Models/RelatedLinkType.cs b/src/Umbraco.Web/Models/RelatedLinkType.cs
new file mode 100644
index 0000000000..eec7817ab6
--- /dev/null
+++ b/src/Umbraco.Web/Models/RelatedLinkType.cs
@@ -0,0 +1,27 @@
+// --------------------------------------------------------------------------------------------------------------------
+//
+// Umbraco
+//
+//
+// Defines the RelatedLinkType type.
+//
+// --------------------------------------------------------------------------------------------------------------------
+
+namespace Umbraco.Web.Models
+{
+ ///
+ /// The related link type.
+ ///
+ public enum RelatedLinkType
+ {
+ ///
+ /// Internal link type
+ ///
+ Internal,
+
+ ///
+ /// External link type
+ ///
+ External
+ }
+}
diff --git a/src/Umbraco.Web/Models/RelatedLinks.cs b/src/Umbraco.Web/Models/RelatedLinks.cs
new file mode 100644
index 0000000000..22cdcd11b6
--- /dev/null
+++ b/src/Umbraco.Web/Models/RelatedLinks.cs
@@ -0,0 +1,42 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+
+namespace Umbraco.Web.Models
+{
+ [TypeConverter(typeof(RelatedLinksTypeConverter))]
+ public class RelatedLinks : IEnumerable
+ {
+ private readonly string _propertyData;
+
+ private readonly IEnumerable _relatedLinks;
+
+ public RelatedLinks(IEnumerable relatedLinks, string propertyData)
+ {
+ _relatedLinks = relatedLinks;
+ _propertyData = propertyData;
+ }
+
+ ///
+ /// Gets the property data.
+ ///
+ internal string PropertyData
+ {
+ get
+ {
+ return this._propertyData;
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _relatedLinks.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinksConfiguration.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinksConfiguration.cs
new file mode 100644
index 0000000000..5db14c6842
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/RelatedLinksConfiguration.cs
@@ -0,0 +1,13 @@
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ ///
+ /// Represents the configuration for the related links value editor.
+ ///
+ public class RelatedLinksConfiguration
+ {
+ [ConfigurationField("max", "Maximum number of links", "number", Description = "Enter the maximum amount of links to be added, enter 0 for unlimited")]
+ public int Maximum { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinksConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinksConfigurationEditor.cs
new file mode 100644
index 0000000000..07ff359a82
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/RelatedLinksConfigurationEditor.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ ///
+ /// Represents the configuration editor for the related links value editor.
+ ///
+ public class RelatedLinksConfigurationEditor : ConfigurationEditor
+ {
+ public override IDictionary ToValueEditor(object configuration)
+ {
+ var d = base.ToValueEditor(configuration);
+ d["idType"] = "udi";
+ return d;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs
new file mode 100644
index 0000000000..b450fcc67f
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/RelatedLinksPropertyEditor.cs
@@ -0,0 +1,16 @@
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.PropertyEditors
+{
+ [DataEditor(Constants.PropertyEditors.Aliases.RelatedLinks, "Related links", "relatedlinks", ValueType = ValueTypes.Json, Icon = "icon-thumbnail-list", Group = "pickers")]
+ public class RelatedLinksPropertyEditor : DataEditor
+ {
+ public RelatedLinksPropertyEditor(ILogger logger)
+ : base(logger)
+ { }
+
+ protected override IConfigurationEditor CreateConfigurationEditor() => new RelatedLinksConfigurationEditor();
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksLegacyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksLegacyValueConverter.cs
new file mode 100644
index 0000000000..6c2a4331d0
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksLegacyValueConverter.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Xml;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Umbraco.Core;
+using Umbraco.Core.Cache;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Models.PublishedContent;
+using Umbraco.Core.PropertyEditors;
+using Umbraco.Core.PropertyEditors.ValueConverters;
+using Umbraco.Core.Services;
+
+namespace Umbraco.Web.PropertyEditors.ValueConverters
+{
+ [DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter
+ public class RelatedLinksLegacyValueConverter : PropertyValueConverterBase
+ {
+ private static readonly string[] MatchingEditors = {
+ Constants.PropertyEditors.Aliases.RelatedLinks
+ };
+
+ private readonly IUmbracoContextAccessor _umbracoContextAccessor;
+ private readonly ILogger _logger;
+ private readonly ServiceContext _services;
+
+ public RelatedLinksLegacyValueConverter(IUmbracoContextAccessor umbracoContextAccessor, ServiceContext services, ILogger logger)
+ {
+ _umbracoContextAccessor = umbracoContextAccessor;
+ _services = services;
+ _logger = logger;
+ }
+
+ public override bool IsConverter(PublishedPropertyType propertyType)
+ => MatchingEditors.Contains(propertyType.EditorAlias);
+
+ public override Type GetPropertyValueType(PublishedPropertyType propertyType)
+ => typeof (JArray);
+
+ public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
+ => PropertyCacheLevel.Element;
+
+ public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
+ {
+ if (source == null) return null;
+ var sourceString = source.ToString();
+
+ if (sourceString.DetectIsJson())
+ {
+ try
+ {
+ var obj = JsonConvert.DeserializeObject(sourceString);
+ //update the internal links if we have a context
+ if (UmbracoContext.Current != null)
+ {
+ var helper = new UmbracoHelper(_umbracoContextAccessor.UmbracoContext, _services);
+ foreach (var a in obj)
+ {
+ var type = a.Value("type");
+ if (type.IsNullOrWhiteSpace() == false)
+ {
+ if (type == "internal")
+ {
+ switch (propertyType.EditorAlias)
+ {
+ case Constants.PropertyEditors.Aliases.RelatedLinks:
+ var strLinkId = a.Value("link");
+ var udiAttempt = strLinkId.TryConvertTo();
+ if (udiAttempt)
+ {
+ var content = helper.PublishedContent(udiAttempt.Result);
+ if (content == null) break;
+ a["link"] = helper.Url(content.Id);
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ return obj;
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Could not parse the string '{Json}' to a json object", sourceString);
+ }
+ }
+
+ //it's not json, just return the string
+ return sourceString;
+ }
+
+ public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object source, bool preview)
+ {
+ if (source == null) return null;
+ var sourceString = source.ToString();
+
+ if (sourceString.DetectIsJson())
+ {
+ try
+ {
+ var obj = JsonConvert.DeserializeObject(sourceString);
+
+ var d = new XmlDocument();
+ var e = d.CreateElement("links");
+ d.AppendChild(e);
+
+ foreach (dynamic link in obj)
+ {
+ var ee = d.CreateElement("link");
+ ee.SetAttribute("title", link.title);
+ ee.SetAttribute("link", link.link);
+ ee.SetAttribute("type", link.type);
+ ee.SetAttribute("newwindow", link.newWindow);
+
+ e.AppendChild(ee);
+ }
+
+ return d.CreateNavigator();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Could not parse the string '{Json}' to a json object", sourceString);
+ }
+ }
+
+ //it's not json, just return the string
+ return sourceString;
+ }
+ }
+}
diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksValueConverter.cs
new file mode 100644
index 0000000000..983d122a83
--- /dev/null
+++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/RelatedLinksValueConverter.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.Xml;
+using Newtonsoft.Json;
+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.Web.Models;
+using Umbraco.Web.PublishedCache;
+
+namespace Umbraco.Web.PropertyEditors.ValueConverters
+{
+ ///
+ /// The related links property value converter.
+ ///
+ [DefaultPropertyValueConverter(typeof(RelatedLinksLegacyValueConverter), typeof(JsonValueConverter))]
+ public class RelatedLinksValueConverter : PropertyValueConverterBase
+ {
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+ private readonly IUmbracoContextAccessor _umbracoContextAccessor;
+ private readonly ILogger _logger;
+
+ public RelatedLinksValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IUmbracoContextAccessor umbracoContextAccessor, ILogger logger)
+ {
+ _publishedSnapshotAccessor = publishedSnapshotAccessor;
+ _umbracoContextAccessor = umbracoContextAccessor;
+ _logger = logger;
+ }
+
+ ///
+ /// Checks if this converter can convert the property editor and registers if it can.
+ ///
+ ///
+ /// The property type.
+ ///
+ ///
+ /// The .
+ ///
+ public override bool IsConverter(PublishedPropertyType propertyType)
+ => propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.RelatedLinks);
+
+ public override Type GetPropertyValueType(PublishedPropertyType propertyType)
+ => typeof (JArray);
+
+ public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType)
+ => PropertyCacheLevel.Element;
+
+ public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview)
+ {
+ if (source == null) return null;
+ var sourceString = source.ToString();
+
+ var relatedLinksData = JsonConvert.DeserializeObject>(sourceString);
+ var relatedLinks = new List();
+
+ foreach (var linkData in relatedLinksData)
+ {
+ var relatedLink = new RelatedLink
+ {
+ Caption = linkData.Caption,
+ NewWindow = linkData.NewWindow,
+ IsInternal = linkData.IsInternal,
+ Type = linkData.Type,
+ Link = linkData.Link
+ };
+
+ int contentId;
+ if (int.TryParse(relatedLink.Link, out contentId))
+ {
+ relatedLink.Id = contentId;
+ relatedLink = CreateLink(relatedLink);
+ }
+ else
+ {
+ var strLinkId = linkData.Link;
+ var udiAttempt = strLinkId.TryConvertTo();
+ if (udiAttempt.Success && udiAttempt.Result != null)
+ {
+ var content = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(udiAttempt.Result.Guid);
+ if (content != null)
+ {
+ relatedLink.Id = content.Id;
+ relatedLink = CreateLink(relatedLink);
+ relatedLink.Content = content;
+ }
+ }
+ }
+
+ if (relatedLink.IsDeleted == false)
+ {
+ relatedLinks.Add(relatedLink);
+ }
+ else
+ {
+ _logger.Warn("Related Links value converter skipped a link as the node has been unpublished/deleted (Internal Link NodeId: {RelatedLinkNodeId}, Link Caption: '{RelatedLinkCaption}')", relatedLink.Link, relatedLink.Caption);
+ }
+ }
+
+ return new RelatedLinks(relatedLinks, sourceString);
+ }
+
+ private RelatedLink CreateLink(RelatedLink link)
+ {
+ var umbracoContext = _umbracoContextAccessor.UmbracoContext;
+
+ if (link.IsInternal && link.Id != null)
+ {
+ if (umbracoContext == null)
+ return null;
+
+ var urlProvider = umbracoContext.UrlProvider;
+
+ link.Link = urlProvider.GetUrl((int)link.Id);
+ if (link.Link.Equals("#"))
+ {
+ link.IsDeleted = true;
+ link.Link = link.Id.ToString();
+ }
+ else
+ {
+ link.IsDeleted = false;
+ }
+ }
+
+ return link;
+ }
+
+ public override object ConvertIntermediateToXPath(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
+ {
+ if (inter == null) return null;
+ var sourceString = inter.ToString();
+
+ if (sourceString.DetectIsJson())
+ {
+ try
+ {
+ var obj = JsonConvert.DeserializeObject(sourceString);
+
+ var d = new XmlDocument();
+ var e = d.CreateElement("links");
+ d.AppendChild(e);
+
+ foreach (dynamic link in obj)
+ {
+ var ee = d.CreateElement("link");
+ ee.SetAttribute("title", link.title);
+ ee.SetAttribute("link", link.link);
+ ee.SetAttribute("type", link.type);
+ ee.SetAttribute("newwindow", link.newWindow);
+
+ e.AppendChild(ee);
+ }
+
+ return d.CreateNavigator();
+ }
+ catch (Exception ex)
+ {
+ _logger.Error(ex, "Could not parse the string {Json} to a json object", sourceString);
+ }
+ }
+
+ //it's not json, just return the string
+ return sourceString;
+ }
+ }
+}
diff --git a/src/Umbraco.Web/RelatedLinksTypeConverter.cs b/src/Umbraco.Web/RelatedLinksTypeConverter.cs
new file mode 100644
index 0000000000..647959b920
--- /dev/null
+++ b/src/Umbraco.Web/RelatedLinksTypeConverter.cs
@@ -0,0 +1,98 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+using Umbraco.Core;
+using Umbraco.Core.Logging;
+using Umbraco.Core.Composing;
+using Umbraco.Web.Models;
+
+namespace Umbraco.Web
+{
+ public class RelatedLinksTypeConverter : TypeConverter
+ {
+ private readonly UmbracoHelper _umbracoHelper;
+
+ public RelatedLinksTypeConverter(UmbracoHelper umbracoHelper)
+ {
+ _umbracoHelper = umbracoHelper;
+ }
+
+ public RelatedLinksTypeConverter()
+ {
+
+ }
+
+ private static readonly Type[] ConvertableTypes = new[]
+ {
+ typeof(JArray)
+ };
+
+ public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
+ {
+ return ConvertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType))
+ || base.CanConvertFrom(context, destinationType);
+ }
+
+ public override object ConvertTo(
+ ITypeDescriptorContext context,
+ CultureInfo culture,
+ object value,
+ Type destinationType)
+ {
+ var relatedLinks = value as RelatedLinks;
+ if (relatedLinks == null)
+ return null;
+
+ if (TypeHelper.IsTypeAssignableFrom(destinationType))
+ {
+ // Conversion to JArray taken from old value converter
+
+ var obj = JsonConvert.DeserializeObject(relatedLinks.PropertyData);
+
+ var umbracoHelper = GetUmbracoHelper();
+
+ //update the internal links if we have a context
+ if (umbracoHelper != null)
+ {
+ foreach (var a in obj)
+ {
+ var type = a.Value("type");
+ if (type.IsNullOrWhiteSpace() == false)
+ {
+ if (type == "internal")
+ {
+ var linkId = a.Value("link");
+ var link = umbracoHelper.Url(linkId);
+ a["link"] = link;
+ }
+ }
+ }
+ }
+ return obj;
+
+ }
+
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+
+ private UmbracoHelper GetUmbracoHelper()
+ {
+ if (_umbracoHelper != null)
+ return _umbracoHelper;
+
+ if (UmbracoContext.Current == null)
+ {
+ Current.Logger.Warn("Cannot create an UmbracoHelper the UmbracoContext is null");
+ return null;
+ }
+
+ //DO NOT assign to _umbracoHelper variable, this is a singleton class and we cannot assign this based on an UmbracoHelper which is request based
+ return new UmbracoHelper(UmbracoContext.Current, Current.Services);
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index 64a60d824a..6736f7512b 100755
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -404,6 +404,10 @@
+
+
+
+
@@ -449,6 +453,9 @@
+
+
+
@@ -464,6 +471,7 @@
+
@@ -527,6 +535,7 @@
+
@@ -851,6 +860,7 @@
+