diff --git a/build/Azure/azuregalleryrelease.ps1 b/build/Azure/azuregalleryrelease.ps1 index 502ca3010e..61505171a2 100644 --- a/build/Azure/azuregalleryrelease.ps1 +++ b/build/Azure/azuregalleryrelease.ps1 @@ -3,57 +3,57 @@ [string]$Directory ) $workingDirectory = $Directory -CD $workingDirectory +CD "$($workingDirectory)" # Clone repo -$fullGitUrl = "https://$env:GIT_URL/$env:GIT_REPOSITORYNAME.git" -git clone $fullGitUrl 2>&1 | % { $_.ToString() } +$fullGitUrl = "https://$($env:GIT_URL)/$($env:GIT_REPOSITORYNAME).git" +git clone $($fullGitUrl) $($env:GIT_REPOSITORYNAME) 2>&1 | % { $_.ToString() } # Remove everything so that unzipping the release later will update everything # Don't remove the readme file nor the git directory Write-Host "Cleaning up git directory before adding new version" -Remove-Item -Recurse $workingDirectory\$env:GIT_REPOSITORYNAME\* -Exclude README.md,.git +Remove-Item -Recurse "$($workingDirectory)\$($env:GIT_REPOSITORYNAME)\*" -Exclude README.md,.git # Find release zip -$zipsDir = "$workingDirectory\$env:BUILD_DEFINITIONNAME\zips" +$zipsDir = "$($workingDirectory)\$($env:BUILD_DEFINITIONNAME)\zips" $pattern = "UmbracoCms.([0-9]{1,2}.[0-9]{1,3}.[0-9]{1,3}).zip" -Write-Host "Searching for Umbraco release files in $workingDirectory\$zipsDir for a file with pattern $pattern" -$file = (Get-ChildItem $zipsDir | Where-Object { $_.Name -match "$pattern" }) +Write-Host "Searching for Umbraco release files in $($zipsDir) for a file with pattern $($pattern)" +$file = (Get-ChildItem "$($zipsDir)" | Where-Object { $_.Name -match "$($pattern)" }) if($file) { # Get release name - $version = [regex]::Match($file.Name, $pattern).captures.groups[1].value - $releaseName = "Umbraco $version" - Write-Host "Found $releaseName" + $version = [regex]::Match($($file.Name), $($pattern)).captures.groups[1].value + $releaseName = "Umbraco $($version)" + Write-Host "Found $($releaseName)" # Unzip into repository to update release Add-Type -AssemblyName System.IO.Compression.FileSystem - Write-Host "Unzipping $($file.FullName) to $workingDirectory\$env:GIT_REPOSITORYNAME" - [System.IO.Compression.ZipFile]::ExtractToDirectory("$($file.FullName)", "$workingDirectory\$env:GIT_REPOSITORYNAME") + Write-Host "Unzipping $($file.FullName) to $($workingDirectory)\$($env:GIT_REPOSITORYNAME)" + [System.IO.Compression.ZipFile]::ExtractToDirectory("$($file.FullName)", "$($workingDirectory)\$($env:GIT_REPOSITORYNAME)") # Telling git who we are git config --global user.email "coffee@umbraco.com" 2>&1 | % { $_.ToString() } git config --global user.name "Umbraco HQ" 2>&1 | % { $_.ToString() } # Commit - CD $env:GIT_REPOSITORYNAME - Write-Host "Committing Umbraco $version Release from Build Output" + CD "$($workingDirectory)\$($env:GIT_REPOSITORYNAME)" + Write-Host "Committing Umbraco $($version) Release from Build Output" git add . 2>&1 | % { $_.ToString() } - git commit -m " Release $releaseName from Build Output" 2>&1 | % { $_.ToString() } + git commit -m " Release $($releaseName) from Build Output" 2>&1 | % { $_.ToString() } # Tag the release - git tag -a "v$version" -m "v$version" + git tag -a "v$($version)" -m "v$($version)" # Push release to master - $fullGitAuthUrl = "https://$($env:GIT_USERNAME):$GitHubPersonalAccessToken@$env:GIT_URL/$env:GIT_REPOSITORYNAME.git" - git push $fullGitAuthUrl 2>&1 | % { $_.ToString() } + $fullGitAuthUrl = "https://$($env:GIT_USERNAME):$($GitHubPersonalAccessToken)@$($env:GIT_URL)/$($env:GIT_REPOSITORYNAME).git" + git push $($fullGitAuthUrl) 2>&1 | % { $_.ToString() } #Push tag to master - git push $fullGitAuthUrl --tags 2>&1 | % { $_.ToString() } + git push $($fullGitAuthUrl) --tags 2>&1 | % { $_.ToString() } } else { - Write-Error "Umbraco release file not found, searched in $workingDirectory\$zipsDir for a file with pattern $pattern - cancelling" + Write-Error "Umbraco release file not found, searched in $($workingDirectory)\$($zipsDir) for a file with pattern $($pattern) - canceling" } diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 35e79d8127..30fa303b30 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -25,7 +25,7 @@ - + diff --git a/src/Umbraco.Core/Components/Composition.cs b/src/Umbraco.Core/Components/Composition.cs index dd0b83dcb3..6df86d793f 100644 --- a/src/Umbraco.Core/Components/Composition.cs +++ b/src/Umbraco.Core/Components/Composition.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.Components public class Composition : IRegister { private readonly Dictionary _builders = new Dictionary(); - private readonly Dictionary _uniques = new Dictionary(); + private readonly Dictionary> _uniques = new Dictionary>(); private readonly IRegister _register; /// @@ -83,11 +83,32 @@ namespace Umbraco.Core.Components /// public void Register(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class => _register.Register(factory, lifetime); /// - public void RegisterInstance(Type serviceType, object instance) - => _register.RegisterInstance(serviceType, instance); + public void Register(Type serviceType, object instance) + => _register.Register(serviceType, instance); + + /// + public void RegisterFor(Lifetime lifetime = Lifetime.Transient) + where TService : class + => _register.RegisterFor(lifetime); + + /// + public void RegisterFor(Type implementingType, Lifetime lifetime = Lifetime.Transient) + where TService : class + => _register.RegisterFor(implementingType, lifetime); + + /// + public void RegisterFor(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class + => _register.RegisterFor(factory, lifetime); + + /// + public void RegisterFor(TService instance) + where TService : class + => _register.RegisterFor(instance); /// public void RegisterAuto(Type serviceBaseType) @@ -104,10 +125,12 @@ namespace Umbraco.Core.Components onCreating(); foreach (var unique in _uniques.Values) - unique.RegisterWith(_register); + unique(_register); + _uniques.Clear(); // no point keep them around foreach (var builder in _builders.Values) builder.RegisterWith(_register); + _builders.Clear(); // no point keep them around Configs.RegisterWith(_register); @@ -123,74 +146,78 @@ namespace Umbraco.Core.Components #region Unique + private string GetUniqueName() + => GetUniqueName(typeof(TService)); + + private string GetUniqueName(Type serviceType) + => serviceType.FullName; + + private string GetUniqueName() + => GetUniqueName(typeof(TService), typeof(TTarget)); + + private string GetUniqueName(Type serviceType, Type targetType) + => serviceType.FullName + "::" + targetType.FullName; + /// - /// Registers a unique service. + /// Registers a unique service as its own implementation. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUnique(Type serviceType) + => _uniques[GetUniqueName(serviceType)] = register => register.Register(serviceType, Lifetime.Singleton); + + /// + /// Registers a unique service with an implementation type. /// /// Unique services have one single implementation, and a Singleton lifetime. public void RegisterUnique(Type serviceType, Type implementingType) - => _uniques[serviceType] = new Unique(serviceType, implementingType); + => _uniques[GetUniqueName(serviceType)] = register => register.Register(serviceType, implementingType, Lifetime.Singleton); /// - /// Registers a unique service. - /// - /// Unique services have one single implementation, and a Singleton lifetime. - public void RegisterUnique(Type serviceType, object instance) - => _uniques[serviceType] = new Unique(serviceType, instance); - - /// - /// Registers a unique service. + /// Registers a unique service with an implementation factory. /// /// Unique services have one single implementation, and a Singleton lifetime. public void RegisterUnique(Func factory) - => _uniques[typeof(TService)] = new Unique(factory); + where TService : class + => _uniques[GetUniqueName()] = register => register.Register(factory, Lifetime.Singleton); - private class Unique - { - private readonly Type _serviceType; - private readonly Type _implementingType; - private readonly object _instance; + /// + /// Registers a unique service with an implementing instance. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUnique(Type serviceType, object instance) + => _uniques[GetUniqueName(serviceType)] = register => register.Register(serviceType, instance); - protected Unique(Type serviceType) - { - _serviceType = serviceType; - } + /// + /// Registers a unique service for a target, as its own implementation. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor() + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(Lifetime.Singleton); - public Unique(Type serviceType, Type implementingType) - : this(serviceType) - { - _implementingType = implementingType; - } + /// + /// Registers a unique service for a target, with an implementing type. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor(Type implementingType) + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(implementingType, Lifetime.Singleton); - public Unique(Type serviceType, object instance) - : this(serviceType) - { - _instance = instance; - } + /// + /// Registers a unique service for a target, with an implementation factory. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor(Func factory) + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(factory, Lifetime.Singleton); - public virtual void RegisterWith(IRegister register) - { - if (_implementingType != null) - register.Register(_serviceType, _implementingType, Lifetime.Singleton); - else if (_instance != null) - register.RegisterInstance(_serviceType, _instance); - } - } - - private class Unique : Unique - { - private readonly Func _factory; - - public Unique(Func factory) - : base(typeof(TService)) - { - _factory = factory; - } - - public override void RegisterWith(IRegister register) - { - register.Register(_factory, Lifetime.Singleton); - } - } + /// + /// Registers a unique service for a target, with an implementing instance. + /// + /// Unique services have one single implementation, and a Singleton lifetime. + public void RegisterUniqueFor(TService instance) + where TService : class + => _uniques[GetUniqueName()] = register => register.RegisterFor(instance); #endregion diff --git a/src/Umbraco.Core/Components/CompositionExtensions.cs b/src/Umbraco.Core/Components/CompositionExtensions.cs index 93d190d17e..bb23e89b81 100644 --- a/src/Umbraco.Core/Components/CompositionExtensions.cs +++ b/src/Umbraco.Core/Components/CompositionExtensions.cs @@ -26,15 +26,16 @@ namespace Umbraco.Core.Components /// The type of the filesystem. /// The implementing type. /// The composition. - /// A factory method creating the supporting filesystem. /// The register. - public static void RegisterFileSystem(this Composition composition, Func supportingFileSystemFactory) + public static void RegisterFileSystem(this Composition composition) where TImplementing : FileSystemWrapper, TFileSystem + where TFileSystem : class { composition.RegisterUnique(factory => { var fileSystems = factory.GetInstance(); - return fileSystems.GetFileSystem(supportingFileSystemFactory(factory)); + var supporting = factory.GetInstance(); + return fileSystems.GetFileSystem(supporting.For()); }); } @@ -43,15 +44,15 @@ namespace Umbraco.Core.Components /// /// The type of the filesystem. /// The composition. - /// A factory method creating the supporting filesystem. /// The register. - public static void RegisterFileSystem(this Composition composition, Func supportingFileSystemFactory) + public static void RegisterFileSystem(this Composition composition) where TFileSystem : FileSystemWrapper { composition.RegisterUnique(factory => { var fileSystems = factory.GetInstance(); - return fileSystems.GetFileSystem(supportingFileSystemFactory(factory)); + var supporting = factory.GetInstance(); + return fileSystems.GetFileSystem(supporting.For()); }); } @@ -280,6 +281,22 @@ namespace Umbraco.Core.Components composition.RegisterUnique(_ => helper); } + /// + /// Sets the underlying media filesystem. + /// + /// A composition. + /// A filesystem factory. + public static void SetMediaFileSystem(this Composition composition, Func filesystemFactory) + => composition.RegisterUniqueFor(filesystemFactory); + + /// + /// Sets the underlying media filesystem. + /// + /// A composition. + /// A filesystem factory. + public static void SetMediaFileSystem(this Composition composition, Func filesystemFactory) + => composition.RegisterUniqueFor(_ => filesystemFactory()); + #endregion } } diff --git a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs index 7633f6b001..41038ea4e9 100644 --- a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class CollectionBuilderBase : ICollectionBuilder where TBuilder: CollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { private readonly List _types = new List(); private readonly object _locker = new object(); diff --git a/src/Umbraco.Core/Composing/Composers/FileSystemsComposer.cs b/src/Umbraco.Core/Composing/Composers/FileSystemsComposer.cs index f1fb095406..4c598f27e4 100644 --- a/src/Umbraco.Core/Composing/Composers/FileSystemsComposer.cs +++ b/src/Umbraco.Core/Composing/Composers/FileSystemsComposer.cs @@ -12,10 +12,9 @@ namespace Umbraco.Core.Composing.Composers * * Create a component and use it to modify the composition by adding something like: * - * composition.Container.RegisterFileSystem( - * factory => new PhysicalFileSystem("~/somewhere")); + * composition.RegisterUniqueFor(...); * - * return whatever supporting filesystem you like. + * and register whatever supporting filesystem you like. * * * HOW TO IMPLEMENT MY OWN FILESYSTEM @@ -30,12 +29,15 @@ namespace Umbraco.Core.Composing.Composers * { } * } * - * The ctor can have more parameters that will be resolved by the container. + * The ctor can have more parameters, that will be resolved by the container. * * Register your filesystem, in a component: * - * composition.Container.RegisterFileSystem( - * factory => new PhysicalFileSystem("~/my")); + * composition.RegisterFileSystem(); + * + * Register the underlying filesystem: + * + * composition.RegisterUniqueFor(...); * * And that's it, you can inject MyFileSystem wherever it's needed. * @@ -48,8 +50,8 @@ namespace Umbraco.Core.Composing.Composers * Make the class implement the interface, then * register your filesystem, in a component: * - * composition.Container.RegisterFileSystem( - * factory => new PhysicalFileSystem("~/my")); + * composition.RegisterFileSystem(); + * composition.RegisterUniqueFor(...); * * And that's it, you can inject IMyFileSystem wherever it's needed. * @@ -79,9 +81,16 @@ namespace Umbraco.Core.Composing.Composers // register the scheme for media paths composition.RegisterUnique(); - // register the IMediaFileSystem implementation with a supporting filesystem - composition.RegisterFileSystem( - factory => new PhysicalFileSystem("~/media")); + // register the IMediaFileSystem implementation + composition.RegisterFileSystem(); + + // register the supporting filesystems provider + composition.Register(factory => new SupportingFileSystems(factory), Lifetime.Singleton); + + // register the IFileSystem supporting the IMediaFileSystem + // THIS IS THE ONLY THING THAT NEEDS TO CHANGE, IN ORDER TO REPLACE THE UNDERLYING FILESYSTEM + // and, SupportingFileSystem.For() returns the underlying filesystem + composition.SetMediaFileSystem(() => new PhysicalFileSystem("~/media")); return composition; } diff --git a/src/Umbraco.Core/Composing/CompositionExtensions.cs b/src/Umbraco.Core/Composing/CompositionExtensions.cs index cfc465b59d..2307d757c9 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions.cs @@ -51,6 +51,13 @@ namespace Umbraco.Core.Composing public static void RegisterUnique(this Composition composition) => composition.RegisterUnique(typeof(TService), typeof(TImplementing)); + /// + /// Registers a unique service with an implementation type, for a target. + /// + public static void RegisterUniqueFor(this Composition composition) + where TService : class + => composition.RegisterUniqueFor(typeof(TImplementing)); + /// /// Registers a unique service with an implementing instance. /// diff --git a/src/Umbraco.Core/Composing/FactoryExtensions.cs b/src/Umbraco.Core/Composing/FactoryExtensions.cs index 2640b7f7e6..8027f2c7a1 100644 --- a/src/Umbraco.Core/Composing/FactoryExtensions.cs +++ b/src/Umbraco.Core/Composing/FactoryExtensions.cs @@ -17,6 +17,7 @@ namespace Umbraco.Core.Composing /// An instance of the specified type. /// Throws an exception if the factory failed to get an instance of the specified type. public static T GetInstance(this IFactory factory) + where T : class => (T)factory.GetInstance(typeof(T)); /// @@ -28,6 +29,7 @@ namespace Umbraco.Core.Composing /// of the specified type. Throws an exception if the factory does know how /// to get an instance of the specified type, but failed to do so. public static T TryGetInstance(this IFactory factory) + where T : class => (T)factory.TryGetInstance(typeof(T)); /// @@ -42,6 +44,7 @@ namespace Umbraco.Core.Composing /// The arguments are used as dependencies by the factory. /// public static T CreateInstance(this IFactory factory, params object[] args) + where T : class => (T)factory.CreateInstance(typeof(T), args); /// diff --git a/src/Umbraco.Core/Composing/IFactory.cs b/src/Umbraco.Core/Composing/IFactory.cs index 9a59b1c052..768b9207a3 100644 --- a/src/Umbraco.Core/Composing/IFactory.cs +++ b/src/Umbraco.Core/Composing/IFactory.cs @@ -3,13 +3,6 @@ using System.Collections.Generic; namespace Umbraco.Core.Composing { - // Implementing: - // - // The factory - // - always picks the constructor with the most parameters - // - supports Lazy parameters (and prefers them over non-Lazy) in constructors - // - what happens with 'releasing' is unclear - /// /// Defines a service factory for Umbraco. /// @@ -28,6 +21,15 @@ namespace Umbraco.Core.Composing /// Throws an exception if the container failed to get an instance of the specified type. object GetInstance(Type type); + /// + /// Gets a targeted instance of a service. + /// + /// The type of the service. + /// The type of the target. + /// The instance of the specified type for the specified target. + /// Throws an exception if the container failed to get an instance of the specified type. + TService GetInstanceFor(); + /// /// Tries to get an instance of a service. /// @@ -48,7 +50,8 @@ namespace Umbraco.Core.Composing /// Gets all instances of a service. /// /// The type of the service. - IEnumerable GetAllInstances(); + IEnumerable GetAllInstances() + where TService : class; /// /// Releases an instance. diff --git a/src/Umbraco.Core/Composing/IRegister.cs b/src/Umbraco.Core/Composing/IRegister.cs index 8ad3db5409..cbf12f54a3 100644 --- a/src/Umbraco.Core/Composing/IRegister.cs +++ b/src/Umbraco.Core/Composing/IRegister.cs @@ -2,17 +2,6 @@ namespace Umbraco.Core.Composing { - // Implementing: - // - // The register - // - supports registering a service, even after some instances of other services have been created - // - supports re-registering a service, as long as no instance of that service has been created - // - throws when re-registering a service, and an instance of that service has been created - // - // - registers only one implementation of a nameless service, re-registering replaces the previous - // registration - names are required to register multiple implementations - and getting an - // IEnumerable of the service, nameless, returns them all - /// /// Defines a service register for Umbraco. /// @@ -36,12 +25,53 @@ namespace Umbraco.Core.Composing /// /// Registers a service with an implementation factory. /// - void Register(Func factory, Lifetime lifetime = Lifetime.Transient); + void Register(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class; /// /// Registers a service with an implementing instance. /// - void RegisterInstance(Type serviceType, object instance); + void Register(Type serviceType, object instance); + + /// + /// Registers a service for a target, as its own implementation. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(Lifetime lifetime = Lifetime.Transient) + where TService : class; + + /// + /// Registers a service for a target, with an implementation type. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(Type implementingType, Lifetime lifetime = Lifetime.Transient) + where TService : class; + + /// + /// Registers a service for a target, with an implementation factory. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class; + + /// + /// Registers a service for a target, with an implementing instance. + /// + /// + /// There can only be one implementation or instanced registered for a service and target; + /// what happens if many are registered is not specified. + /// + void RegisterFor(TService instance) + where TService : class; /// /// Registers a base type for auto-registration. diff --git a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs index a1a06621e9..46b06daf7d 100644 --- a/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/LazyCollectionBuilderBase.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class LazyCollectionBuilderBase : CollectionBuilderBase where TBuilder : LazyCollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { private readonly List>> _producers = new List>>(); private readonly List _excluded = new List(); diff --git a/src/Umbraco.Core/Composing/LightInject/LightInjectContainer.cs b/src/Umbraco.Core/Composing/LightInject/LightInjectContainer.cs index b39622f66a..d8a554ee8c 100644 --- a/src/Umbraco.Core/Composing/LightInject/LightInjectContainer.cs +++ b/src/Umbraco.Core/Composing/LightInject/LightInjectContainer.cs @@ -102,18 +102,25 @@ namespace Umbraco.Core.Composing.LightInject /// public IFactory CreateFactory() => this; + private static string GetTargetedServiceName() => "TARGET:" + typeof(TTarget).FullName; + #region Factory /// public object GetInstance(Type type) => Container.GetInstance(type); + /// + public TService GetInstanceFor() + => Container.GetInstance(GetTargetedServiceName()); + /// public object TryGetInstance(Type type) => Container.TryGetInstance(type); /// public IEnumerable GetAllInstances() + where T : class => Container.GetAllInstances(); /// @@ -138,21 +145,7 @@ namespace Umbraco.Core.Composing.LightInject /// public void Register(Type serviceType, Lifetime lifetime = Lifetime.Transient) - { - switch (lifetime) - { - case Lifetime.Transient: - Container.Register(serviceType); - break; - case Lifetime.Request: - case Lifetime.Scope: - case Lifetime.Singleton: - Container.Register(serviceType, GetLifetime(lifetime)); - break; - default: - throw new NotSupportedException($"Lifetime {lifetime} is not supported."); - } - } + => Container.Register(serviceType, GetLifetime(lifetime)); /// public void Register(Type serviceType, Type implementingType, Lifetime lifetime = Lifetime.Transient) @@ -174,22 +167,41 @@ namespace Umbraco.Core.Composing.LightInject /// public void Register(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class { - switch (lifetime) - { - case Lifetime.Transient: - Container.Register(f => factory(this)); - break; - case Lifetime.Request: - case Lifetime.Scope: - case Lifetime.Singleton: - Container.Register(f => factory(this), GetLifetime(lifetime)); - break; - default: - throw new NotSupportedException($"Lifetime {lifetime} is not supported."); - } + Container.Register(f => factory(this), GetLifetime(lifetime)); } + /// + public void Register(Type serviceType, object instance) + => Container.RegisterInstance(serviceType, instance); + + /// + public void RegisterFor(Lifetime lifetime = Lifetime.Transient) + where TService : class + => RegisterFor(typeof(TService), lifetime); + + /// + public void RegisterFor(Type implementingType, Lifetime lifetime = Lifetime.Transient) + where TService : class + { + // note that there can only be one implementation or instance registered "for" a service + Container.Register(typeof(TService), implementingType, GetTargetedServiceName(), GetLifetime(lifetime)); + } + + /// + public void RegisterFor(Func factory, Lifetime lifetime = Lifetime.Transient) + where TService : class + { + // note that there can only be one implementation or instance registered "for" a service + Container.Register(f => factory(this), GetTargetedServiceName(), GetLifetime(lifetime)); + } + + /// + public void RegisterFor(TService instance) + where TService : class + => Container.RegisterInstance(typeof(TService), instance, GetTargetedServiceName()); + private ILifetime GetLifetime(Lifetime lifetime) { switch (lifetime) @@ -207,10 +219,6 @@ namespace Umbraco.Core.Composing.LightInject } } - /// - public void RegisterInstance(Type serviceType, object instance) - => Container.RegisterInstance(serviceType, instance); - /// public void RegisterAuto(Type serviceBaseType) { @@ -223,17 +231,6 @@ namespace Umbraco.Core.Composing.LightInject }, null); } - // was the Light-Inject specific way of dealing with args, but we've replaced it with our own - // beware! does NOT work on singletons, see https://github.com/seesharper/LightInject/issues/294 - // - ///// - //public void RegisterConstructorDependency(Func factory) - // => Container.RegisterConstructorDependency((f, x) => factory(this, x)); - // - ///// - //public void RegisterConstructorDependency(Func factory) - // => Container.RegisterConstructorDependency((f, x, a) => factory(this, x, a)); - #endregion #region Control @@ -256,21 +253,14 @@ namespace Umbraco.Core.Composing.LightInject private class AssemblyScanner : IAssemblyScanner { - //private readonly IAssemblyScanner _scanner; - - //public AssemblyScanner(IAssemblyScanner scanner) - //{ - // _scanner = scanner; - //} - public void Scan(Assembly assembly, IServiceRegistry serviceRegistry, Func lifetime, Func shouldRegister, Func serviceNameProvider) { - // nothing - we *could* scan non-Umbraco assemblies, though + // nothing - we don't want LightInject to scan } public void Scan(Assembly assembly, IServiceRegistry serviceRegistry) { - // nothing - we *could* scan non-Umbraco assemblies, though + // nothing - we don't want LightInject to scan } } diff --git a/src/Umbraco.Core/Composing/LightInject/MixedLightInjectScopeManagerProvider.cs b/src/Umbraco.Core/Composing/LightInject/MixedLightInjectScopeManagerProvider.cs index 470079c6c0..897c58dd43 100644 --- a/src/Umbraco.Core/Composing/LightInject/MixedLightInjectScopeManagerProvider.cs +++ b/src/Umbraco.Core/Composing/LightInject/MixedLightInjectScopeManagerProvider.cs @@ -13,6 +13,9 @@ namespace Umbraco.Core.Composing.LightInject // of PerWebRequestScopeManagerProvider - but all delegates see is the mixed one - and therefore // they can transition without issues. // + // The PerWebRequestScopeManager maintains the scope in HttpContext and LightInject registers a + // module (PreApplicationStartMethod) which disposes it on EndRequest + // // the mixed provider is installed in container.ConfigureUmbracoCore() and then, // when doing eg container.EnableMvc() or anything that does container.EnablePerWebRequestScope() // we need to take great care to preserve the mixed scope manager provider! diff --git a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs index bde1bf96c5..241b84d8d2 100644 --- a/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/OrderedCollectionBuilderBase.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class OrderedCollectionBuilderBase : CollectionBuilderBase where TBuilder : OrderedCollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { protected abstract TBuilder This { get; } diff --git a/src/Umbraco.Core/Composing/RegisterExtensions.cs b/src/Umbraco.Core/Composing/RegisterExtensions.cs index 4db1a2e9e4..d1eacc0c0f 100644 --- a/src/Umbraco.Core/Composing/RegisterExtensions.cs +++ b/src/Umbraco.Core/Composing/RegisterExtensions.cs @@ -11,22 +11,32 @@ public static void Register(this IRegister register, Lifetime lifetime = Lifetime.Transient) => register.Register(typeof(TService), typeof(TImplementing), lifetime); + /// + /// Registers a service with an implementation type, for a target. + /// + public static void RegisterFor(this IRegister register, Lifetime lifetime = Lifetime.Transient) + where TService : class + => register.RegisterFor(typeof(TImplementing), lifetime); + /// /// Registers a service as its own implementation. /// public static void Register(this IRegister register, Lifetime lifetime = Lifetime.Transient) + where TService : class => register.Register(typeof(TService), lifetime); /// /// Registers a service with an implementing instance. /// - public static void RegisterInstance(this IRegister register, TService instance) - => register.RegisterInstance(typeof(TService), instance); + public static void Register(this IRegister register, TService instance) + where TService : class + => register.Register(typeof(TService), instance); /// /// Registers a base type for auto-registration. /// public static void RegisterAuto(this IRegister register) + where TServiceBase : class => register.RegisterAuto(typeof(TServiceBase)); } } diff --git a/src/Umbraco.Core/Composing/TargetedServiceFactory.cs b/src/Umbraco.Core/Composing/TargetedServiceFactory.cs new file mode 100644 index 0000000000..53022c0043 --- /dev/null +++ b/src/Umbraco.Core/Composing/TargetedServiceFactory.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Composing +{ + /// + /// Provides a base class for targeted service factories. + /// + /// + public abstract class TargetedServiceFactory + { + private readonly IFactory _factory; + + protected TargetedServiceFactory(IFactory factory) + { + _factory = factory; + } + + public TService For() => _factory.GetInstanceFor(); + } +} diff --git a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs index da47c53bf8..f8ecc11d98 100644 --- a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Composing /// The type of the items. public abstract class WeightedCollectionBuilderBase : CollectionBuilderBase where TBuilder : WeightedCollectionBuilderBase - where TCollection : IBuilderCollection + where TCollection : class, IBuilderCollection { protected abstract TBuilder This { get; } diff --git a/src/Umbraco.Core/Configuration/Configs.cs b/src/Umbraco.Core/Configuration/Configs.cs index 3dbbe5d4ff..51e1a327a0 100644 --- a/src/Umbraco.Core/Configuration/Configs.cs +++ b/src/Umbraco.Core/Configuration/Configs.cs @@ -100,7 +100,7 @@ namespace Umbraco.Core.Configuration if (_registerings == null) throw new InvalidOperationException("Configurations have already been registered."); - register.RegisterInstance(this); + register.Register(this); foreach (var registering in _registerings.Values) registering(register); diff --git a/src/Umbraco.Core/Deploy/IValueConnector.cs b/src/Umbraco.Core/Deploy/IValueConnector.cs index 35304e3fde..92589ab6cf 100644 --- a/src/Umbraco.Core/Deploy/IValueConnector.cs +++ b/src/Umbraco.Core/Deploy/IValueConnector.cs @@ -17,19 +17,19 @@ namespace Umbraco.Core.Deploy IEnumerable PropertyEditorAliases { get; } /// - /// Gets the deploy property corresponding to a content property. + /// Gets the deploy property value corresponding to a content property value, and gather dependencies. /// - /// The content property. + /// The content property value. /// The content dependencies. /// The deploy property value. - string GetValue(Property property, ICollection dependencies); + string ToArtifact(object value, ICollection dependencies); /// - /// Sets a content property value using a deploy property. + /// Gets the content property value corresponding to a deploy property value. /// - /// The content item. - /// The property alias. /// The deploy property value. - void SetValue(IContentBase content, string alias, string value); + /// The current content property value. + /// The content property value. + object FromArtifact(string value, object currentValue); } } diff --git a/src/Umbraco.Core/IO/SupportingFileSystems.cs b/src/Umbraco.Core/IO/SupportingFileSystems.cs new file mode 100644 index 0000000000..43ac2ba85a --- /dev/null +++ b/src/Umbraco.Core/IO/SupportingFileSystems.cs @@ -0,0 +1,11 @@ +using Umbraco.Core.Composing; + +namespace Umbraco.Core.IO +{ + public class SupportingFileSystems : TargetedServiceFactory + { + public SupportingFileSystems(IFactory factory) + : base(factory) + { } + } +} diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index 5d2359d04c..cb6a1cb031 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -319,7 +319,7 @@ namespace Umbraco.Core.Runtime /// Gets a profiler. /// protected virtual IProfiler GetProfiler() - => new LogProfiler(ProfilingLogger); + => new LogProfiler(Logger); /// /// Gets the application caches. diff --git a/src/Umbraco.Core/Scoping/ScopeContext.cs b/src/Umbraco.Core/Scoping/ScopeContext.cs index dd26fda85e..4ba1999474 100644 --- a/src/Umbraco.Core/Scoping/ScopeContext.cs +++ b/src/Umbraco.Core/Scoping/ScopeContext.cs @@ -7,27 +7,32 @@ namespace Umbraco.Core.Scoping internal class ScopeContext : IScopeContext, IInstanceIdentifiable { private Dictionary _enlisted; - private bool _exiting; public void ScopeExit(bool completed) { if (_enlisted == null) return; - _exiting = true; + // fixme - can we create infinite loops? + // fixme - what about nested events? will they just be plainly ignored = really bad? List exceptions = null; - foreach (var enlisted in _enlisted.Values.OrderBy(x => x.Priority)) + List orderedEnlisted; + while ((orderedEnlisted = _enlisted.Values.OrderBy(x => x.Priority).ToList()).Count > 0) { - try + _enlisted.Clear(); + foreach (var enlisted in orderedEnlisted) { - enlisted.Execute(completed); - } - catch (Exception e) - { - if (exceptions == null) - exceptions = new List(); - exceptions.Add(e); + try + { + enlisted.Execute(completed); + } + catch (Exception e) + { + if (exceptions == null) + exceptions = new List(); + exceptions.Add(e); + } } } @@ -74,9 +79,6 @@ namespace Umbraco.Core.Scoping public T Enlist(string key, Func creator, Action action = null, int priority = 100) { - if (_exiting) - throw new InvalidOperationException("Cannot enlist now, context is exiting."); - var enlistedObjects = _enlisted ?? (_enlisted = new Dictionary()); if (enlistedObjects.TryGetValue(key, out var enlisted)) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 1e89e190c8..9943893473 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2733,6 +2733,8 @@ namespace Umbraco.Core.Services.Implement } } + private static readonly string[] ArrayOfOneNullString = { null }; + public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = 0) { if (blueprint == null) throw new ArgumentNullException(nameof(blueprint)); @@ -2745,7 +2747,7 @@ namespace Umbraco.Core.Services.Implement content.WriterId = userId; var now = DateTime.Now; - var cultures = blueprint.CultureInfos.Any() ? blueprint.CultureInfos.Select(x=>x.Key) : new[] {(string)null}; + var cultures = blueprint.CultureInfos.Any() ? blueprint.CultureInfos.Select(x=>x.Key) : ArrayOfOneNullString; foreach (var culture in cultures) { foreach (var property in blueprint.Properties) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b59a5190bd..6b334b234b 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -196,6 +196,7 @@ + @@ -342,6 +343,7 @@ + diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 5674698b20..d8e698cdad 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -42,9 +42,9 @@ namespace Umbraco.Examine var values = new Dictionary> { {"icon", c.ContentType.Icon.Yield()}, - {UmbracoExamineIndex.PublishedFieldName, new object[] {c.Published ? 1 : 0}}, //Always add invariant published value + {UmbracoExamineIndex.PublishedFieldName, new object[] {c.Published ? "y" : "n"}}, //Always add invariant published value {"id", new object[] {c.Id}}, - {"key", new object[] {c.Key}}, + {UmbracoExamineIndex.NodeKeyFieldName, new object[] {c.Key}}, {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, {"level", new object[] {c.Level}}, {"creatorID", new object[] {c.CreatorId}}, @@ -61,12 +61,12 @@ namespace Umbraco.Examine {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, {"writerID", new object[] {c.WriterId}}, {"templateID", new object[] {c.TemplateId ?? 0}}, - {UmbracoContentIndex.VariesByCultureFieldName, new object[] {0}}, + {UmbracoContentIndex.VariesByCultureFieldName, new object[] {"n"}}, }; if (isVariant) { - values[UmbracoContentIndex.VariesByCultureFieldName] = new object[] { 1 }; + values[UmbracoContentIndex.VariesByCultureFieldName] = new object[] { "y" }; foreach (var culture in c.AvailableCultures) { @@ -76,7 +76,7 @@ namespace Umbraco.Examine values[$"nodeName_{lowerCulture}"] = PublishedValuesOnly ? c.GetPublishName(culture).Yield() : c.GetCultureName(culture).Yield(); - values[$"{UmbracoExamineIndex.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? 1 : 0).Yield(); + values[$"{UmbracoExamineIndex.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? "y" : "n").Yield(); values[$"updateDate_{lowerCulture}"] = PublishedValuesOnly ? c.GetPublishDate(culture).Yield() : c.GetUpdateDate(culture).Yield(); diff --git a/src/Umbraco.Examine/ContentValueSetValidator.cs b/src/Umbraco.Examine/ContentValueSetValidator.cs index d4f6ceb15f..9555566c53 100644 --- a/src/Umbraco.Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Examine/ContentValueSetValidator.cs @@ -95,17 +95,17 @@ namespace Umbraco.Examine if (!valueSet.Values.TryGetValue(UmbracoExamineIndex.PublishedFieldName, out var published)) return ValueSetValidationResult.Failed; - if (!published[0].Equals(1)) + if (!published[0].Equals("y")) return ValueSetValidationResult.Failed; //deal with variants, if there are unpublished variants than we need to remove them from the value set if (valueSet.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var variesByCulture) - && variesByCulture.Count > 0 && variesByCulture[0].Equals(1)) + && variesByCulture.Count > 0 && variesByCulture[0].Equals("y")) { //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values foreach(var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndex.PublishedFieldName}_")).ToList()) { - if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals(1)) + if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals("y")) { //this culture is not published, so remove all of these culture values var cultureSuffix = publishField.Key.Substring(publishField.Key.LastIndexOf('_')); diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 525f0deaa1..4fe6c359d7 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using Examine; using Examine.LuceneEngine.Providers; using Lucene.Net.Analysis; @@ -7,6 +9,7 @@ using Lucene.Net.Index; using Lucene.Net.QueryParsers; using Lucene.Net.Search; using Lucene.Net.Store; +using Umbraco.Core; using Version = Lucene.Net.Util.Version; using Umbraco.Core.Logging; @@ -15,9 +18,35 @@ namespace Umbraco.Examine /// /// Extension methods for the LuceneIndex /// - internal static class ExamineExtensions + public static class ExamineExtensions { - public static bool TryParseLuceneQuery(string query) + /// + /// Matches a culture iso name suffix + /// + /// + /// myFieldName_en-us will match the "en-us" + /// + internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^([_\\w]+)_([a-z]{2}-[a-z0-9]{2,4})$", RegexOptions.Compiled); + + /// + /// Returns all index fields that are culture specific (suffixed) + /// + /// + /// + /// + public static IEnumerable GetCultureFields(this IUmbracoIndex index, string culture) + { + var allFields = index.GetFields(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in allFields) + { + var match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && match.Groups.Count == 3 && culture.InvariantEquals(match.Groups[2].Value)) + yield return field; + } + } + + internal static bool TryParseLuceneQuery(string query) { //TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll // also do this rudimentary check diff --git a/src/Umbraco.Examine/MediaValueSetBuilder.cs b/src/Umbraco.Examine/MediaValueSetBuilder.cs index f0e5e895e6..2676093eeb 100644 --- a/src/Umbraco.Examine/MediaValueSetBuilder.cs +++ b/src/Umbraco.Examine/MediaValueSetBuilder.cs @@ -32,7 +32,7 @@ namespace Umbraco.Examine { {"icon", m.ContentType.Icon.Yield()}, {"id", new object[] {m.Id}}, - {"key", new object[] {m.Key}}, + {UmbracoExamineIndex.NodeKeyFieldName, new object[] {m.Key}}, {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, {"level", new object[] {m.Level}}, {"creatorID", new object[] {m.CreatorId}}, diff --git a/src/Umbraco.Examine/MemberValueSetBuilder.cs b/src/Umbraco.Examine/MemberValueSetBuilder.cs index 9864aba18d..d9f0b7806d 100644 --- a/src/Umbraco.Examine/MemberValueSetBuilder.cs +++ b/src/Umbraco.Examine/MemberValueSetBuilder.cs @@ -23,7 +23,7 @@ namespace Umbraco.Examine { {"icon", m.ContentType.Icon.Yield()}, {"id", new object[] {m.Id}}, - {"key", new object[] {m.Key}}, + {UmbracoExamineIndex.NodeKeyFieldName, new object[] {m.Key}}, {"parentID", new object[] {m.Level > 1 ? m.ParentId : -1}}, {"level", new object[] {m.Level}}, {"creatorID", new object[] {m.CreatorId}}, diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index a68131da0d..1320f3b0d2 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -48,7 +48,7 @@ - + diff --git a/src/Umbraco.Examine/UmbracoFieldDefinitionCollection.cs b/src/Umbraco.Examine/UmbracoFieldDefinitionCollection.cs index 97d1f68727..1e7b51aa14 100644 --- a/src/Umbraco.Examine/UmbracoFieldDefinitionCollection.cs +++ b/src/Umbraco.Examine/UmbracoFieldDefinitionCollection.cs @@ -1,4 +1,7 @@ -using Examine; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Examine; +using Umbraco.Core; namespace Umbraco.Examine { @@ -30,7 +33,7 @@ namespace Umbraco.Examine new FieldDefinition("createDate", FieldDefinitionTypes.DateTime), new FieldDefinition("updateDate", FieldDefinitionTypes.DateTime), - new FieldDefinition("key", FieldDefinitionTypes.InvariantCultureIgnoreCase), + new FieldDefinition(UmbracoExamineIndex.NodeKeyFieldName, FieldDefinitionTypes.InvariantCultureIgnoreCase), new FieldDefinition("version", FieldDefinitionTypes.Raw), new FieldDefinition("nodeType", FieldDefinitionTypes.InvariantCultureIgnoreCase), new FieldDefinition("template", FieldDefinitionTypes.Raw), @@ -40,36 +43,54 @@ namespace Umbraco.Examine new FieldDefinition("email", FieldDefinitionTypes.EmailAddress), new FieldDefinition(UmbracoExamineIndex.PublishedFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineIndex.NodeKeyFieldName, FieldDefinitionTypes.Raw), new FieldDefinition(UmbracoExamineIndex.IndexPathFieldName, FieldDefinitionTypes.Raw), - new FieldDefinition(UmbracoExamineIndex.IconFieldName, FieldDefinitionTypes.Raw) + new FieldDefinition(UmbracoExamineIndex.IconFieldName, FieldDefinitionTypes.Raw), + new FieldDefinition(UmbracoContentIndex.VariesByCultureFieldName, FieldDefinitionTypes.Raw), }; - ///// - ///// Overridden to dynamically add field definitions for culture variations - ///// - ///// - ///// - ///// - //public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) - //{ - // var result = base.TryGetValue(fieldName, out fieldDefinition); - // if (result) return true; - // //if the fieldName is not suffixed with _iso-Code - // var underscoreIndex = fieldName.LastIndexOf('_'); - // if (underscoreIndex == -1) return false; + /// + /// Overridden to dynamically add field definitions for culture variations + /// + /// + /// + /// + /// + /// We need to do this so that we don't have to maintain a huge static list of all field names and their definitions + /// otherwise we'd have to dynamically add/remove definitions anytime languages are added/removed, etc... + /// For example, we have things like `nodeName` and `__Published` which are also used for culture fields like `nodeName_en-us` + /// and we don't want to have a full static list of all of these definitions when we can just define the one definition and then + /// dynamically apply that to culture specific fields. + /// + /// There is a caveat to this however, when a field definition is found for a non-culture field we will create and store a new field + /// definition for that culture so that the next time it needs to be looked up and used we are not allocating more objects. This does mean + /// however that if a language is deleted, the field definitions for that language will still exist in memory. This isn't going to cause any + /// problems and the mem will be cleared on next site restart but it's worth pointing out. + /// + public override bool TryGetValue(string fieldName, out FieldDefinition fieldDefinition) + { + if (base.TryGetValue(fieldName, out fieldDefinition)) + return true; + //before we use regex to match do some faster simple matching since this is going to execute quite a lot + if (!fieldName.Contains("_") || !fieldName.Contains("-")) + return false; + var match = ExamineExtensions.CultureIsoCodeFieldNameMatchExpression.Match(fieldName); + if (match.Success && match.Groups.Count == 3) + { + var nonCultureFieldName = match.Groups[1].Value; + //check if there's a definition for this and if so return the field definition for the culture field based on the non-culture field + if (base.TryGetValue(nonCultureFieldName, out var existingFieldDefinition)) + { + //now add a new field def + fieldDefinition = GetOrAdd(fieldName, s => new FieldDefinition(s, existingFieldDefinition.Type)); + return true; + } + } + return false; + } - // var isoCode = fieldName.Substring(underscoreIndex); - // if (isoCode.Length < 6) return false; //invalid isoCode - - // var hyphenIndex = isoCode.IndexOf('-'); - // if (hyphenIndex != 3) return false; //invalid isoCode - - // //we'll assume this is a valid isoCode - - //} + } } diff --git a/src/Umbraco.Examine/UmbracoMemberIndex.cs b/src/Umbraco.Examine/UmbracoMemberIndex.cs index 9782f94fe4..fbf8a1cc0f 100644 --- a/src/Umbraco.Examine/UmbracoMemberIndex.cs +++ b/src/Umbraco.Examine/UmbracoMemberIndex.cs @@ -32,35 +32,6 @@ namespace Umbraco.Examine base(name, luceneDirectory, fieldDefinitions, analyzer, profilingLogger, validator) { } - - /// - /// Overridden to ensure that the umbraco system field definitions are in place - /// - /// - /// - protected override FieldValueTypeCollection CreateFieldValueTypes(IReadOnlyDictionary indexValueTypesFactory = null) - { - var keyDef = new FieldDefinition("__key", FieldDefinitionTypes.Raw); - FieldDefinitionCollection.TryAdd(keyDef); - - return base.CreateFieldValueTypes(indexValueTypesFactory); - } - - /// - /// Ensure some custom values are added to the index - /// - /// - protected override void OnTransformingIndexValues(IndexingItemEventArgs e) - { - base.OnTransformingIndexValues(e); - - if (e.ValueSet.Values.TryGetValue("key", out var key) && e.ValueSet.Values.ContainsKey("__key") == false) - { - //double __ prefix means it will be indexed as culture invariant - e.ValueSet.Values["__key"] = key; - } - - } - + } } diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs index 8254807ea0..6add88009d 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs @@ -202,7 +202,7 @@ namespace Umbraco.Tests.Cache.PublishedCache {"creatorName", "Shannon"} }; - var result = new SearchResult("1234", 1, 1, () => fields.ToDictionary(x => x.Key, x => new List { x.Value })); + var result = new SearchResult("1234", 1, () => fields.ToDictionary(x => x.Key, x => new List { x.Value })); var store = new PublishedMediaCache(new XmlStore((XmlDocument)null, null, null, null), ServiceContext.MediaService, ServiceContext.UserService, new StaticCacheProvider(), ContentTypesCache, Factory.GetInstance()); var doc = store.CreateFromCacheValues(store.ConvertFromSearchResult(result)); diff --git a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs index 9585c98cb2..21ea961636 100644 --- a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs +++ b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs @@ -194,8 +194,8 @@ namespace Umbraco.Tests.Composing var register = GetRegister(); // define two instances - register.RegisterInstance(typeof(Thing1), new Thing1()); - register.RegisterInstance(typeof(Thing1), new Thing2()); + register.Register(typeof(Thing1), new Thing1()); + register.Register(typeof(Thing1), new Thing2()); var factory = register.CreateFactory(); @@ -212,8 +212,8 @@ namespace Umbraco.Tests.Composing var register = GetRegister(); // define two instances - register.RegisterInstance(typeof(IThing), new Thing1()); - register.RegisterInstance(typeof(IThing), new Thing2()); + register.Register(typeof(IThing), new Thing1()); + register.Register(typeof(IThing), new Thing2()); var factory = register.CreateFactory(); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index c5b8e21870..5f3a51f4f6 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -195,7 +195,7 @@ namespace Umbraco.Tests.PublishedContent [Test] public void PublishedContentQueryTypedContentList() { - var query = new PublishedContentQuery(UmbracoContext.Current.ContentCache, UmbracoContext.Current.MediaCache); + var query = new PublishedContentQuery(UmbracoContext.Current.ContentCache, UmbracoContext.Current.MediaCache, UmbracoContext.Current.VariationContextAccessor); var result = query.Content(new[] { 1, 2, 4 }).ToArray(); Assert.AreEqual(2, result.Length); Assert.AreEqual(1, result[0].Id); diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index 3f2740291b..6c5e9a74b5 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -540,7 +540,7 @@ namespace Umbraco.Tests.Scoping var scopeProvider = ScopeProvider; bool? completed = null; - Exception exception = null; + bool? completed2 = null; Assert.IsNull(scopeProvider.AmbientScope); using (var scope = scopeProvider.CreateScope()) @@ -551,15 +551,7 @@ namespace Umbraco.Tests.Scoping // at that point the scope is gone, but the context is still there var ambientContext = scopeProvider.AmbientContext; - - try - { - ambientContext.Enlist("another", c2 => { }); - } - catch (Exception e) - { - exception = e; - } + ambientContext.Enlist("another", c2 => { completed2 = c2; }); }); if (complete) scope.Complete(); @@ -567,8 +559,8 @@ namespace Umbraco.Tests.Scoping Assert.IsNull(scopeProvider.AmbientScope); Assert.IsNull(scopeProvider.AmbientContext); Assert.IsNotNull(completed); - Assert.IsNotNull(exception); - Assert.IsInstanceOf(exception); + Assert.AreEqual(complete,completed.Value); + Assert.AreEqual(complete, completed2.Value); } [Test] diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 28b48629be..8d3900088c 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,7 +79,7 @@ - + 1.8.9 diff --git a/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs index 8d7a446ccb..8bdb0c71c7 100644 --- a/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs @@ -179,7 +179,7 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoExamineIndex.PublishedFieldName] = 1 + [UmbracoExamineIndex.PublishedFieldName] = "y" })); Assert.AreEqual(ValueSetValidationResult.Valid, result); } @@ -213,7 +213,7 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoExamineIndex.PublishedFieldName] = 0 + [UmbracoExamineIndex.PublishedFieldName] = "n" })); Assert.AreEqual(ValueSetValidationResult.Failed, result); @@ -222,7 +222,7 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoExamineIndex.PublishedFieldName] = 1 + [UmbracoExamineIndex.PublishedFieldName] = "y" })); Assert.AreEqual(ValueSetValidationResult.Valid, result); } @@ -237,8 +237,8 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoContentIndex.VariesByCultureFieldName] = 1, - [UmbracoExamineIndex.PublishedFieldName] = 0 + [UmbracoContentIndex.VariesByCultureFieldName] = "y", + [UmbracoExamineIndex.PublishedFieldName] = "n" })); Assert.AreEqual(ValueSetValidationResult.Failed, result); @@ -247,8 +247,8 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoContentIndex.VariesByCultureFieldName] = 1, - [UmbracoExamineIndex.PublishedFieldName] = 1 + [UmbracoContentIndex.VariesByCultureFieldName] = "y", + [UmbracoExamineIndex.PublishedFieldName] = "y" })); Assert.AreEqual(ValueSetValidationResult.Valid, result); @@ -257,14 +257,14 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoContentIndex.VariesByCultureFieldName] = 1, - [$"{UmbracoExamineIndex.PublishedFieldName}_en-us"] = 1, + [UmbracoContentIndex.VariesByCultureFieldName] = "y", + [$"{UmbracoExamineIndex.PublishedFieldName}_en-us"] = "y", ["hello_en-us"] = "world", ["title_en-us"] = "my title", - [$"{UmbracoExamineIndex.PublishedFieldName}_es-es"] = 0, + [$"{UmbracoExamineIndex.PublishedFieldName}_es-es"] = "n", ["hello_es-ES"] = "world", ["title_es-ES"] = "my title", - [UmbracoExamineIndex.PublishedFieldName] = 1 + [UmbracoExamineIndex.PublishedFieldName] = "y" }); Assert.AreEqual(10, valueSet.Values.Count()); Assert.IsTrue(valueSet.Values.ContainsKey($"{UmbracoExamineIndex.PublishedFieldName}_es-es")); 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.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 0af327d148..22ac11b991 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -248,6 +248,17 @@ gulp.task('dependencies', function () { "src": ["./node_modules/bootstrap-social/bootstrap-social.css"], "base": "./node_modules/bootstrap-social" }, + + { + "name": "angular-chart.js", + "src": ["./node_modules/angular-chart.js/dist/angular-chart.min.js"], + "base": "./node_modules/angular-chart.js/dist" + }, + { + "name": "chart.js", + "src": ["./node_modules/chart.js/dist/chart.min.js"], + "base": "./node_modules/chart.js/dist" + }, { "name": "clipboard", "src": ["./node_modules/clipboard/dist/clipboard.min.js"], diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 750be3fa80..f6d915eede 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -852,6 +852,26 @@ "resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.7.5.tgz", "integrity": "sha512-kU/fHIGf2a4a3bH7E1tzALTHk+QfoUSCK9fEcMFisd6ZWvNDwPzXWAilItqOC3EDiAXPmGHaNc9/aXiD9xrAxQ==" }, + "angular-chart.js": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/angular-chart.js/-/angular-chart.js-1.1.1.tgz", + "integrity": "sha1-SfDhjQgXYrbUyXkeSHr/L7sw9a4=", + "requires": { + "angular": "1.x", + "chart.js": "2.3.x" + }, + "dependencies": { + "chart.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.3.0.tgz", + "integrity": "sha1-QEYOSOLEF8BfwzJc2E97AA3H19Y=", + "requires": { + "chartjs-color": "^2.0.0", + "moment": "^2.10.6" + } + } + } + }, "angular-cookies": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/angular-cookies/-/angular-cookies-1.7.5.tgz", @@ -1970,6 +1990,39 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "chart.js": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.3.tgz", + "integrity": "sha512-3+7k/DbR92m6BsMUYP6M0dMsMVZpMnwkUyNSAbqolHKsbIzH2Q4LWVEHHYq7v0fmEV8whXE0DrjANulw9j2K5g==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz", + "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=", + "requires": { + "chartjs-color-string": "^0.5.0", + "color-convert": "^0.5.3" + }, + "dependencies": { + "color-convert": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz", + "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0=" + } + } + }, + "chartjs-color-string": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz", + "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==", + "requires": { + "color-name": "^1.0.0" + } + }, "chokidar": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", @@ -2216,8 +2269,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "color-string": { "version": "1.5.3", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 2002cca0a1..e35fdc3442 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -8,6 +8,7 @@ "ace-builds": "1.4.2", "angular": "1.7.5", "angular-animate": "1.7.5", + "angular-chart.js": "^1.1.1", "angular-cookies": "1.7.5", "angular-dynamic-locale": "0.1.37", "angular-i18n": "1.7.5", @@ -20,6 +21,7 @@ "angular-ui-sortable": "0.19.0", "animejs": "2.2.0", "bootstrap-social": "5.1.1", + "chart.js": "^2.7.3", "clipboard": "2.0.4", "diff": "3.5.0", "flatpickr": "4.5.2", diff --git a/src/Umbraco.Web.UI.Client/src/app.js b/src/Umbraco.Web.UI.Client/src/app.js index c7b813c1bf..8e0eaa4943 100644 --- a/src/Umbraco.Web.UI.Client/src/app.js +++ b/src/Umbraco.Web.UI.Client/src/app.js @@ -14,7 +14,8 @@ var app = angular.module('umbraco', [ 'ngMessages', 'tmh.dynamicLocale', 'ngFileUpload', - 'LocalStorageModule' + 'LocalStorageModule', + 'chart.js' ]); app.config(['$compileProvider', function ($compileProvider) { @@ -76,7 +77,7 @@ angular.module("umbraco.viewcache", []) var _op = (url.indexOf("?") > 0) ? "&" : "?"; url += _op + "umb__rnd=" + rnd; } - + return get(url, config); }; return $delegate; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js index c2c9ec068b..9390d64cdb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js @@ -54,7 +54,6 @@ @param {boolean} checked Set to true or false to toggle the switch. -@param {boolean} disabled Set to true or false to disable the switch. @param {callback} onClick The function which should be called when the toggle is clicked. @param {string=} showLabels Set to true or false to show a "On" or "Off" label next to the switch. @param {string=} labelOn Set a custom label for when the switched is turned on. It will default to "On". diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 71fbabe943..b65bf447e7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -97,9 +97,9 @@ } //load in the audit trail if we are currently looking at the INFO tab - if (umbVariantContentCtrl) { + if (umbVariantContentCtrl && umbVariantContentCtrl.editor) { var activeApp = _.find(umbVariantContentCtrl.editor.content.apps, a => a.active); - if (activeApp.alias === "umbInfo") { + if (activeApp && activeApp.alias === "umbInfo") { isInfoTab = true; loadAuditTrail(); loadRedirectUrls(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js index 8bad5ae8fd..3d784c999f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js @@ -130,8 +130,7 @@ if (!changes.value.isFirstChange() && changes.value.currentValue !== changes.value.previousValue) { configureViewModel(); - //this is required to re-validate - vm.tagEditorForm.tagCount.$setViewValue(vm.viewModel.length); + reValidate() } } @@ -182,6 +181,8 @@ else { vm.onValueChanged({ value: [] }); } + + reValidate(); } /** @@ -189,7 +190,7 @@ */ function validateMandatory() { return { - isValid: !vm.validation.mandatory || (vm.viewModel != null && vm.viewModel.length > 0), + isValid: !vm.validation.mandatory || (vm.viewModel != null && vm.viewModel.length > 0)|| (vm.value != null && vm.value.length > 0), errorMsg: "Value cannot be empty", errorKey: "required" }; @@ -271,6 +272,10 @@ }); } + function reValidate() { + //this is required to re-validate + vm.tagEditorForm.tagCount.$setViewValue(vm.viewModel.length); + } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js index 47d1431e13..56a18a217e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js @@ -86,20 +86,21 @@ angular.module("umbraco.directives") function generateAlias(value) { if (generateAliasTimeout) { - $timeout.cancel(generateAliasTimeout); + $timeout.cancel(generateAliasTimeout); } - if( value !== undefined && value !== "" && value !== null) { + if (value !== undefined && value !== "" && value !== null) { - scope.alias = ""; + scope.alias = ""; scope.placeholderText = scope.labels.busy; generateAliasTimeout = $timeout(function () { updateAlias = true; entityResource.getSafeAlias(value, true).then(function (safeAlias) { if (updateAlias) { - scope.alias = safeAlias.alias; - } + scope.alias = safeAlias.alias; + } + scope.placeholderText = scope.labels.idle; }); }, 500); @@ -108,7 +109,6 @@ angular.module("umbraco.directives") scope.alias = ""; scope.placeholderText = scope.labels.idle; } - } // if alias gets unlocked - stop watching alias @@ -119,17 +119,17 @@ angular.module("umbraco.directives") })); // validate custom entered alias - eventBindings.push(scope.$watch('alias', function(newValue, oldValue){ - - if(scope.alias === "" && bindWatcher === true || scope.alias === null && bindWatcher === true) { - // add watcher - eventBindings.push(scope.$watch('aliasFrom', function(newValue, oldValue) { - if(bindWatcher) { - generateAlias(newValue); - } - })); - } - + eventBindings.push(scope.$watch('alias', function (newValue, oldValue) { + if (scope.alias === "" || scope.alias === null || scope.alias === undefined) { + if (bindWatcher === true) { + // add watcher + eventBindings.push(scope.$watch('aliasFrom', function (newValue, oldValue) { + if (bindWatcher) { + generateAlias(newValue); + } + })); + } + } })); // clean up diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js index bb1dad1dbd..1fe739154a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.codefileResource * @description Loads in data for files that contain code such as js scripts, partial views and partial view macros **/ -function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { +function codefileResource($q, $http, umbDataFormatter, umbRequestHelper, localizationService) { return { @@ -106,13 +106,16 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ deleteByPath: function (type, virtualpath) { + + var promise = localizationService.localize("codefile_deleteItemFailed", [virtualpath]); + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "codeFileApiBaseUrl", "Delete", [{ type: type }, { virtualPath: virtualpath}])), - "Failed to delete item: " + virtualpath); + promise); }, /** @@ -236,13 +239,19 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ - createContainer: function(type, parentId, name) { + createContainer: function (type, parentId, name) { + + // Is the parent ID numeric? + var key = "codefile_createFolderFailedBy" + (isNaN(parseInt(parentId)) ? "Name" : "Id"); + + var promise = localizationService.localize(key, [parentId]); + return umbRequestHelper.resourcePromise( $http.post(umbRequestHelper.getApiUrl( "codeFileApiBaseUrl", "PostCreateContainer", { type: type, parentId: parentId, name: encodeURIComponent(name) })), - 'Failed to create a folder under parent id ' + parentId); + promise); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js index f969864ba1..377bb415fc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/template.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.templateResource * @description Loads in data for templates **/ -function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { +function templateResource($q, $http, umbDataFormatter, umbRequestHelper, localizationService) { return { @@ -152,13 +152,16 @@ function templateResource($q, $http, umbDataFormatter, umbRequestHelper) { * */ deleteById: function(id) { + + var promise = localizationService.localize("template_deleteByIdFailed", [id]); + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( "templateApiBaseUrl", "DeleteById", [{ id: id }])), - "Failed to delete item " + id); + promise); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js index 06b82d6eab..e023c6d23c 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js @@ -9,7 +9,7 @@ * * @param {navigationService} navigationService A reference to the navigationService */ -function NavigationController($scope, $rootScope, $location, $log, $q, $routeParams, $timeout, treeService, appState, navigationService, keyboardService, historyService, eventsService, angularHelper, languageResource, contentResource) { +function NavigationController($scope, $rootScope, $location, $log, $q, $routeParams, $timeout, $cookies, treeService, appState, navigationService, keyboardService, historyService, eventsService, angularHelper, languageResource, contentResource) { //this is used to trigger the tree to start loading once everything is ready var treeInitPromise = $q.defer(); @@ -344,9 +344,6 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.languages = languages; if ($scope.languages.length > 1) { - var defaultLang = _.find($scope.languages, function (l) { - return l.isDefault; - }); //if there's already one set, check if it exists var currCulture = null; var mainCulture = $location.search().mculture; @@ -356,7 +353,20 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar }); } if (!currCulture) { - $location.search("mculture", defaultLang ? defaultLang.culture : null); + // no culture in the request, let's look for one in the cookie that's set when changing language + var defaultCulture = $cookies.get("UMB_MCULTURE"); + if (!defaultCulture || !_.find($scope.languages, function (l) { + return l.culture.toLowerCase() === defaultCulture.toLowerCase(); + })) { + // no luck either, look for the default language + var defaultLang = _.find($scope.languages, function (l) { + return l.isDefault; + }); + if (defaultLang) { + defaultCulture = defaultLang.culture; + } + } + $location.search("mculture", defaultCulture ? defaultCulture : null); } } @@ -391,6 +401,10 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.selectLanguage = function (language) { $location.search("mculture", language.culture); + // add the selected culture to a cookie so the user will log back into the same culture later on (cookie lifetime = one year) + var expireDate = new Date(); + expireDate.setDate(expireDate.getDate() + 365); + $cookies.put("UMB_MCULTURE", language.culture, {path: "/", expires: expireDate}); // close the language selector $scope.page.languageSelectorIsOpen = false; diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index a28c128706..a65b94444e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -161,6 +161,7 @@ @import "components/umb-file-dropzone.less"; @import "components/umb-node-preview.less"; @import "components/umb-mini-editor.less"; +@import "components/umb-property-file-upload.less"; @import "components/users/umb-user-cards.less"; @import "components/users/umb-user-details.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less index 4803c05f6e..b7c58ad3cf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less @@ -1,5 +1,5 @@ -.umb-file-dropzone-directive{ +.umb-file-dropzone { // drop zone // tall and small version - animate height diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index df8977a2bf..6ddd9d8d50 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -3,6 +3,13 @@ position: relative; } +.umb-nested-content-property-container { + position: relative; + &:not(:last-child){ + margin-bottom: 12px; + } +} + .umb-nested-content--not-supported { opacity: 0.3; pointer-events: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less new file mode 100644 index 0000000000..08b1a1b5e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-file-upload.less @@ -0,0 +1,28 @@ +.umb-property-file-upload { + + .umb-upload-button-big { + display: block; + padding: 20px; + opacity: 1; + border: 1px dashed @gray-8; + background: none; + text-align: center; + font-size: 14px; + + &, &:hover { + color: @gray-8; + } + + i.icon { + font-size: 55px; + line-height: 70px + } + + input { + left: 0; + bottom: 0; + height: 100%; + width: 100%; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index e2d4cb708d..3ec8c383da 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -1,3 +1,5 @@ +@checkered-background: url(../img/checkered-background.png); + // // Container styles // -------------------------------------------------- @@ -353,7 +355,7 @@ max-height:100%; margin:auto; display:block; - background-image: url(../img/checkered-background.png); + background-image: @checkered-background; } .umb-sortable-thumbnails li .trashed { @@ -576,12 +578,18 @@ vertical-align: top; } - .gravity-container .viewport { - max-width: 600px; - } + .gravity-container { + border: 1px solid @gray-8; + line-height: 0; - .gravity-container .viewport:hover { - cursor: pointer; + .viewport { + max-width: 600px; + background: @checkered-background; + + &:hover { + cursor: pointer; + } + } } .imagecropper { @@ -594,6 +602,10 @@ float: left; max-width: 100%; } + + .viewport img { + background: @checkered-background; + } } .imagecropper .umb-cropper__container { @@ -687,7 +699,7 @@ // // folder-browser // -------------------------------------------------- -.umb-folderbrowser .add-link{ +.umb-folderbrowser .add-link { display: inline-block; height: 120px; width: 120px; @@ -696,17 +708,6 @@ line-height: 120px } -.umb-upload-button-big:hover{color: @gray-8;} - -.umb-upload-button-big {display: block} -.umb-upload-button-big input { - left: 0; - bottom: 0; - height: 100%; - width: 100%; -} - - // // File upload // -------------------------------------------------- @@ -724,6 +725,10 @@ list-style: none; vertical-align: middle; margin-bottom: 0; + + img { + background: @checkered-background; + } } .umb-fileupload label { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 2591789a36..360c6b2bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -24,7 +24,7 @@ - .{{item.extension}} + .{{item.extension}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html index 9d2997625e..c99655b7fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -1,4 +1,4 @@ -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index f205dffa57..dea532b7ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -1,4 +1,5 @@ -
+
+
0 && userData.startContentIds.indexOf(-1) == -1; + }); + + function nodeSelectHandler(args) { + + if (args && args.event) { + args.event.preventDefault(); + args.event.stopPropagation(); + } + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + + } + + function nodeExpandedHandler(args) { + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + } + + $scope.hideSearch = function () { + $scope.searchInfo.showSearch = false; + $scope.searchInfo.results = []; + } + + // method to select a search result + $scope.selectResult = function (evt, result) { + result.selected = result.selected === true ? false : true; + nodeSelectHandler(evt, { event: evt, node: result }); + }; + + //callback when there are search results + $scope.onSearchResults = function (results) { + $scope.searchInfo.results = results; + $scope.searchInfo.showSearch = true; + }; + + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + $scope.dialogTreeApi.callbacks.treeNodeExpanded(nodeExpandedHandler); + } + + // Mini list view + $scope.selectListViewNode = function (node) { + node.selected = node.selected === true ? false : true; + nodeSelectHandler({}, { node: node }); + }; + + $scope.closeMiniListView = function () { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } relationResource.getByChildId($scope.source.id, "relateParentDocumentOnDelete").then(function (data) { $scope.loading = false; if (!data.length) { - localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_noRestoreRelation"]) - .then(function(values) { - $scope.success = false; - $scope.error = { - errorMsg: values[0], - data: { - Message: values[1] - } - } - }); + $scope.moving = true; return; } @@ -30,40 +95,32 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" $scope.target = { id: -1, name: "Root" }; } else { - $scope.loading = true; + $scope.loading = true; + entityResource.getById($scope.relation.parentId, "Document").then(function (data) { $scope.loading = false; - $scope.target = data; - // make sure the target item isn't in the recycle bin - if($scope.target.path.indexOf("-20") !== -1) { - localizationService.localizeMany(["recycleBin_itemCannotBeRestored", "recycleBin_restoreUnderRecycled"]) - .then(function (values) { - $scope.success = false; - $scope.error = { - errorMsg: values[0], - data: { - Message: values[1].replace('%0%', $scope.target.name) - } - } - }); - $scope.success = false; - } + $scope.target = data; + // make sure the target item isn't in the recycle bin + if ($scope.target.path.indexOf("-20") !== -1) { + $scope.moving = true; + $scope.target = null; + } }, function (err) { - $scope.success = false; - $scope.error = err; $scope.loading = false; + $scope.error = err; }); } }, function (err) { - $scope.success = false; - $scope.error = err; + $scope.loading = false; + $scope.error = err; }); $scope.restore = function () { $scope.loading = true; - // this code was copied from `content.move.controller.js` + + // this code was copied from `content.move.controller.js` contentResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { @@ -88,9 +145,8 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" }); }, function (err) { - $scope.success = false; - $scope.error = err; $scope.loading = false; + $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/restore.html b/src/Umbraco.Web.UI.Client/src/views/content/restore.html index 83a69effb6..ead6ed91ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/restore.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/restore.html @@ -1,34 +1,93 @@
-
- +
+ + + - - +
+
+
{{error.errorMsg}}
+
{{error.data.Message}}
+
+
-

