Merge branch 'v10/dev' into v10/contrib

# Conflicts:
#	src/Umbraco.Core/EmbeddedResources/Lang/en.xml
#	src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
This commit is contained in:
Sebastiaan Janssen
2022-09-19 16:48:36 +02:00
202 changed files with 5381 additions and 2298 deletions

View File

@@ -41,17 +41,7 @@ public class DeepCloneableList<T> : List<T>, IDeepCloneable, IRememberBeingDirty
// we are cloning once, so create a new list in none mode
// and deep clone all items into it
var newList = new DeepCloneableList<T>(ListCloneBehavior.None);
foreach (T item in this)
{
if (item is IDeepCloneable dc)
{
newList.Add((T)dc.DeepClone());
}
else
{
newList.Add(item);
}
}
DeepCloneHelper.CloneListItems<DeepCloneableList<T>, T>(this, newList);
return newList;
case ListCloneBehavior.None:
@@ -60,17 +50,7 @@ public class DeepCloneableList<T> : List<T>, IDeepCloneable, IRememberBeingDirty
case ListCloneBehavior.Always:
// always clone to new list
var newList2 = new DeepCloneableList<T>(ListCloneBehavior.Always);
foreach (T item in this)
{
if (item is IDeepCloneable dc)
{
newList2.Add((T)dc.DeepClone());
}
else
{
newList2.Add(item);
}
}
DeepCloneHelper.CloneListItems<DeepCloneableList<T>, T>(this, newList2);
return newList2;
default:

View File

@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Collections;
@@ -7,7 +8,7 @@ namespace Umbraco.Cms.Core.Collections;
/// Allows clearing all event handlers
/// </summary>
/// <typeparam name="TValue"></typeparam>
public class EventClearingObservableCollection<TValue> : ObservableCollection<TValue>, INotifyCollectionChanged
public class EventClearingObservableCollection<TValue> : ObservableCollection<TValue>, INotifyCollectionChanged, IDeepCloneable
{
// need to explicitly implement with event accessor syntax in order to override in order to to clear
// c# events are weird, they do not behave the same way as other c# things that are 'virtual',
@@ -39,4 +40,12 @@ public class EventClearingObservableCollection<TValue> : ObservableCollection<TV
/// Clears all event handlers for the <see cref="CollectionChanged" /> event
/// </summary>
public void ClearCollectionChangedEvents() => _changed = null;
public object DeepClone()
{
var clone = new EventClearingObservableCollection<TValue>();
DeepCloneHelper.CloneListItems<EventClearingObservableCollection<TValue>, TValue>(this, clone);
return clone;
}
}

View File

@@ -157,6 +157,7 @@ public class ContentSettings
internal const bool StaticHideBackOfficeLogo = false;
internal const bool StaticDisableDeleteWhenReferenced = false;
internal const bool StaticDisableUnpublishWhenReferenced = false;
internal const bool StaticAllowEditInvariantFromNonDefault = false;
/// <summary>
/// Gets or sets a value for the content notification settings.
@@ -242,4 +243,10 @@ public class ContentSettings
/// Get or sets the model representing the global content version cleanup policy
/// </summary>
public ContentVersionCleanupPolicySettings ContentVersionCleanupPolicy { get; set; } = new();
/// <summary>
/// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation.
/// </summary>
[DefaultValue(StaticAllowEditInvariantFromNonDefault)]
public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault;
}

View File

@@ -86,9 +86,10 @@ public class SecuritySettings
[DefaultValue(StaticUserBypassTwoFactorForExternalLogins)]
public bool UserBypassTwoFactorForExternalLogins { get; set; } = StaticUserBypassTwoFactorForExternalLogins;
/// <summary>
/// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation.
/// </summary>
[DefaultValue(StaticAllowEditInvariantFromNonDefault)]
public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault;
/// <summary>
/// Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation.
/// </summary>
[Obsolete("Use ContentSettings.AllowEditFromInvariant instead")]
[DefaultValue(StaticAllowEditInvariantFromNonDefault)]
public bool AllowEditInvariantFromNonDefault { get; set; } = StaticAllowEditInvariantFromNonDefault;
}

View File

