-
{{model.label}} Maximum %0% characters, %1% too many.
-
Maximum %0% characters, %1% too many.
+
+
{{model.label}} Maximum %0% characters, %1% too many.
+
Maximum %0% characters, %1% too many.
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js
index b2fbee4b36..bd990961bf 100644
--- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js
@@ -26,7 +26,7 @@ function RelationTypeCreateController($scope, $location, relationTypeResource, n
}
function createRelationType() {
- if (formHelper.submitForm({ scope: $scope, formCtrl: this.createRelationTypeForm, statusMessage: "Creating relation type..." })) {
+ if (formHelper.submitForm({ scope: $scope, formCtrl: $scope.createRelationTypeForm, statusMessage: "Creating relation type..." })) {
var node = $scope.currentNode;
relationTypeResource.create(vm.relationType).then(function (data) {
@@ -36,12 +36,12 @@ function RelationTypeCreateController($scope, $location, relationTypeResource, n
var currentPath = node.path ? node.path : "-1";
navigationService.syncTree({ tree: "relationTypes", path: currentPath + "," + data, forceReload: true, activate: true });
- formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm });
+ formHelper.resetForm({ scope: $scope, formCtrl: $scope.createRelationTypeForm });
var currentSection = appState.getSectionState("currentSection");
$location.path("/" + currentSection + "/relationTypes/edit/" + data);
}, function (err) {
- formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm, hasErrors: true });
+ formHelper.resetForm({ scope: $scope, formCtrl: $scope.createRelationTypeForm, hasErrors: true });
if (err.data && err.data.message) {
notificationsService.error(err.data.message);
navigationService.hideMenu();
diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js
index d0f59c110f..c039e59ee1 100644
--- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js
+++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js
@@ -100,14 +100,7 @@ module.exports = function (config) {
// - PhantomJS
// - IE (only Windows)
// CLI --browsers Chrome,Firefox,Safari
- browsers: ['ChromeHeadless'],
-
- customLaunchers: {
- ChromeDebugging: {
- base: 'Chrome',
- flags: ['--remote-debugging-port=9333']
- }
- },
+ browsers: ['jsdom'],
// allow waiting a bit longer, some machines require this
@@ -123,11 +116,9 @@ module.exports = function (config) {
plugins: [
require('karma-jasmine'),
- require('karma-phantomjs-launcher'),
- require('karma-chrome-launcher'),
+ require('karma-jsdom-launcher'),
require('karma-junit-reporter'),
require('karma-spec-reporter')
-
],
// the default configuration
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
index dff3615883..9610d7ab57 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml
@@ -1845,8 +1845,9 @@ Mange hilsner fra Umbraco robotten
Tilføj speciel visning
Tilføj instillinger
Overskriv label form
-
%0%.]]>
-
Indhold der benytter sig af denne blok vil gå bort.
+
%0%.]]>
+
%0%.]]>
+
Indholdet vil stadigt eksistere, men redigering af dette indhold vil ikke være muligt. Indholdet vil blive vist som ikke understøttet indhold.
Billede
Tilføj billede
@@ -1856,6 +1857,8 @@ Mange hilsner fra Umbraco robotten
Avanceret
Skjuld indholds editoren
Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem?
+
Annuller oprettelse?
+
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
index 13f076a492..a7479a3392 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml
@@ -2475,8 +2475,9 @@ To manage your website, simply open the Umbraco back office and start adding con
Add custom view
Add settings
Overwrite label template
-
%0%.]]>
-
Content using this block will be lost.
+
%0%.]]>
+
%0%.]]>
+
The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content.
Thumbnail
Add thumbnail
@@ -2486,6 +2487,8 @@ To manage your website, simply open the Umbraco back office and start adding con
Advanced
Force hide content editor
You have made changes to this content. Are you sure you want to discard them?
+
Discard creation?
+
What are Content Templates?
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
index c44b7d15a8..d00af6fe5e 100644
--- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
+++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml
@@ -2497,8 +2497,9 @@ To manage your website, simply open the Umbraco back office and start adding con
Add custom view
Add settings
Overwrite label template
-
%0%.]]>
-
Content using this block will be lost.
+
%0%.]]>
+
%0%.]]>
+
The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content.
Thumbnail
Add thumbnail
@@ -2508,6 +2509,8 @@ To manage your website, simply open the Umbraco back office and start adding con
Advanced
Force hide content editor
You have made changes to this content. Are you sure you want to discard them?
+
Discard creation?
+
What are Content Templates?
diff --git a/src/Umbraco.Web/Compose/BlockEditorComponent.cs b/src/Umbraco.Web/Compose/BlockEditorComponent.cs
new file mode 100644
index 0000000000..a8b4cfb8ca
--- /dev/null
+++ b/src/Umbraco.Web/Compose/BlockEditorComponent.cs
@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Umbraco.Core;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Models.Blocks;
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Web.Compose
+{
+ ///
+ /// A component for Block editors used to bind to events
+ ///
+ public class BlockEditorComponent : IComponent
+ {
+ private ComplexPropertyEditorContentEventHandler _handler;
+ private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter();
+
+ public void Initialize()
+ {
+ _handler = new ComplexPropertyEditorContentEventHandler(
+ Constants.PropertyEditors.Aliases.BlockList,
+ ReplaceBlockListUdis);
+ }
+
+ public void Terminate() => _handler?.Dispose();
+
+ private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis)
+ {
+ // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process
+ if (onlyMissingUdis) return rawJson;
+
+ return ReplaceBlockListUdis(rawJson, null);
+ }
+
+ // internal for tests
+ internal string ReplaceBlockListUdis(string rawJson, Func
createGuid = null)
+ {
+ // used so we can test nicely
+ if (createGuid == null)
+ createGuid = () => Guid.NewGuid();
+
+ if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson())
+ return rawJson;
+
+ // Parse JSON
+ // This will throw a FormatException if there are null UDIs (expected)
+ var blockListValue = _converter.Deserialize(rawJson);
+
+ UpdateBlockListRecursively(blockListValue, createGuid);
+
+ return JsonConvert.SerializeObject(blockListValue.BlockValue);
+ }
+
+ private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid)
+ {
+ var oldToNew = new Dictionary();
+ MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid);
+ MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid);
+
+ for (var i = 0; i < blockListData.References.Count; i++)
+ {
+ var reference = blockListData.References[i];
+ var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out var contentMap);
+ Udi settingsMap = null;
+ var hasSettingsMap = reference.SettingsUdi != null && oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap);
+
+ if (hasContentMap)
+ {
+ // replace the reference
+ blockListData.References.RemoveAt(i);
+ blockListData.References.Insert(i, new ContentAndSettingsReference(contentMap, hasSettingsMap ? settingsMap : null));
+ }
+ }
+
+ // build the layout with the new UDIs
+ var layout = (JArray)blockListData.Layout;
+ layout.Clear();
+ foreach (var reference in blockListData.References)
+ {
+ layout.Add(JObject.FromObject(new BlockListLayoutItem
+ {
+ ContentUdi = reference.ContentUdi,
+ SettingsUdi = reference.SettingsUdi
+ }));
+ }
+
+
+ RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid);
+ RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid);
+ }
+
+ private void RecursePropertyValues(IEnumerable blockData, Func createGuid)
+ {
+ foreach (var data in blockData)
+ {
+ // check if we need to recurse (make a copy of the dictionary since it will be modified)
+ foreach (var propertyAliasToBlockItemData in new Dictionary(data.RawPropertyValues))
+ {
+ if (propertyAliasToBlockItemData.Value is JToken jtoken)
+ {
+ if (ProcessJToken(jtoken, createGuid, out var result))
+ {
+ // need to re-save this back to the RawPropertyValues
+ data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result;
+ }
+ }
+ else
+ {
+ var asString = propertyAliasToBlockItemData.Value?.ToString();
+
+ if (asString != null && asString.DetectIsJson())
+ {
+ // this gets a little ugly because there could be some other complex editor that contains another block editor
+ // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor
+ // of our type
+ var json = JToken.Parse(asString);
+ if (ProcessJToken(json, createGuid, out var result))
+ {
+ // need to re-save this back to the RawPropertyValues
+ data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private bool ProcessJToken(JToken json, Func createGuid, out JToken result)
+ {
+ var updated = false;
+ result = json;
+
+ // select all tokens (flatten)
+ var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList();
+ foreach (var prop in allProperties)
+ {
+ if (prop.Name == Constants.PropertyEditors.Aliases.BlockList)
+ {
+ // get it's parent 'layout' and it's parent's container
+ var layout = prop.Parent?.Parent as JProperty;
+ if (layout != null && layout.Parent is JObject layoutJson)
+ {
+ // recurse
+ var blockListValue = _converter.ConvertFrom(layoutJson);
+ UpdateBlockListRecursively(blockListValue, createGuid);
+
+ // set new value
+ if (layoutJson.Parent != null)
+ {
+ // we can replace the object
+ layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue));
+ updated = true;
+ }
+ else
+ {
+ // if there is no parent it means that this json property was the root, in which case we just return
+ result = JObject.FromObject(blockListValue.BlockValue);
+ return true;
+ }
+ }
+ }
+ else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey")
+ {
+ // this is an arbitrary property that could contain a nested complex editor
+ var propVal = prop.Value?.ToString();
+ // check if this might contain a nested Block Editor
+ if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList))
+ {
+ if (_converter.TryDeserialize(propVal, out var nestedBlockData))
+ {
+ // recurse
+ UpdateBlockListRecursively(nestedBlockData, createGuid);
+ // set the value to the updated one
+ prop.Value = JObject.FromObject(nestedBlockData.BlockValue);
+ updated = true;
+ }
+ }
+ }
+ }
+
+ return updated;
+ }
+
+ private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, Func createGuid)
+ {
+ foreach (var data in blockData)
+ {
+ // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here
+ if (data.Udi == null)
+ throw new InvalidOperationException("Block data cannot contain a null UDI");
+
+ // replace the UDIs
+ var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid());
+ oldToNew[data.Udi] = newUdi;
+ data.Udi = newUdi;
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Web/Compose/BlockEditorComposer.cs b/src/Umbraco.Web/Compose/BlockEditorComposer.cs
new file mode 100644
index 0000000000..e281bcb19f
--- /dev/null
+++ b/src/Umbraco.Web/Compose/BlockEditorComposer.cs
@@ -0,0 +1,12 @@
+using Umbraco.Core;
+using Umbraco.Core.Composing;
+
+namespace Umbraco.Web.Compose
+{
+ ///
+ /// A composer for Block editors to run a component
+ ///
+ [RuntimeLevel(MinLevel = RuntimeLevel.Run)]
+ public class BlockEditorComposer : ComponentComposer, ICoreComposer
+ { }
+}
diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs
index 5794a2734e..633e814bd9 100644
--- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs
+++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs
@@ -6,67 +6,32 @@ using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
+using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Web.PropertyEditors;
namespace Umbraco.Web.Compose
{
+
+ ///
+ /// A component for NestedContent used to bind to events
+ ///
public class NestedContentPropertyComponent : IComponent
{
+ private ComplexPropertyEditorContentEventHandler _handler;
+
public void Initialize()
{
- ContentService.Copying += ContentService_Copying;
- ContentService.Saving += ContentService_Saving;
+ _handler = new ComplexPropertyEditorContentEventHandler(
+ Constants.PropertyEditors.Aliases.NestedContent,
+ CreateNestedContentKeys);
}
- private void ContentService_Copying(IContentService sender, CopyEventArgs e)
- {
- // When a content node contains nested content property
- // Check if the copied node contains a nested content
- var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent);
- UpdateNestedContentProperties(nestedContentProps, false);
- }
+ public void Terminate() => _handler?.Dispose();
- private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e)
- {
- // One or more content nodes could be saved in a bulk publish
- foreach (var entity in e.SavedEntities)
- {
- // When a content node contains nested content property
- // Check if the copied node contains a nested content
- var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent);
- UpdateNestedContentProperties(nestedContentProps, true);
- }
- }
+ private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null);
- public void Terminate()
- {
- ContentService.Copying -= ContentService_Copying;
- ContentService.Saving -= ContentService_Saving;
- }
-
- private void UpdateNestedContentProperties(IEnumerable nestedContentProps, bool onlyMissingKeys)
- {
- // Each NC Property on a doctype
- foreach (var nestedContentProp in nestedContentProps)
- {
- // A NC Prop may have one or more values due to cultures
- var propVals = nestedContentProp.Values;
- foreach (var cultureVal in propVals)
- {
- // Remove keys from published value & any nested NC's
- var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys);
- cultureVal.PublishedValue = updatedPublishedVal;
-
- // Remove keys from edited/draft value & any nested NC's
- var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys);
- cultureVal.EditedValue = updatedEditedVal;
- }
- }
- }
-
-
// internal for tests
internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null)
{
@@ -77,7 +42,7 @@ namespace Umbraco.Web.Compose
if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson())
return rawJson;
- // Parse JSON
+ // Parse JSON
var complexEditorValue = JToken.Parse(rawJson);
UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid);
@@ -98,7 +63,6 @@ namespace Umbraco.Web.Compose
{
// get it's sibling 'key' property
var ncKeyVal = prop.Parent["key"] as JValue;
- // TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys?
if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null))
{
// create or replace
diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs
index 4c9d9dee1c..8e4cfbfffc 100644
--- a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs
+++ b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs
@@ -3,6 +3,9 @@ using Umbraco.Core.Composing;
namespace Umbraco.Web.Compose
{
+ ///
+ /// A composer for nested content to run a component
+ ///
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class NestedContentPropertyComposer : ComponentComposer, ICoreComposer
{ }
diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs
index b74a20c27c..a998a172fc 100644
--- a/src/Umbraco.Web/Install/InstallHelper.cs
+++ b/src/Umbraco.Web/Install/InstallHelper.cs
@@ -30,6 +30,18 @@ namespace Umbraco.Web.Install
private readonly IInstallationService _installationService;
private InstallationType? _installationType;
+
+ [Obsolete("Use the constructor with IInstallationService injected.")]
+ public InstallHelper(
+ IUmbracoContextAccessor umbracoContextAccessor,
+ DatabaseBuilder databaseBuilder,
+ ILogger logger,
+ IGlobalSettings globalSettings)
+ : this(umbracoContextAccessor, databaseBuilder, logger, globalSettings, Current.Factory.GetInstance())
+ {
+
+ }
+
public InstallHelper(IUmbracoContextAccessor umbracoContextAccessor,
DatabaseBuilder databaseBuilder,
ILogger logger, IGlobalSettings globalSettings, IInstallationService installationService)
diff --git a/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs b/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs
index c9f15e2e2f..641cc8e42a 100644
--- a/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs
+++ b/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs
@@ -7,7 +7,7 @@ namespace Umbraco.Web.PropertyEditors
///
public class TextboxConfiguration
{
- [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 500 character limit")]
+ [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")]
public int? MaxChars { get; set; }
}
}
diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
index 0c90a41fbd..f46c118174 100644
--- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
+++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
@@ -120,7 +120,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
settingsData = null;
}
- var layoutRef = new BlockListItem(contentGuidUdi, contentData, settingGuidUdi, settingsData);
+ var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsData?.GetType() ?? typeof(IPublishedElement));
+ var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData);
+
layout.Add(layoutRef);
}
diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj
index b2068290f6..33051671ee 100644
--- a/src/Umbraco.Web/Umbraco.Web.csproj
+++ b/src/Umbraco.Web/Umbraco.Web.csproj
@@ -133,6 +133,8 @@
+
+