- Restore {{source.name}} under {{target.name}}? -

+
+
+ {{source.name}} + was restored under + was moved underneath + {{target.name}} +
+ +
-
-
-
{{error.errorMsg}}
-
{{error.data.Message}}
-
-
+
-
-
- {{source.name}} was moved underneath {{target.name}} -
- -
+

+ Restore {{source.name}} under {{target.name}}? +

- +
+ +
+
+
+
Cannot automatically restore this item
+
There is no location where this item can be automatically restored. You can move the item manually using the tree below.
+
+
+ +
+ + + +
+ + + + +
+ + +
+
+ + + + +
+ +
- + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html index 1165776be6..3cca62b2b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/create.html @@ -57,7 +57,7 @@
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js index b25ac3c7b8..1e615ad0d4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.controller.js @@ -12,6 +12,9 @@ function PartialViewsDeleteController($scope, codefileResource, treeService, nav //mark it for deletion (used in the UI) $scope.currentNode.loading = true; + + // Reset the error message + $scope.error = null; codefileResource.deleteByPath('partialViews', $scope.currentNode.id) .then(function() { @@ -21,6 +24,9 @@ function PartialViewsDeleteController($scope, codefileResource, treeService, nav //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); + }, function (err) { + $scope.currentNode.loading = false; + $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html index 0f75e8514e..c0fdf2c77f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialviews/delete.html @@ -1,6 +1,13 @@
+
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
+
+

Are you sure you want to delete {{currentNode.name}} ?

diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js index 17be7718d6..79ae34ea21 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js @@ -140,6 +140,11 @@ function ColorPickerController($scope) { if (!$scope.model.value) return; + // Backwards compatibility, the color used to be stored as a hex value only + if (typeof $scope.model.value === "string") { + $scope.model.value = { value: $scope.model.value, label: $scope.model.value }; + } + // Complex color (value and label)? if (!$scope.model.value.hasOwnProperty("value")) return; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 5fc986dca9..af3ce50609 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -98,7 +98,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper opacity: 0.7, tolerance: "pointer", scroll: true, - zIndex: 6000 + zIndex: 6000, + update: function (e, ui) { + angularHelper.getCurrentForm($scope).$setDirty(); + } }; if ($scope.model.config) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 96e7ccdbfd..c84644d251 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,4 +1,4 @@ -function listViewController($scope, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, listViewHelper, navigationService, editorService, overlayService, languageResource) { +function listViewController($scope, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, listViewHelper, navigationService, editorService, overlayService, languageResource, mediaHelper) { //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content // that isn't created yet, if we continue this will use the parent id in the route params which isn't what @@ -279,10 +279,12 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs $scope.listViewResultSet = data; //update all values for display + var section = appState.getSectionState("currentSection"); if ($scope.listViewResultSet.items) { _.each($scope.listViewResultSet.items, function (e, index) { setPropertyValues(e); - if (e.contentTypeAlias === 'Folder') { + // create the folders collection (only for media list views) + if (section === "media" && !mediaHelper.hasFilePropertyType(e)) { $scope.folders.push(e); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 6e67d2d251..79e00fa453 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -81,25 +81,6 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop : undefined; }); - $scope.editIconTitle = ''; - $scope.moveIconTitle = ''; - $scope.deleteIconTitle = ''; - - // localize the edit icon title - localizationService.localize('general_edit').then(function (value) { - $scope.editIconTitle = value; - }); - - // localize the delete icon title - localizationService.localize('general_delete').then(function (value) { - $scope.deleteIconTitle = value; - }); - - // localize the move icon title - localizationService.localize('actions_move').then(function (value) { - $scope.moveIconTitle = value; - }); - $scope.nodes = []; $scope.currentNode = undefined; $scope.realCurrentNode = undefined; @@ -116,6 +97,11 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.showIcons = $scope.model.config.showIcons || true; $scope.wideMode = $scope.model.config.hideLabel == "1"; + $scope.labels = {}; + localizationService.localizeMany(["grid_insertControl"]).then(function(data) { + $scope.labels.docTypePickerTitle = data[0]; + }); + // helper to force the current form into the dirty state $scope.setDirty = function () { if ($scope.propertyForm) { @@ -138,7 +124,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop } $scope.overlayMenu = { - title: localizationService.localize('grid_insertControl'), + title: $scope.labels.docTypePickerTitle, show: false, style: {}, filter: $scope.scaffolds.length > 15 ? true : false, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index 83076b54a0..0cf67022c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -1,5 +1,5 @@ 
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html index 1ff6666907..572021aebd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.html @@ -12,13 +12,13 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index 688ac7693f..a61930f877 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -1,9 +1,12 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.TagsController", - function ($scope) { + function ($scope, angularHelper) { $scope.valueChanged = function(value) { $scope.model.value = value; + // the model value seems to be a reference to the same array, so we need + // to set the form as dirty explicitly when the content of the array changes + angularHelper.getCurrentForm($scope).$setDirty(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js index 8995cb1a31..d019a44a10 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/templates/delete.controller.js @@ -12,6 +12,10 @@ function TemplatesDeleteController($scope, templateResource , treeService, navig //mark it for deletion (used in the UI) $scope.currentNode.loading = true; + + // Reset the error message + $scope.error = null; + templateResource.deleteById($scope.currentNode.id).then(function () { $scope.currentNode.loading = false; @@ -21,6 +25,9 @@ function TemplatesDeleteController($scope, templateResource , treeService, navig //TODO: Need to sync tree, etc... treeService.removeNode($scope.currentNode); navigationService.hideMenu(); + }, function (err) { + $scope.currentNode.loading = false; + $scope.error = err; }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/delete.html b/src/Umbraco.Web.UI.Client/src/views/templates/delete.html index 34648aa43e..c98677f764 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/delete.html @@ -1,11 +1,18 @@
+
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
+
+

Are you sure you want to delete {{currentNode.name}} ?

- +
diff --git a/src/Umbraco.Web.UI/.eslintignore b/src/Umbraco.Web.UI/.eslintignore deleted file mode 100644 index 6cac59fac0..0000000000 --- a/src/Umbraco.Web.UI/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ - -/Umbraco/** -/Umbraco_Client/** diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 8787d78444..9d3297dbc2 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -88,7 +88,7 @@ - + @@ -206,7 +206,6 @@ - 404handlers.config diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 383dc855fd..7a370ba90e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -180,6 +180,11 @@ Overførsel af egenskaber kunne ikke fuldføres, da en eller flere egenskaber er indstillet til at blive overført mere end én gang. Kun andre dokumenttyper, der er gyldige på denne placering, vises. + + Oprettelse af mappen under parent med ID %0% fejlede + Oprettelse af mappen under parent med navnet %0% fejlede + Sletning af filen/mappen fejlede: %0% + Udgivet Om siden @@ -276,6 +281,7 @@ Hvor ønsker du at oprette den nye %0% Opret under Vælg den dokumenttype, du vil oprette en indholdsskabelon til + Angiv et navn for mappen Vælg en type og skriv en titel "dokument typer".]]> "media typer".]]> @@ -1062,6 +1068,7 @@ Mange hilsner fra Umbraco robotten Editor + Sletning af skabelonen med ID %0% fejlede Rediger skabelon Sektioner Indsæt indholdsområde diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 69f106a775..386d3af518 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -189,6 +189,11 @@ Could not complete property mapping as one or more properties have more than one mapping defined. Only alternate types valid for the current location are displayed. + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + Failed to delete item: %0% + Is Published About this page @@ -291,6 +296,7 @@ Where do you want to create the new %0% Create an item under Select the document type you want to make a content template for + Enter a folder name Choose a type and a title "document types".]]> "media types".]]> @@ -1248,6 +1254,7 @@ To manage your website, simply open the Umbraco back office and start adding con This Content Type uses as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template Add icon @@ -1347,6 +1354,7 @@ To manage your website, simply open the Umbraco back office and start adding con Editor + Failed to delete template with ID %0% Edit template Sections Insert content area @@ -1968,8 +1976,8 @@ To manage your website, simply open the Umbraco back office and start adding con Trashed content with Id: {0} related to original parent content with Id: {1} Trashed media with Id: {0} related to original parent media item with Id: {1} Cannot automatically restore this item - There is no 'restore' relation found for this node. Use the Move menu item to move it manually. - The item you want to restore it under ('%0%') is in the recycle bin. Use the Move menu item to move the item manually. + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under Direction 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 eaa1c6c39e..5de373f571 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -196,6 +196,11 @@ Could not complete property mapping as one or more properties have more than one mapping defined. Only alternate types valid for the current location are displayed. + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + Failed to delete item: %0% + Is Published About this page @@ -316,6 +321,7 @@ Where do you want to create the new %0% Create an item under Select the document type you want to make a content template for + Enter a folder name Choose a type and a title "document types".]]> "media types".]]> @@ -1272,6 +1278,7 @@ To manage your website, simply open the Umbraco back office and start adding con This Content Type uses as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template Add icon @@ -1387,6 +1394,7 @@ To manage your website, simply open the Umbraco back office and start adding con How the text will look like in the rich text editor. + Failed to delete template with ID %0% Edit template Sections Insert content area @@ -2024,8 +2032,8 @@ To manage your website, simply open the Umbraco back office and start adding con Trashed content with Id: {0} related to original parent content with Id: {1} Trashed media with Id: {0} related to original parent media item with Id: {1} Cannot automatically restore this item - There is no 'restore' relation found for this node. Use the Move menu item to move it manually. - The item you want to restore it under ('%0%') is in the recycle bin. Use the Move menu item to move the item manually. + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under Direction diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs index 81b133b9ef..d522e54de6 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs @@ -142,8 +142,8 @@ namespace Umbraco.Web.Cache () => ContentService.Saved -= ContentService_Saved); Bind(() => ContentService.Copied += ContentService_Copied, // needed for permissions () => ContentService.Copied -= ContentService_Copied); - Bind(() => ContentService.TreeChanged += ContentService_Changed,// handles all content changes - () => ContentService.TreeChanged -= ContentService_Changed); + Bind(() => ContentService.TreeChanged += ContentService_TreeChanged,// handles all content changes + () => ContentService.TreeChanged -= ContentService_TreeChanged); // TreeChanged should also deal with this //Bind(() => ContentService.SavedBlueprint += ContentService_SavedBlueprint, @@ -206,7 +206,7 @@ namespace Umbraco.Web.Cache { } - private void ContentService_Changed(IContentService sender, TreeChange.EventArgs args) + private void ContentService_TreeChanged(IContentService sender, TreeChange.EventArgs args) { _distributedCache.RefreshContentCache(args.Changes.ToArray()); } diff --git a/src/Umbraco.Web/Composing/ModuleInjector.cs b/src/Umbraco.Web/Composing/ModuleInjector.cs index 01930d55fd..57ef766dea 100644 --- a/src/Umbraco.Web/Composing/ModuleInjector.cs +++ b/src/Umbraco.Web/Composing/ModuleInjector.cs @@ -1,5 +1,4 @@ -using System; -using System.Web; +using System.Web; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; @@ -11,7 +10,7 @@ namespace Umbraco.Web.Composing /// /// The type of the injected module. public abstract class ModuleInjector : IHttpModule - where TModule : IHttpModule + where TModule : class, IHttpModule { protected TModule Module { get; private set; } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 9458ec6b8d..3b361fcf1e 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -357,7 +357,9 @@ namespace Umbraco.Web.Editors var mapped = MapToDisplay(emptyContent); // translate the content type name if applicable mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); - mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); + // if your user type doesn't have access to the Settings section it would not get this property mapped + if(mapped.DocumentType != null) + mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); //remove the listview app if it exists mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); diff --git a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs index 2b2bf337de..985c42bbbf 100644 --- a/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs +++ b/src/Umbraco.Web/Editors/Filters/UserGroupEditorAuthorizationHelper.cs @@ -53,6 +53,16 @@ namespace Umbraco.Web.Editors.Filters if (currentUser.IsAdmin()) return Attempt.Succeed(); + var existingGroups = _userService.GetUserGroupsByAlias(groupAliases); + + if(!existingGroups.Any()) + { + // We're dealing with new groups, + // so authorization should be given to any user with access to Users section + if (currentUser.AllowedSections.Contains(Constants.Applications.Users)) + return Attempt.Succeed(); + } + var userGroups = currentUser.Groups.Select(x => x.Alias).ToArray(); var missingAccess = groupAliases.Except(userGroups).ToArray(); return missingAccess.Length == 0 diff --git a/src/Umbraco.Web/Editors/UserGroupsController.cs b/src/Umbraco.Web/Editors/UserGroupsController.cs index 677890bcc4..5069d99cfe 100644 --- a/src/Umbraco.Web/Editors/UserGroupsController.cs +++ b/src/Umbraco.Web/Editors/UserGroupsController.cs @@ -31,6 +31,7 @@ namespace Umbraco.Web.Editors //authorize that the user has access to save this user group var authHelper = new UserGroupEditorAuthorizationHelper( Services.UserService, Services.ContentService, Services.MediaService, Services.EntityService); + var isAuthorized = authHelper.AuthorizeGroupAccess(Security.CurrentUser, userGroupSave.Alias); if (isAuthorized == false) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result)); @@ -51,6 +52,14 @@ namespace Umbraco.Web.Editors if (isAuthorized == false) throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.Unauthorized, isAuthorized.Result)); + //current user needs to be added to a new group if not an admin (possibly only if no other users are added?) to avoid a 401 + if(!Security.CurrentUser.IsAdmin() && (userGroupSave.Id == null || Convert.ToInt32(userGroupSave.Id) >= 0)/* && !userGroupSave.Users.Any() */) + { + var userIds = userGroupSave.Users.ToList(); + userIds.Add(Security.CurrentUser.Id); + userGroupSave.Users = userIds; + } + //save the group Services.UserService.Save(userGroupSave.PersistedUserGroup, userGroupSave.Users.ToArray()); diff --git a/src/Umbraco.Web/ExamineExtensions.cs b/src/Umbraco.Web/ExamineExtensions.cs index f1ed6c0659..9a9fa98d95 100644 --- a/src/Umbraco.Web/ExamineExtensions.cs +++ b/src/Umbraco.Web/ExamineExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Examine; using Umbraco.Core; diff --git a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs index 2e82fe252d..9c30f74ea7 100644 --- a/src/Umbraco.Web/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperRenderExtensions.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Exceptions; using Umbraco.Core.IO; +using Umbraco.Web.Models; using Umbraco.Web.Mvc; using Umbraco.Web.Security; using Current = Umbraco.Web.Composing.Current; @@ -831,5 +832,37 @@ 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/IPublishedContentQuery.cs b/src/Umbraco.Web/IPublishedContentQuery.cs index c3a065b9dc..76e7be5e97 100644 --- a/src/Umbraco.Web/IPublishedContentQuery.cs +++ b/src/Umbraco.Web/IPublishedContentQuery.cs @@ -40,6 +40,7 @@ namespace Umbraco.Web /// Optional index name. /// /// When the is not specified, all cultures are searched. + /// While enumerating results, the ambient culture is changed to be the searched culture. /// IEnumerable Search(string term, string culture = null, string indexName = null); @@ -54,17 +55,24 @@ namespace Umbraco.Web /// Optional index name. /// /// When the is not specified, all cultures are searched. + /// While enumerating results, the ambient culture is changed to be the searched culture. /// IEnumerable Search(string term, int skip, int take, out long totalRecords, string culture = null, string indexName = null); /// /// Executes the query and converts the results to PublishedSearchResult. /// + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// IEnumerable Search(IQueryExecutor query); /// /// Executes the query and converts the results to PublishedSearchResult. /// + /// + /// While enumerating results, the ambient culture is changed to be the searched culture. + /// IEnumerable Search(IQueryExecutor query, int skip, int take, out long totalRecords); } } diff --git a/src/Umbraco.Web/Media/Exif/ImageFile.cs b/src/Umbraco.Web/Media/Exif/ImageFile.cs index d0c3ef7411..acd8ce8eec 100644 --- a/src/Umbraco.Web/Media/Exif/ImageFile.cs +++ b/src/Umbraco.Web/Media/Exif/ImageFile.cs @@ -137,7 +137,8 @@ namespace Umbraco.Web.Media.Exif return new SvgFile(stream); } - throw new NotValidImageFileException(); + // We don't know + return null; } #endregion } diff --git a/src/Umbraco.Web/Media/ImageHelper.cs b/src/Umbraco.Web/Media/ImageHelper.cs index 3fbc1e060a..5a5724dc7d 100644 --- a/src/Umbraco.Web/Media/ImageHelper.cs +++ b/src/Umbraco.Web/Media/ImageHelper.cs @@ -33,7 +33,8 @@ namespace Umbraco.Web.Media //Try to load with exif var jpgInfo = ImageFile.FromStream(stream); - if (jpgInfo.Format != ImageFileFormat.Unknown + if (jpgInfo != null + && jpgInfo.Format != ImageFileFormat.Unknown && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) { diff --git a/src/Umbraco.Web/Media/TypeDetector/JpegDetector.cs b/src/Umbraco.Web/Media/TypeDetector/JpegDetector.cs index 287709f9c6..d06671f667 100644 --- a/src/Umbraco.Web/Media/TypeDetector/JpegDetector.cs +++ b/src/Umbraco.Web/Media/TypeDetector/JpegDetector.cs @@ -7,8 +7,7 @@ namespace Umbraco.Web.Media.TypeDetector public static bool IsOfType(Stream fileStream) { var header = GetFileHeader(fileStream); - - return header[0] == 0xff && header[1] == 0xD8; + return header != null && header[0] == 0xff && header[1] == 0xD8; } } } diff --git a/src/Umbraco.Web/Media/TypeDetector/RasterizedTypeDetector.cs b/src/Umbraco.Web/Media/TypeDetector/RasterizedTypeDetector.cs index 4bb074e5a2..a4ded5f8c4 100644 --- a/src/Umbraco.Web/Media/TypeDetector/RasterizedTypeDetector.cs +++ b/src/Umbraco.Web/Media/TypeDetector/RasterizedTypeDetector.cs @@ -7,9 +7,13 @@ namespace Umbraco.Web.Media.TypeDetector public static byte[] GetFileHeader(Stream fileStream) { fileStream.Seek(0, SeekOrigin.Begin); - byte[] header = new byte[8]; + var header = new byte[8]; fileStream.Seek(0, SeekOrigin.Begin); + // Invalid header + if (fileStream.Read(header, 0, header.Length) != header.Length) + return null; + return header; } } diff --git a/src/Umbraco.Web/Media/TypeDetector/TIFFDetector.cs b/src/Umbraco.Web/Media/TypeDetector/TIFFDetector.cs index 08126136b8..7adb1cd9de 100644 --- a/src/Umbraco.Web/Media/TypeDetector/TIFFDetector.cs +++ b/src/Umbraco.Web/Media/TypeDetector/TIFFDetector.cs @@ -7,17 +7,17 @@ namespace Umbraco.Web.Media.TypeDetector { public static bool IsOfType(Stream fileStream) { - string tiffHeader = GetFileHeader(fileStream); - - return tiffHeader == "MM\x00\x2a" || tiffHeader == "II\x2a\x00"; + var tiffHeader = GetFileHeader(fileStream); + return tiffHeader != null && tiffHeader == "MM\x00\x2a" || tiffHeader == "II\x2a\x00"; } public static string GetFileHeader(Stream fileStream) { var header = RasterizedTypeDetector.GetFileHeader(fileStream); + if (header == null) + return null; - string tiffHeader = Encoding.ASCII.GetString(header, 0, 4); - + var tiffHeader = Encoding.ASCII.GetString(header, 0, 4); return tiffHeader; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 4ebddee6dc..dc8fcf8c0e 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Web.Hosting; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; @@ -15,7 +14,6 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.Repositories.Implement; @@ -26,7 +24,6 @@ using Umbraco.Core.Services.Implement; using Umbraco.Web.Cache; using Umbraco.Web.Install; using Umbraco.Web.PublishedCache.NuCache.DataSource; -using Umbraco.Web.PublishedCache.XmlPublishedCache; using Umbraco.Web.Routing; namespace Umbraco.Web.PublishedCache.NuCache diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs index a845be286f..6f6a39144a 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/DictionaryPublishedContent.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache LoadedFromExamine = fromExamine; ValidateAndSetProperty(valueDictionary, val => _id = Int32.Parse(val), "id", "nodeId", "__NodeId"); //should validate the int! - ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key"); + ValidateAndSetProperty(valueDictionary, val => _key = Guid.Parse(val), "key", "__key", "__Key"); //ValidateAndSetProperty(valueDictionary, val => _templateId = int.Parse(val), "template", "templateId"); ValidateAndSetProperty(valueDictionary, val => _sortOrder = Int32.Parse(val), "sortOrder"); ValidateAndSetProperty(valueDictionary, val => _name = val, "nodeName"); diff --git a/src/Umbraco.Web/PublishedContentQuery.cs b/src/Umbraco.Web/PublishedContentQuery.cs index 3d8f36ec1a..2c4d08502e 100644 --- a/src/Umbraco.Web/PublishedContentQuery.cs +++ b/src/Umbraco.Web/PublishedContentQuery.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -24,16 +25,19 @@ namespace Umbraco.Web { private readonly IPublishedContentCache _contentCache; private readonly IPublishedMediaCache _mediaCache; + private readonly IVariationContextAccessor _variationContextAccessor; /// /// Constructor used to return results from the caches /// /// /// - public PublishedContentQuery(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache) + /// + public PublishedContentQuery(IPublishedContentCache contentCache, IPublishedMediaCache mediaCache, IVariationContextAccessor variationContextAccessor) { _contentCache = contentCache ?? throw new ArgumentNullException(nameof(contentCache)); _mediaCache = mediaCache ?? throw new ArgumentNullException(nameof(mediaCache)); + _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); } #region Content @@ -201,6 +205,9 @@ namespace Umbraco.Web // default to max 500 results var count = skip == 0 && take == 0 ? 500 : skip + take; + //set this to the specific culture or to the culture in the request + culture = culture ?? _variationContextAccessor.VariationContext.Culture; + ISearchResults results; if (culture.IsNullOrWhiteSpace()) { @@ -209,23 +216,15 @@ namespace Umbraco.Web else { //get all index fields suffixed with the culture name supplied - var cultureFields = new List(); - var fields = umbIndex.GetFields(); - var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, 1); //must vary by culture - // ReSharper disable once LoopCanBeConvertedToQuery - foreach (var field in fields) - { - var match = CultureIsoCodeFieldName.Match(field); - if (match.Success && match.Groups.Count == 2 && culture.InvariantEquals(match.Groups[1].Value)) - cultureFields.Add(field); - } - + var cultureFields = umbIndex.GetCultureFields(culture); + var qry = searcher.CreateQuery().Field(UmbracoContentIndex.VariesByCultureFieldName, "y"); //must vary by culture qry = qry.And().ManagedQuery(term, cultureFields.ToArray()); results = qry.Execute(count); } totalRecords = results.TotalItemCount; - return results.ToPublishedSearchResults(_contentCache); + + return new CultureContextualSearchResults(results.ToPublishedSearchResults(_contentCache), _variationContextAccessor, culture); } /// @@ -246,12 +245,76 @@ namespace Umbraco.Web } /// - /// Matches a culture iso name suffix + /// This is used to contextualize the values in the search results when enumerating over them so that the correct culture values are used /// - /// - /// myFieldName_en-us will match the "en-us" - /// - private static readonly Regex CultureIsoCodeFieldName = new Regex("^[_\\w]+_([a-z]{2}-[a-z0-9]{2,4})$", RegexOptions.Compiled); + private class CultureContextualSearchResults : IEnumerable + { + private readonly IEnumerable _wrapped; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly string _culture; + + public CultureContextualSearchResults(IEnumerable wrapped, IVariationContextAccessor variationContextAccessor, string culture) + { + _wrapped = wrapped; + _variationContextAccessor = variationContextAccessor; + _culture = culture; + } + + public IEnumerator GetEnumerator() + { + //We need to change the current culture to what is requested and then change it back + var originalContext = _variationContextAccessor.VariationContext; + if (!_culture.IsNullOrWhiteSpace() && !_culture.InvariantEquals(originalContext.Culture)) + _variationContextAccessor.VariationContext = new VariationContext(_culture); + + //now the IPublishedContent returned will be contextualized to the culture specified and will be reset when the enumerator is disposed + return new CultureContextualSearchResultsEnumerator(_wrapped.GetEnumerator(), _variationContextAccessor, originalContext); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Resets the variation context when this is disposed + /// + private class CultureContextualSearchResultsEnumerator : IEnumerator + { + private readonly IEnumerator _wrapped; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly VariationContext _originalContext; + + public CultureContextualSearchResultsEnumerator(IEnumerator wrapped, IVariationContextAccessor variationContextAccessor, VariationContext originalContext) + { + _wrapped = wrapped; + _variationContextAccessor = variationContextAccessor; + _originalContext = originalContext; + } + + public void Dispose() + { + _wrapped.Dispose(); + //reset + _variationContextAccessor.VariationContext = _originalContext; + } + + public bool MoveNext() + { + return _wrapped.MoveNext(); + } + + public void Reset() + { + _wrapped.Reset(); + } + + public PublishedSearchResult Current => _wrapped.Current; + object IEnumerator.Current => Current; + } + } + + #endregion diff --git a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs index 99b4e22b5a..1f01270bc6 100644 --- a/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs +++ b/src/Umbraco.Web/Routing/ContentFinderByLegacy404.cs @@ -63,7 +63,7 @@ namespace Umbraco.Web.Routing var error404 = NotFoundHandlerHelper.GetCurrentNotFoundPageId( _contentConfigSection.Error404Collection.ToArray(), _entityService, - new PublishedContentQuery(frequest.UmbracoContext.ContentCache, frequest.UmbracoContext.MediaCache), + new PublishedContentQuery(frequest.UmbracoContext.ContentCache, frequest.UmbracoContext.MediaCache, frequest.UmbracoContext.VariationContextAccessor), errorCulture); IPublishedContent content = null; diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index 47e73d383c..a60e5f1d1b 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -367,9 +367,9 @@ namespace Umbraco.Web.Search { m.AdditionalData["Email"] = result.Values["email"]; } - if (result.Values.ContainsKey("__key") && result.Values["__key"] != null) + if (result.Values.ContainsKey(UmbracoExamineIndex.NodeKeyFieldName) && result.Values[UmbracoExamineIndex.NodeKeyFieldName] != null) { - if (Guid.TryParse(result.Values["__key"], out var key)) + if (Guid.TryParse(result.Values[UmbracoExamineIndex.NodeKeyFieldName], out var key)) { m.Key = key; } @@ -416,7 +416,7 @@ namespace Umbraco.Web.Search if (intId.Success) { //if it varies by culture, return the default language URL - if (result.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var varies) && varies == "1") + if (result.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var varies) && varies == "y") { entity.AdditionalData["Url"] = _umbracoHelper.Url(intId.Result, defaultLang); } diff --git a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js index 75ca46437a..62d294ee6f 100644 --- a/src/Umbraco.Web/UI/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/UI/JavaScript/JsInitialize.js @@ -23,8 +23,11 @@ 'lib/ng-file-upload/ng-file-upload.min.js', 'lib/angular-local-storage/angular-local-storage.min.js', + 'lib/chart.js/chart.min.js', + 'lib/angular-chart.js/angular-chart.min.js', + 'lib/umbraco/Extensions.js', - + 'lib/umbraco/NamespaceManager.js', 'lib/umbraco/LegacySpeechBubble.js', diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 36550f9b54..f8f33ca0fa 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 2.6.2.25 diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index 6914efb3e2..f4be2a1700 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -106,7 +106,7 @@ namespace Umbraco.Web /// Gets the query context. /// public IPublishedContentQuery ContentQuery => _query ?? - (_query = new PublishedContentQuery(UmbracoContext.ContentCache, UmbracoContext.MediaCache)); + (_query = new PublishedContentQuery(UmbracoContext.ContentCache, UmbracoContext.MediaCache, UmbracoContext.VariationContextAccessor)); /// /// Gets the Umbraco context.