@@ -63,6 +63,11 @@ public static partial class Constants
public const string AreaToken = "area";
}
public static class AttributeRouting
{
public const string BackOfficeToken = "umbracoBackOffice";
}
public static class EmailTypes
{
public const string HealthCheck = "HealthCheck";

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration;
@@ -104,6 +105,20 @@ public static partial class UmbracoBuilderExtensions
builder.Services.Configure<RequestHandlerSettings>(options => options.MergeReplacements(builder.Config));
// TODO: Remove this in V12
// This is to make the move of the AllowEditInvariantFromNonDefault setting from SecuritySettings to ContentSettings backwards compatible
// If there is a value in security settings, but no value in content setting we'll use that value, otherwise content settings always wins.
builder.Services.Configure<ContentSettings>(settings =>
{
var securitySettingsValue = builder.Config.GetSection($"{Constants.Configuration.ConfigSecurity}").GetValue<bool?>(nameof(SecuritySettings.AllowEditInvariantFromNonDefault));
var contentSettingsValue = builder.Config.GetSection($"{Constants.Configuration.ConfigContent}").GetValue<bool?>(nameof(ContentSettings.AllowEditInvariantFromNonDefault));
if (securitySettingsValue is not null && contentSettingsValue is null)
{
settings.AllowEditInvariantFromNonDefault = securitySettingsValue.Value;
}
});
return builder;
}
}

View File

@@ -324,7 +324,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
Services.AddUnique<IPropertyTypeUsageService, PropertyTypeUsageService>();
Services.AddUnique<IDataTypeUsageService, DataTypeUsageService>();
Services.AddUnique<ICultureImpactFactory, CultureImpactFactory>();
Services.AddUnique<ICultureImpactFactory>(provider => new CultureImpactFactory(provider.GetRequiredService<IOptionsMonitor<ContentSettings>>()));
}
}
}

View File

@@ -1327,11 +1327,8 @@ public static class StringExtensions
/// <param name="path"></param>
/// <returns></returns>
// From: http://stackoverflow.com/a/35046453/5018
public static bool IsFullPath(this string path) =>
string.IsNullOrWhiteSpace(path) == false
&& path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1
&& Path.IsPathRooted(path)
&& Path.GetPathRoot(path)?.Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false;
// Updated from .NET 2.1+: https://stackoverflow.com/a/58250915
public static bool IsFullPath(this string path) => Path.IsPathFullyQualified(path);
// FORMAT STRINGS

View File

@@ -53,8 +53,7 @@ public abstract class IOHelper : IIOHelper
throw new ArgumentNullException(nameof(path));
}
// Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1
if (IsPathFullyQualified(path))
if (path.IsFullPath())
{
return path;
}
@@ -231,13 +230,7 @@ public abstract class IOHelper : IIOHelper
: CleanFolderResult.Success();
}
/// <summary>
/// Returns true if the path has a root, and is considered fully qualified for the OS it is on
/// See
/// https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281
/// for the .NET Standard 2.1 version of this
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path is fully qualified, false otherwise</returns>
public abstract bool IsPathFullyQualified(string path);
[Obsolete("Use Path.IsPathFullyQualified instead. This will be removed in Umbraco 13.")]
public virtual bool IsPathFullyQualified(string path) => Path.IsPathFullyQualified(path);
}

View File

@@ -9,8 +9,6 @@ public class IOHelperLinux : IOHelper
{
}
public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
public override bool PathStartsWith(string path, string root, params char[] separators)
{
// either it is identical to root,

View File

@@ -9,8 +9,6 @@ public class IOHelperOSX : IOHelper
{
}
public override bool IsPathFullyQualified(string path) => Path.IsPathRooted(path);
public override bool PathStartsWith(string path, string root, params char[] separators)
{
// either it is identical to root,

View File

@@ -9,35 +9,6 @@ public class IOHelperWindows : IOHelper
{
}
public override bool IsPathFullyQualified(string path)
{
// TODO: This implementation is taken from the .NET Standard 2.1 implementation. We should switch to using Path.IsPathFullyQualified once we are on .NET Standard 2.1
if (path.Length < 2)
{
// It isn't fixed, it must be relative. There is no way to specify a fixed
// path with one character (or less).
return false;
}
if (path[0] == Path.DirectorySeparatorChar || path[0] == Path.AltDirectorySeparatorChar)
{
// There is no valid way to specify a relative path with two initial slashes or
// \? as ? isn't valid for drive relative paths and \??\ is equivalent to \\?\
return path[1] == '?' || path[1] == Path.DirectorySeparatorChar ||
path[1] == Path.AltDirectorySeparatorChar;
}
// The only way to specify a fixed path that doesn't begin with two slashes
// is the drive, colon, slash format- i.e. C:\
return path.Length >= 3
&& path[1] == Path.VolumeSeparatorChar
&& (path[2] == Path.DirectorySeparatorChar || path[2] == Path.AltDirectorySeparatorChar)
// To match old behavior we'll check the drive character for validity as the path is technically
// not qualified if you don't have a valid drive. "=:\" is the "=" file's default data stream.
&& ((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z'));
}
public override bool PathStartsWith(string path, string root, params char[] separators)
{
// either it is identical to root,

View File

@@ -1,4 +1,4 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Install;

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Collections;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Serialization;
@@ -9,6 +9,7 @@ namespace Umbraco.Cms.Core.Install;
/// <summary>
/// An internal in-memory status tracker for the current installation
/// </summary>
[Obsolete("This will no longer be used with the new backoffice APi, instead all steps run in one go")]
public class InstallStatusTracker
{
private static ConcurrentHashSet<InstallTrackingItem> _steps = new();

View File

@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.
// Copyright (c) Umbraco.
// See LICENSE for more details.
using Umbraco.Cms.Core.Install.Models;
@@ -10,6 +10,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps;
/// <summary>
/// Represents a step in the installation that ensure all the required permissions on files and folders are correct.
/// </summary>
[Obsolete("Will be replace with a new step with the new backoffice")]
[InstallSetupStep(
InstallationType.NewInstall | InstallationType.Upgrade,
"Permissions",

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration;
@@ -9,6 +9,7 @@ using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Core.Install.InstallSteps;
[Obsolete("Will be replace with a new step with the new backoffice")]
[InstallSetupStep(
InstallationType.NewInstall | InstallationType.Upgrade,
"TelemetryIdConfiguration",

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Configuration;
using Umbraco.Cms.Core.Install.Models;
using Umbraco.Cms.Core.Semver;
using Umbraco.Cms.Core.Services;
@@ -8,6 +8,7 @@ namespace Umbraco.Cms.Core.Install.InstallSteps
/// <summary>
/// This step is purely here to show the button to commence the upgrade
/// </summary>
[Obsolete("Will be replace with a new step with the new backoffice")]
[InstallSetupStep(InstallationType.Upgrade, "Upgrade", "upgrade", 1, "Upgrading Umbraco to the latest and greatest version.")]
public class UpgradeStep : InstallSetupStep<object>
{

View File

@@ -11,6 +11,8 @@ public class DatabaseModel
[DataMember(Name = "providerName")]
public string? ProviderName { get; set; }
// TODO: Make this nullable in V11
// Server can be null, for instance when installing a SQLite database.
[DataMember(Name = "server")]
public string Server { get; set; } = null!;
@@ -18,10 +20,10 @@ public class DatabaseModel
public string DatabaseName { get; set; } = null!;
[DataMember(Name = "login")]
public string Login { get; set; } = null!;
public string? Login { get; set; }
[DataMember(Name = "password")]
public string Password { get; set; } = null!;
public string? Password { get; set; }
[DataMember(Name = "integratedAuth")]
public bool IntegratedAuth { get; set; }

View File

@@ -1,7 +1,8 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Install.Models;
[Obsolete("Will no longer be required with the new backoffice API")]
[DataContract(Name = "installInstructions", Namespace = "")]
public class InstallInstructions
{

View File

@@ -1,10 +1,11 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Install.Models;
/// <summary>
/// Returned to the UI for each installation step that is completed
/// </summary>
[Obsolete("Will no longer be required with the new backoffice API")]
[DataContract(Name = "result", Namespace = "")]
public class InstallProgressResultModel
{

View File

@@ -1,4 +1,4 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Install.Models;
@@ -6,6 +6,7 @@ namespace Umbraco.Cms.Core.Install.Models;
/// Model containing all the install steps for setting up the UI
/// </summary>
[DataContract(Name = "installSetup", Namespace = "")]
[Obsolete("Will no longer be required with the new backoffice API")]
public class InstallSetup
{
public InstallSetup()

View File

@@ -1,8 +1,9 @@
namespace Umbraco.Cms.Core.Install.Models;
namespace Umbraco.Cms.Core.Install.Models;
/// <summary>
/// The object returned from each installation step
/// </summary>
[Obsolete("Will no longer be required with the new backoffice API")]
public class InstallSetupResult
{
public InstallSetupResult()

View File

@@ -1,4 +1,4 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Install.Models;
@@ -7,6 +7,7 @@ namespace Umbraco.Cms.Core.Install.Models;
/// Model to give to the front-end to collect the information for each step
/// </summary>
[DataContract(Name = "step", Namespace = "")]
[Obsolete("Will be replaced with IInstallStep in the new backoffice API")]
public abstract class InstallSetupStep<T> : InstallSetupStep
{
/// <summary>
@@ -30,6 +31,7 @@ public abstract class InstallSetupStep<T> : InstallSetupStep
}
[DataContract(Name = "step", Namespace = "")]
[Obsolete("Will be replaced with IInstallStep in the new backoffice API")]
public abstract class InstallSetupStep
{
protected InstallSetupStep()

View File

@@ -1,5 +1,6 @@
namespace Umbraco.Cms.Core.Install.Models;
namespace Umbraco.Cms.Core.Install.Models;
[Obsolete("Will no longer be required with the use of IInstallStep in the new backoffice API")]
public sealed class InstallSetupStepAttribute : Attribute
{
public InstallSetupStepAttribute(InstallationType installTypeTarget, string name, string view, int serverOrder, string description)

View File

@@ -1,5 +1,6 @@
namespace Umbraco.Cms.Core.Install.Models;
namespace Umbraco.Cms.Core.Install.Models;
[Obsolete("Will no longer be required with the new backoffice API")]
public class InstallTrackingItem
{
public InstallTrackingItem(string name, int serverOrder)

View File

@@ -1,5 +1,6 @@
namespace Umbraco.Cms.Core.Install.Models;
[Obsolete("This will no longer be used with the new backoffice APi, install steps and upgrade steps is instead two different interfaces.")]
[Flags]
public enum InstallationType
{

View File

@@ -1,7 +1,8 @@
using System.Runtime.Serialization;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Install.Models;
[Obsolete("This is no longer used, instead PackageDefinition and InstalledPackage is used")]
[DataContract(Name = "package")]
public class Package
{

View File

@@ -3,6 +3,7 @@ using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Install.Models;
[Obsolete("Will no longer be required with the new backoffice API")]
[DataContract(Name = "user", Namespace = "")]
public class UserModel
{

View File

@@ -60,15 +60,8 @@ public abstract class ContentBase : TreeEntityBase, IContentBase
_contentTypeId = contentType.Id;
_properties = properties ?? throw new ArgumentNullException(nameof(properties));
_properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes);
// track all property types on this content type, these can never change during the lifetime of this single instance
// there is no real extra memory overhead of doing this since these property types are already cached on this object via the
// properties already.
AllPropertyTypes = new List<IPropertyType>(contentType.CompositionPropertyTypes);
}
internal IReadOnlyList<IPropertyType> AllPropertyTypes { get; }
[IgnoreDataMember]
public ISimpleContentType ContentType { get; private set; }
@@ -146,7 +139,6 @@ public abstract class ContentBase : TreeEntityBase, IContentBase
base.PerformDeepClone(clone);
var clonedContent = (ContentBase)clone;
// Need to manually clone this since it's not settable
clonedContent.ContentType = ContentType;

View File

@@ -212,4 +212,16 @@ public static class DeepCloneHelper
public bool IsList => GenericListType != null;
}
public static void CloneListItems<TList, TEntity>(TList source, TList target)
where TList : ICollection<TEntity>
{
target.Clear();
foreach (TEntity entity in source)
{
target.Add(entity is IDeepCloneable deepCloneableEntity
? (TEntity)deepCloneableEntity.DeepClone()
: entity);
}
}
}

View File

@@ -85,6 +85,8 @@ public abstract class BeingDirtyBase : IRememberBeingDirty
/// </summary>
public event PropertyChangedEventHandler? PropertyChanged;
protected void ClearPropertyChangedEvents() => PropertyChanged = null;
/// <summary>
/// Registers that a property has changed.
/// </summary>

View File

@@ -20,7 +20,7 @@ public class ContentVariantMapper
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
private readonly IContentService _contentService;
private readonly IUserService _userService;
private SecuritySettings _securitySettings;
private ContentSettings _contentSettings;
public ContentVariantMapper(
ILocalizationService localizationService,
@@ -28,17 +28,36 @@ public class ContentVariantMapper
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IContentService contentService,
IUserService userService,
IOptionsMonitor<SecuritySettings> securitySettings)
IOptionsMonitor<ContentSettings> contentSettings)
{
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
_localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService));
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_contentService = contentService;
_userService = userService;
_securitySettings = securitySettings.CurrentValue;
securitySettings.OnChange(settings => _securitySettings = settings);
_contentSettings = contentSettings.CurrentValue;
contentSettings.OnChange(settings => _contentSettings = settings);
}
[Obsolete("Use constructor that takes all parameters instead")]
public ContentVariantMapper(
ILocalizationService localizationService,
ILocalizedTextService localizedTextService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
IContentService contentService,
IUserService userService,
IOptionsMonitor<SecuritySettings> securitySettings)
: this(
localizationService,
localizedTextService,
backOfficeSecurityAccessor,
contentService,
userService,
StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<ContentSettings>>())
{
}
[Obsolete("Use constructor that takes all parameters instead")]
public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService)
: this(
localizationService,
@@ -244,7 +263,7 @@ public class ContentVariantMapper
if (variantDisplay.Language is null)
{
var defaultLanguageId = _localizationService.GetDefaultLanguageId();
if (_securitySettings.AllowEditInvariantFromNonDefault || (defaultLanguageId.HasValue && group.HasAccessToLanguage(defaultLanguageId.Value)))
if (_contentSettings.AllowEditInvariantFromNonDefault || (defaultLanguageId.HasValue && group.HasAccessToLanguage(defaultLanguageId.Value)))
{
hasAccess = true;
}

View File

@@ -1,6 +1,8 @@
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Web.Common.DependencyInjection;
namespace Umbraco.Cms.Core.Models.Mapping;
@@ -14,8 +16,9 @@ public class DictionaryMapDefinition : IMapDefinition
private readonly ILocalizationService _localizationService;
[Obsolete("Use the constructor with the CommonMapper")]
public DictionaryMapDefinition(ILocalizationService localizationService) =>
_localizationService = localizationService;
public DictionaryMapDefinition(ILocalizationService localizationService) : this(localizationService, StaticServiceProvider.Instance.GetRequiredService<CommonMapper>())
{
}
public DictionaryMapDefinition(ILocalizationService localizationService, CommonMapper commonMapper)
{

View File

@@ -283,12 +283,7 @@ public class PropertyType : EntityBase, IPropertyType, IEquatable<PropertyType>
base.PerformDeepClone(clone);
var clonedEntity = (PropertyType)clone;
// need to manually assign the Lazy value as it will not be automatically mapped
if (PropertyGroupId != null)
{
clonedEntity._propertyGroupId = new Lazy<int>(() => PropertyGroupId.Value);
}
clonedEntity.ClearPropertyChangedEvents();
}
/// <summary>

View File

@@ -10,7 +10,7 @@ namespace Umbraco.Cms.Core.Models;
public class PublicAccessEntry : EntityBase
{
private readonly List<Guid> _removedRules = new();
private readonly EventClearingObservableCollection<PublicAccessRule> _ruleCollection;
private EventClearingObservableCollection<PublicAccessRule> _ruleCollection;
private int _loginNodeId;
private int _noAccessNodeId;
private int _protectedNodeId;
@@ -144,11 +144,13 @@ public class PublicAccessEntry : EntityBase
var cloneEntity = (PublicAccessEntry)clone;
if (cloneEntity._ruleCollection != null)
{
cloneEntity._ruleCollection.ClearCollectionChangedEvents(); // clear this event handler if any
cloneEntity._ruleCollection.CollectionChanged +=
cloneEntity.RuleCollection_CollectionChanged; // re-assign correct event handler
}
// clear this event handler if any
cloneEntity._ruleCollection.ClearCollectionChangedEvents();
// clone the rule collection explicitly
cloneEntity._ruleCollection = (EventClearingObservableCollection<PublicAccessRule>)_ruleCollection.DeepClone();
// re-assign correct event handler
cloneEntity._ruleCollection.CollectionChanged += cloneEntity.RuleCollection_CollectionChanged;
}
}

View File

@@ -21,7 +21,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
"Content Picker",
"contentpicker",
ValueType = ValueTypes.String,
Group = Constants.PropertyEditors.Groups.Pickers)]
Group = Constants.PropertyEditors.Groups.Pickers,
ValueEditorIsReusable = true)]
public class ContentPickerPropertyEditor : DataEditor
{
private readonly IEditorConfigurationParser _editorConfigurationParser;

View File

@@ -20,6 +20,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
[DataContract]
public class DataEditor : IDataEditor
{
private readonly bool _canReuseValueEditor;
private IDataValueEditor? _reusableValueEditor;
private IDictionary<string, object>? _defaultConfiguration;
/// <summary>
@@ -48,6 +50,8 @@ public class DataEditor : IDataEditor
Icon = Attribute.Icon;
Group = Attribute.Group;
IsDeprecated = Attribute.IsDeprecated;
_canReuseValueEditor = Attribute.ValueEditorIsReusable;
}
/// <summary>
@@ -118,18 +122,14 @@ public class DataEditor : IDataEditor
/// instance is returned. Otherwise, a new instance is created by CreateValueEditor.
/// </para>
/// <para>
/// The instance created by CreateValueEditor is not cached, i.e.
/// a new instance is created each time the property value is retrieved. The
/// property editor is a singleton, and the value editor cannot be a singleton
/// since it depends on the datatype configuration.
/// </para>
/// <para>
/// Technically, it could be cached by datatype but let's keep things
/// simple enough for now.
/// The instance created by CreateValueEditor is cached if allowed by the DataEditor
/// attribute (<see cref="DataEditorAttribute.ValueEditorIsReusable"/> == true).
/// </para>
/// </remarks>
// TODO: point of that one? shouldn't we always configure?
public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor();
public IDataValueEditor GetValueEditor() => ExplicitValueEditor
?? (_canReuseValueEditor
? _reusableValueEditor ??= CreateValueEditor()
: CreateValueEditor());
/// <inheritdoc />
/// <remarks>

View File

@@ -178,4 +178,10 @@ public sealed class DataEditorAttribute : Attribute
/// </summary>
/// <remarks>A deprecated editor is still supported but not proposed in the UI.</remarks>
public bool IsDeprecated { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the value editor can be reused (cached).
/// </summary>
/// <remarks>While most value editors can be reused, complex editors (e.g. block based editors) might not be applicable for reuse.</remarks>
public bool ValueEditorIsReusable { get; set; }
}

View File

@@ -11,7 +11,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
EditorType.PropertyValue | EditorType.MacroParameter,
"Decimal",
"decimal",
ValueType = ValueTypes.Decimal)]
ValueType = ValueTypes.Decimal,
ValueEditorIsReusable = true)]
public class DecimalPropertyEditor : DataEditor
{
/// <summary>

View File

@@ -11,7 +11,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
"Eye Dropper Color Picker",
"eyedropper",
Icon = "icon-colorpicker",
Group = Constants.PropertyEditors.Groups.Pickers)]
Group = Constants.PropertyEditors.Groups.Pickers,
ValueEditorIsReusable = true)]
public class EyeDropperColorPickerPropertyEditor : DataEditor
{
private readonly IEditorConfigurationParser _editorConfigurationParser;

View File

@@ -11,7 +11,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
EditorType.PropertyValue | EditorType.MacroParameter,
"Numeric",
"integer",
ValueType = ValueTypes.Integer)]
ValueType = ValueTypes.Integer,
ValueEditorIsReusable = true)]
public class IntegerPropertyEditor : DataEditor
{
public IntegerPropertyEditor(

View File

@@ -18,7 +18,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
Constants.PropertyEditors.Aliases.Label,
"Label",
"readonlyvalue",
Icon = "icon-readonly")]
Icon = "icon-readonly",
ValueEditorIsReusable = true)]
public class LabelPropertyEditor : DataEditor
{
private readonly IEditorConfigurationParser _editorConfigurationParser;

View File

@@ -17,7 +17,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
"markdowneditor",
ValueType = ValueTypes.Text,
Group = Constants.PropertyEditors.Groups.RichContent,
Icon = "icon-code")]
Icon = "icon-code",
ValueEditorIsReusable = true)]
public class MarkdownPropertyEditor : DataEditor
{
private readonly IEditorConfigurationParser _editorConfigurationParser;

View File

@@ -6,7 +6,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
"membergrouppicker",
ValueType = ValueTypes.Text,
Group = Constants.PropertyEditors.Groups.People,
Icon = Constants.Icons.MemberGroup)]
Icon = Constants.Icons.MemberGroup,
ValueEditorIsReusable = true)]
public class MemberGroupPickerPropertyEditor : DataEditor
{
public MemberGroupPickerPropertyEditor(

View File

@@ -6,7 +6,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
"memberpicker",
ValueType = ValueTypes.String,
Group = Constants.PropertyEditors.Groups.People,
Icon = Constants.Icons.Member)]
Icon = Constants.Icons.Member,
ValueEditorIsReusable = true)]
public class MemberPickerPropertyEditor : DataEditor
{
public MemberPickerPropertyEditor(

View File

@@ -6,7 +6,8 @@ namespace Umbraco.Cms.Core.PropertyEditors;
"userpicker",
ValueType = ValueTypes.Integer,
Group = Constants.PropertyEditors.Groups.People,
Icon = Constants.Icons.User)]
Icon = Constants.Icons.User,
ValueEditorIsReusable = true)]
public class UserPickerPropertyEditor : DataEditor
{
public UserPickerPropertyEditor(

View File

@@ -1,25 +1,33 @@
using Microsoft.Extensions.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Services;
public class CultureImpactFactory : ICultureImpactFactory
{
private SecuritySettings _securitySettings;
private ContentSettings _contentSettings;
public CultureImpactFactory(IOptionsMonitor<SecuritySettings> securitySettings)
public CultureImpactFactory(IOptionsMonitor<ContentSettings> contentSettings)
{
_securitySettings = securitySettings.CurrentValue;
_contentSettings = contentSettings.CurrentValue;
securitySettings.OnChange(x => _securitySettings = x);
contentSettings.OnChange(x => _contentSettings = x);
}
[Obsolete("Use constructor that takes IOptionsMonitor<SecuritySettings> instead. Scheduled for removal in V12")]
public CultureImpactFactory(IOptionsMonitor<SecuritySettings> securitySettings)
: this(StaticServiceProvider.Instance.GetRequiredService<IOptionsMonitor<ContentSettings>>())
{
}
/// <inheritdoc/>
public CultureImpact? Create(string? culture, bool isDefault, IContent content)
{
TryCreate(culture, isDefault, content.ContentType.Variations, true, _securitySettings.AllowEditInvariantFromNonDefault, out CultureImpact? impact);
TryCreate(culture, isDefault, content.ContentType.Variations, true, _contentSettings.AllowEditInvariantFromNonDefault, out CultureImpact? impact);
return impact;
}
@@ -48,7 +56,7 @@ public class CultureImpactFactory : ICultureImpactFactory
throw new ArgumentException("Culture \"*\" is not explicit.");
}
return new CultureImpact(culture, isDefault, _securitySettings.AllowEditInvariantFromNonDefault);
return new CultureImpact(culture, isDefault, _contentSettings.AllowEditInvariantFromNonDefault);
}
/// <inheritdoc/>