diff --git a/.github/BUILD.md b/.github/BUILD.md index c6e870f396..ad33872423 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -43,6 +43,8 @@ If you only see a build.bat-file, you're probably on the wrong branch. If you sw You might run into [Powershell quirks](#powershell-quirks). +If it runs without errors; Hooray! Now you can continue with [the next step](CONTRIBUTING.md#how-do-i-begin) and open the solution and build it. + ### Build Infrastructure The Umbraco Build infrastructure relies on a PowerShell object. The object can be retrieved with: diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index cea5859486..0101ac9d16 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -28,7 +28,7 @@ This project and everyone participating in it, is governed by the [our Code of C [Working with the code](#working-with-the-code) * [Building Umbraco from source code](#building-umbraco-from-source-code) * [Working with the source code](#working-with-the-source-code) - * [Making changes after the PR was opened](#making-changes-after-the-pr-was-opened) + * [Making changes after the PR is open](#making-changes-after-the-pr-is-open) * [Which branch should I target for my contributions?](#which-branch-should-i-target-for-my-contributions) * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) @@ -65,7 +65,7 @@ Great question! The short version goes like this: * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/contrib`, create a new branch first. * **Push** - great, now you can push the changes up to your fork on GitHub - * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. + * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here](https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. ![Create a pull request](img/createpullrequest.png) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index a0d969c1b8..5587979a6c 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -2,7 +2,7 @@ using System.Resources; [assembly: AssemblyCompany("Umbraco")] -[assembly: AssemblyCopyright("Copyright © Umbraco 2019")] +[assembly: AssemblyCopyright("Copyright © Umbraco 2020")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/src/Umbraco.Core/Cache/WebCachingAppCache.cs b/src/Umbraco.Core/Cache/WebCachingAppCache.cs index c6e104221a..044aab4c42 100644 --- a/src/Umbraco.Core/Cache/WebCachingAppCache.cs +++ b/src/Umbraco.Core/Cache/WebCachingAppCache.cs @@ -86,7 +86,7 @@ namespace Umbraco.Core.Cache protected override void EnterWriteLock() { - _locker.EnterWriteLock();; + _locker.EnterWriteLock(); } protected override void ExitReadLock() diff --git a/src/Umbraco.Core/Collections/TopoGraph.cs b/src/Umbraco.Core/Collections/TopoGraph.cs index b8ded4a458..955a210465 100644 --- a/src/Umbraco.Core/Collections/TopoGraph.cs +++ b/src/Umbraco.Core/Collections/TopoGraph.cs @@ -126,7 +126,7 @@ namespace Umbraco.Core.Collections if (_items.TryGetValue(key, out value)) yield return value; else if (throwOnMissing) - throw new Exception(MissingDependencyError); + throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}"); } } } diff --git a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs index 41038ea4e9..0d398be83b 100644 --- a/src/Umbraco.Core/Composing/CollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/CollectionBuilderBase.cs @@ -79,9 +79,12 @@ namespace Umbraco.Core.Composing foreach (var type in types) EnsureType(type, "register"); - // register them + // register them - ensuring that each item is registered with the same lifetime as the collection. + // NOTE: Previously each one was not registered with the same lifetime which would mean that if there + // was a dependency on an individual item, it would resolve a brand new transient instance which isn't what + // we would expect to happen. The same item should be resolved from the container as the collection. foreach (var type in types) - register.Register(type); + register.Register(type, CollectionLifetime); _registeredTypes = types; } diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs index 846331a16e..a06f09baf6 100644 --- a/src/Umbraco.Core/Composing/Current.cs +++ b/src/Umbraco.Core/Composing/Current.cs @@ -5,6 +5,7 @@ using Umbraco.Core.Dictionary; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; +using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PackageActions; using Umbraco.Core.Packaging; @@ -208,6 +209,8 @@ namespace Umbraco.Core.Composing public static IVariationContextAccessor VariationContextAccessor => Factory.GetInstance(); + public static IImageUrlGenerator ImageUrlGenerator + => Factory.GetInstance(); #endregion } } diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs index 80cc3c3de4..58034f05d7 100644 --- a/src/Umbraco.Core/HashGenerator.cs +++ b/src/Umbraco.Core/HashGenerator.cs @@ -41,7 +41,7 @@ namespace Umbraco.Core internal void AddDateTime(DateTime d) { - _writer.Write(d.Ticks);; + _writer.Write(d.Ticks); } internal void AddString(string s) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index 0d451e8460..37a83bdd67 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 if (string.IsNullOrEmpty(dataType.Configuration)) { config.Format = "YYYY-MM-DD"; - }; + } } catch (Exception ex) { diff --git a/src/Umbraco.Core/Models/IImageUrlGenerator.cs b/src/Umbraco.Core/Models/IImageUrlGenerator.cs new file mode 100644 index 0000000000..0dc2933fd9 --- /dev/null +++ b/src/Umbraco.Core/Models/IImageUrlGenerator.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Models +{ + public interface IImageUrlGenerator + { + string GetImageUrl(ImageUrlGenerationOptions options); + } +} diff --git a/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs new file mode 100644 index 0000000000..f87657c33d --- /dev/null +++ b/src/Umbraco.Core/Models/ImageUrlGenerationOptions.cs @@ -0,0 +1,66 @@ +namespace Umbraco.Core.Models +{ + /// + /// These are options that are passed to the IImageUrlGenerator implementation to determine + /// the propery URL that is needed + /// + public class ImageUrlGenerationOptions + { + public ImageUrlGenerationOptions (string imageUrl) + { + ImageUrl = imageUrl; + } + + public string ImageUrl { get; } + public int? Width { get; set; } + public int? Height { get; set; } + public decimal? WidthRatio { get; set; } + public decimal? HeightRatio { get; set; } + public int? Quality { get; set; } + public string ImageCropMode { get; set; } + public string ImageCropAnchor { get; set; } + public bool DefaultCrop { get; set; } + public FocalPointPosition FocalPoint { get; set; } + public CropCoordinates Crop { get; set; } + public string CacheBusterValue { get; set; } + public string FurtherOptions { get; set; } + public bool UpScale { get; set; } = true; + public string AnimationProcessMode { get; set; } + + /// + /// The focal point position, in whatever units the registered IImageUrlGenerator uses, + /// typically a percentage of the total image from 0.0 to 1.0. + /// + public class FocalPointPosition + { + public FocalPointPosition (decimal top, decimal left) + { + Left = left; + Top = top; + } + + public decimal Left { get; } + public decimal Top { get; } + } + + /// + /// The bounds of the crop within the original image, in whatever units the registered + /// IImageUrlGenerator uses, typically a percentage between 0 and 100. + /// + public class CropCoordinates + { + public CropCoordinates (decimal x1, decimal y1, decimal x2, decimal y2) + { + X1 = x1; + Y1 = y1; + X2 = x2; + Y2 = y2; + } + + public decimal X1 { get; } + public decimal Y1 { get; } + public decimal X2 { get; } + public decimal Y2 { get; } + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs index 1c0d39a8b8..4e154bf514 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs @@ -63,6 +63,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the name of the user who created the content item. /// + [Obsolete("Use CreatorName(IUserService) extension instead")] string CreatorName { get; } /// @@ -78,6 +79,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Gets the name of the user who last updated the content item. /// + [Obsolete("Use WriterName(IUserService) extension instead")] string WriterName { get; } /// @@ -97,6 +99,7 @@ namespace Umbraco.Core.Models.PublishedContent /// The value of this property is contextual. It depends on the 'current' request uri, /// if any. /// + [Obsolete("Use the Url() extension instead")] string Url { get; } /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs index fb41c95419..b83dc7a013 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs @@ -94,6 +94,7 @@ namespace Umbraco.Core.Models.PublishedContent public virtual DateTime UpdateDate => _content.UpdateDate; /// + [Obsolete("Use the Url() extension instead")] public virtual string Url => _content.Url; /// diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index e00ac4ba15..5be66bac47 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -106,13 +106,14 @@ namespace Umbraco.Core.Models //use the custom avatar var avatarUrl = Current.MediaFileSystem.GetUrl(user.Avatar); + var urlGenerator = Current.ImageUrlGenerator; return new[] { - avatarUrl + "?width=30&height=30&mode=crop", - avatarUrl + "?width=60&height=60&mode=crop", - avatarUrl + "?width=90&height=90&mode=crop", - avatarUrl + "?width=150&height=150&mode=crop", - avatarUrl + "?width=300&height=300&mode=crop" + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 30, Height = 30 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 60, Height = 60 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 90, Height = 90 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 150, Height = 150 }), + urlGenerator.GetImageUrl(new ImageUrlGenerationOptions(avatarUrl) { ImageCropMode = "crop", Width = 300, Height = 300 }) }; } diff --git a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs index 82693677fb..33cb9c40d3 100644 --- a/src/Umbraco.Core/Packaging/ConflictingPackageData.cs +++ b/src/Umbraco.Core/Packaging/ConflictingPackageData.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Packaging { - internal class ConflictingPackageData + internal class ConflictingPackageData { private readonly IMacroService _macroService; private readonly IFileService _fileService; @@ -23,7 +23,7 @@ namespace Umbraco.Core.Packaging return stylesheetNodes .Select(n => { - var xElement = n.Element("Name") ?? n.Element("name"); ; + var xElement = n.Element("Name") ?? n.Element("name"); if (xElement == null) throw new FormatException("Missing \"Name\" element"); diff --git a/src/Umbraco.Core/Packaging/PackagesRepository.cs b/src/Umbraco.Core/Packaging/PackagesRepository.cs index 72954b238d..5549b502fa 100644 --- a/src/Umbraco.Core/Packaging/PackagesRepository.cs +++ b/src/Umbraco.Core/Packaging/PackagesRepository.cs @@ -82,7 +82,7 @@ namespace Umbraco.Core.Packaging { var packagesXml = EnsureStorage(out _); if (packagesXml?.Root == null) - yield break;; + yield break; foreach (var packageXml in packagesXml.Root.Elements("package")) yield return _parser.ToPackageDefinition(packageXml); @@ -139,7 +139,7 @@ namespace Umbraco.Core.Packaging var updatedXml = _parser.ToXml(definition); packageXml.ReplaceWith(updatedXml); } - + packagesXml.Save(packagesFile); return true; @@ -212,7 +212,7 @@ namespace Umbraco.Core.Packaging compiledPackageXml.Save(packageXmlFileName); // check if there's a packages directory below media - + if (Directory.Exists(IOHelper.MapPath(_mediaFolderPath)) == false) Directory.CreateDirectory(IOHelper.MapPath(_mediaFolderPath)); @@ -510,7 +510,6 @@ namespace Umbraco.Core.Packaging private XElement GetStylesheetXml(string name, bool includeProperties) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(name)); -; var sts = _fileService.GetStylesheetByName(name); if (sts == null) return null; var stylesheetXml = new XElement("Stylesheet"); @@ -562,7 +561,7 @@ namespace Umbraco.Core.Packaging package.Add(new XElement("url", definition.Url)); var requirements = new XElement("requirements"); - + requirements.Add(new XElement("major", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Major.ToInvariantString() : definition.UmbracoVersion.Major.ToInvariantString())); requirements.Add(new XElement("minor", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Minor.ToInvariantString() : definition.UmbracoVersion.Minor.ToInvariantString())); requirements.Add(new XElement("patch", definition.UmbracoVersion == null ? UmbracoVersion.SemanticVersion.Patch.ToInvariantString() : definition.UmbracoVersion.Build.ToInvariantString())); @@ -589,7 +588,7 @@ namespace Umbraco.Core.Packaging contributors.Add(new XElement("contributor", contributor)); } } - + info.Add(contributors); info.Add(new XElement("readme", new XCData(definition.Readme))); diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs index 254e04d2d5..4020244733 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentTypeRepositoryBase.cs @@ -27,6 +27,13 @@ namespace Umbraco.Core.Persistence.Repositories /// bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + /// /// Returns true or false depending on whether content nodes have been created based on the provided content type id. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 6f714ff187..357798a8a9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1309,14 +1309,16 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern", return test; } - /// - /// Given the path of a content item, this will return true if the content item exists underneath a list view content item - /// - /// - /// + /// public bool HasContainerInPath(string contentPath) { - var ids = contentPath.Split(',').Select(int.Parse); + var ids = contentPath.Split(',').Select(int.Parse).ToArray(); + return HasContainerInPath(ids); + } + + /// + public bool HasContainerInPath(params int[] ids) + { var sql = new Sql($@"SELECT COUNT(*) FROM cmsContentType INNER JOIN {Constants.DatabaseSchema.Tables.Content} ON cmsContentType.nodeId={Constants.DatabaseSchema.Tables.Content}.contentTypeId WHERE {Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cmsContentType.isContainer=@isContainer", new { ids, isContainer = true }); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs index f0044e225d..1f0b45614b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MacroRepository.cs @@ -40,7 +40,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (macroDto == null) return null; - + var entity = MacroFactory.BuildEntity(macroDto); // reset dirty initial properties (U4-1946) @@ -153,7 +153,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMacro entity) { entity.UpdatingEntity(); -; var dto = MacroFactory.BuildDto(entity); Database.Update(dto); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs index b348317989..2175780916 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/TemplateRepository.cs @@ -215,7 +215,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement //Save updated entity to db template.UpdateDate = DateTime.Now; - ; var dto = TemplateFactory.BuildDto(template, NodeObjectTypeId, templateDto.PrimaryKey); Database.Update(dto.NodeDto); diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 386ab6a8f3..e7b051e17f 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -13,35 +13,46 @@ namespace Umbraco.Core.PropertyEditors public IEnumerable GetAllReferences(PropertyCollection properties, PropertyEditorCollection propertyEditors) { - var trackedRelations = new List(); + var trackedRelations = new HashSet(); foreach (var p in properties) { if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out var editor)) continue; - //TODO: Support variants/segments! This is not required for this initial prototype which is why there is a check here - if (!p.PropertyType.VariesByNothing()) continue; - var val = p.GetValue(); // get the invariant value + //TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants - var valueEditor = editor.GetValueEditor(); - if (valueEditor is IDataValueReference reference) + foreach(var propertyVal in p.Values) { - var refs = reference.GetReferences(val); - trackedRelations.AddRange(refs); - } + var val = propertyVal.EditedValue; - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (var item in this) - { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media/content items - if (item.IsForEditor(editor)) - trackedRelations.AddRange(item.GetDataValueReference().GetReferences(val)); + var valueEditor = editor.GetValueEditor(); + if (valueEditor is IDataValueReference reference) + { + var refs = reference.GetReferences(val); + foreach(var r in refs) + trackedRelations.Add(r); + } + + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (var item in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has referecnes to media/content items + if (item.IsForEditor(editor)) + { + foreach(var r in item.GetDataValueReference().GetReferences(val)) + trackedRelations.Add(r); + } + + } } + + } return trackedRelations; diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs index b211272b51..ab217d3870 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -7,6 +7,8 @@ using System.Runtime.Serialization; using System.Text; using System.Web; using Newtonsoft.Json; +using Umbraco.Core.Composing; +using Umbraco.Core.Models; using Umbraco.Core.Serialization; namespace Umbraco.Core.PropertyEditors.ValueConverters @@ -59,38 +61,34 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters : Crops.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } - internal void AppendCropBaseUrl(StringBuilder url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint) + internal ImageUrlGenerationOptions GetCropBaseOptions(string url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint) { if (preferFocalPoint && HasFocalPoint() || crop != null && crop.Coordinates == null && HasFocalPoint() || defaultCrop && HasFocalPoint()) { - url.Append("?center="); - url.Append(FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); - url.Append(","); - url.Append(FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); - url.Append("&mode=crop"); + return new ImageUrlGenerationOptions(url) { FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(FocalPoint.Top, FocalPoint.Left) }; } else if (crop != null && crop.Coordinates != null && preferFocalPoint == false) { - url.Append("?crop="); - url.Append(crop.Coordinates.X1.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.X2.ToString(CultureInfo.InvariantCulture)).Append(","); - url.Append(crop.Coordinates.Y2.ToString(CultureInfo.InvariantCulture)); - url.Append("&cropmode=percentage"); + return new ImageUrlGenerationOptions(url) { Crop = new ImageUrlGenerationOptions.CropCoordinates(crop.Coordinates.X1, crop.Coordinates.Y1, crop.Coordinates.X2, crop.Coordinates.Y2) }; } else { - url.Append("?anchor=center"); - url.Append("&mode=crop"); + return new ImageUrlGenerationOptions(url) { DefaultCrop = true }; } } /// /// Gets the value image url for a specified crop. /// - public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) + [Obsolete("Use the overload that takes an IImageUrlGenerator")] + public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(alias, Current.ImageUrlGenerator, useCropDimensions, useFocalPoint, cacheBusterValue); + + /// + /// Gets the value image url for a specified crop. + /// + public string GetCropUrl(string alias, IImageUrlGenerator imageUrlGenerator, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null) { var crop = GetCrop(alias); @@ -98,38 +96,37 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (crop == null && !string.IsNullOrWhiteSpace(alias)) return null; - var url = new StringBuilder(); - - AppendCropBaseUrl(url, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint); + var options = GetCropBaseOptions(string.Empty, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint); if (crop != null && useCropDimensions) { - url.Append("&width=").Append(crop.Width); - url.Append("&height=").Append(crop.Height); + options.Width = crop.Width; + options.Height = crop.Height; } - if (cacheBusterValue != null) - url.Append("&rnd=").Append(cacheBusterValue); + options.CacheBusterValue = cacheBusterValue; - return url.ToString(); + return imageUrlGenerator.GetImageUrl(options); } /// /// Gets the value image url for a specific width and height. /// - public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null) + [Obsolete("Use the overload that takes an IImageUrlGenerator")] + public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null) => GetCropUrl(width, height, Current.ImageUrlGenerator, useFocalPoint, cacheBusterValue); + + /// + /// Gets the value image url for a specific width and height. + /// + public string GetCropUrl(int width, int height, IImageUrlGenerator imageUrlGenerator, bool useFocalPoint = false, string cacheBusterValue = null) { - var url = new StringBuilder(); + var options = GetCropBaseOptions(string.Empty, null, true, useFocalPoint); - AppendCropBaseUrl(url, null, true, useFocalPoint); + options.Width = width; + options.Height = height; + options.CacheBusterValue = cacheBusterValue; - url.Append("&width=").Append(width); - url.Append("&height=").Append(height); - - if (cacheBusterValue != null) - url.Append("&rnd=").Append(cacheBusterValue); - - return url.ToString(); + return imageUrlGenerator.GetImageUrl(options); } /// diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs index 6f5bd571b7..36ffddb863 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/ImageCropperValueConverter.cs @@ -47,7 +47,7 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters value = new ImageCropperValue { Src = sourceString }; } - value.ApplyConfiguration(propertyType.DataType.ConfigurationAs()); + value?.ApplyConfiguration(propertyType.DataType.ConfigurationAs()); return value; } diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index 6ed3c85e91..4c818aa87c 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -51,7 +51,10 @@ namespace Umbraco.Core.Services IEnumerable GetComposedOf(int id); // composition axis IEnumerable GetChildren(int id); + IEnumerable GetChildren(Guid id); + bool HasChildren(int id); + bool HasChildren(Guid id); void Save(TItem item, int userId = Constants.Security.SuperUserId); void Save(IEnumerable items, int userId = Constants.Security.SuperUserId); @@ -69,6 +72,13 @@ namespace Umbraco.Core.Services /// bool HasContainerInPath(string contentPath); + /// + /// Gets a value indicating whether there is a list view content item in the path. + /// + /// + /// + bool HasContainerInPath(params int[] ids); + Attempt> CreateContainer(int parentContainerId, string name, int userId = Constants.Security.SuperUserId); Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId); EntityContainer GetContainer(int containerId); diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index da532e2765..fdd2d9ceae 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -321,6 +321,15 @@ namespace Umbraco.Core.Services.Implement } } + public bool HasContainerInPath(params int[] ids) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + // can use same repo for both content and media + return Repository.HasContainerInPath(ids); + } + } + public IEnumerable GetDescendants(int id, bool andSelf) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4cb9d7f4c1..b6d4dedbd3 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -143,6 +143,8 @@ + + diff --git a/src/Umbraco.Core/Xml/XmlHelper.cs b/src/Umbraco.Core/Xml/XmlHelper.cs index 2dd955086d..d6461ec8c6 100644 --- a/src/Umbraco.Core/Xml/XmlHelper.cs +++ b/src/Umbraco.Core/Xml/XmlHelper.cs @@ -214,9 +214,9 @@ namespace Umbraco.Core.Xml var xmlDoc = new XmlDocument(); //Load the file into the XmlDocument xmlDoc.Load(reader); - + return xmlDoc; - } + } } /// @@ -335,7 +335,7 @@ namespace Umbraco.Core.Xml var child = parent.SelectSingleNode(name); if (child != null) { - child.InnerXml = ""; ; + child.InnerXml = ""; return child; } return AddCDataNode(xd, name, value); diff --git a/src/Umbraco.TestData/UmbracoTestDataController.cs b/src/Umbraco.TestData/UmbracoTestDataController.cs index 402c05cc1c..02949d5345 100644 --- a/src/Umbraco.TestData/UmbracoTestDataController.cs +++ b/src/Umbraco.TestData/UmbracoTestDataController.cs @@ -68,7 +68,7 @@ namespace Umbraco.TestData scope.Complete(); } - + return Content("Done"); } @@ -89,7 +89,7 @@ namespace Umbraco.TestData message = "Count not high enough for specified for number of levels required"; return false; } - + return true; } @@ -140,7 +140,7 @@ namespace Umbraco.TestData currChildCount = prev.childCount; // restore the parent parent = prev.parent; - + } else if (contentItem.Level < depth) { @@ -149,10 +149,10 @@ namespace Umbraco.TestData // not at max depth, create below parent = created.container(); - + currChildCount = 0; } - + } } @@ -219,7 +219,7 @@ namespace Umbraco.TestData { var content = Services.ContentService.Create(faker.Commerce.ProductName(), currParent, docType.Alias); content.SetValue("review", faker.Rant.Review()); - content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); ; + content.SetValue("desc", string.Join(", ", Enumerable.Range(0, 5).Select(x => faker.Commerce.ProductAdjective()))); content.SetValue("media", imageIds[random.Next(0, imageIds.Count - 1)]); Services.ContentService.Save(content); @@ -259,7 +259,7 @@ namespace Umbraco.TestData return docType; } - private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); + private IDataType GetOrCreateRichText() => GetOrCreateDataType(RichTextDataTypeName, Constants.PropertyEditors.Aliases.TinyMce); private IDataType GetOrCreateMediaPicker() => GetOrCreateDataType(MediaPickerDataTypeName, Constants.PropertyEditors.Aliases.MediaPicker); diff --git a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs index ec757e09f0..1d8390e07e 100644 --- a/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs +++ b/src/Umbraco.Tests/Composing/CollectionBuildersTests.cs @@ -354,7 +354,7 @@ namespace Umbraco.Tests.Composing var col2 = factory.GetInstance(); AssertCollection(col2, typeof(Resolved1), typeof(Resolved2)); - AssertSameCollection(col1, col2); + AssertSameCollection(factory, col1, col2); } } @@ -413,11 +413,11 @@ namespace Umbraco.Tests.Composing { col1A = factory.GetInstance(); col1B = factory.GetInstance(); - } - AssertCollection(col1A, typeof(Resolved1), typeof(Resolved2)); - AssertCollection(col1B, typeof(Resolved1), typeof(Resolved2)); - AssertSameCollection(col1A, col1B); + AssertCollection(col1A, typeof(Resolved1), typeof(Resolved2)); + AssertCollection(col1B, typeof(Resolved1), typeof(Resolved2)); + AssertSameCollection(factory, col1A, col1B); + } TestCollection col2; @@ -452,7 +452,7 @@ namespace Umbraco.Tests.Composing Assert.IsInstanceOf(expected[i], colA[i]); } - private static void AssertSameCollection(IEnumerable col1, IEnumerable col2) + private static void AssertSameCollection(IFactory factory, IEnumerable col1, IEnumerable col2) { Assert.AreSame(col1, col2); @@ -460,8 +460,19 @@ namespace Umbraco.Tests.Composing var col2A = col2.ToArray(); Assert.AreEqual(col1A.Length, col2A.Length); + + // Ensure each item in each collection is the same but also + // resolve each item from the factory to ensure it's also the same since + // it should have the same lifespan. for (var i = 0; i < col1A.Length; i++) + { Assert.AreSame(col1A[i], col2A[i]); + + var itemA = factory.GetInstance(col1A[i].GetType()); + var itemB = factory.GetInstance(col2A[i].GetType()); + + Assert.AreSame(itemA, itemB); + } } private static void AssertNotSameCollection(IEnumerable col1, IEnumerable col2) @@ -472,8 +483,11 @@ namespace Umbraco.Tests.Composing var col2A = col2.ToArray(); Assert.AreEqual(col1A.Length, col2A.Length); + for (var i = 0; i < col1A.Length; i++) + { Assert.AreNotSame(col1A[i], col2A[i]); + } } #endregion diff --git a/src/Umbraco.Tests/Logging/LogviewerTests.cs b/src/Umbraco.Tests/Logging/LogviewerTests.cs index ed9deec177..08ba070b70 100644 --- a/src/Umbraco.Tests/Logging/LogviewerTests.cs +++ b/src/Umbraco.Tests/Logging/LogviewerTests.cs @@ -163,7 +163,7 @@ namespace Umbraco.Tests.Logging //Query @Level='Warning' BUT we pass in array of LogLevels for Debug & Info (Expect to get 0 results) string[] logLevelMismatch = { "Debug", "Information" }; - var filterLevelQuery = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, filterExpression: "@Level='Warning'", logLevels: logLevelMismatch); ; + var filterLevelQuery = _logViewer.GetLogs(_logTimePeriod, pageNumber: 1, filterExpression: "@Level='Warning'", logLevels: logLevelMismatch); Assert.AreEqual(0, filterLevelQuery.TotalItems); } diff --git a/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs new file mode 100644 index 0000000000..30ead90de9 --- /dev/null +++ b/src/Umbraco.Tests/Models/ImageProcessorImageUrlGeneratorTest.cs @@ -0,0 +1,232 @@ +using System; +using System.Globalization; +using Moq; +using Newtonsoft.Json; +using NUnit.Framework; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Core.Services; +using Umbraco.Tests.Components; +using Umbraco.Tests.TestHelpers; +using Umbraco.Web.Models; +using Umbraco.Web; +using Umbraco.Web.PropertyEditors; +using System.Text; + +namespace Umbraco.Tests.Models +{ + [TestFixture] + public class ImageProcessorImageUrlGeneratorTest + { + private const string MediaPath = "/media/1005/img_0671.jpg"; + private static readonly ImageUrlGenerationOptions.CropCoordinates Crop = new ImageUrlGenerationOptions.CropCoordinates(0.58729977382575338m, 0.055768992440203169m, 0m, 0.32457553600198386m); + private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus1 = new ImageUrlGenerationOptions.FocalPointPosition(0.80827067669172936m, 0.96m); + private static readonly ImageUrlGenerationOptions.FocalPointPosition Focus2 = new ImageUrlGenerationOptions.FocalPointPosition(0.41m, 0.4275m); + private static readonly ImageProcessorImageUrlGenerator Generator = new ImageProcessorImageUrlGenerator(); + + [Test] + public void GetCropUrl_CropAliasTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Crop = Crop, Width = 100, Height = 100 }); + Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + } + + [Test] + public void GetCropUrl_WidthHeightTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 200, Height = 300 }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300", urlString); + } + + [Test] + public void GetCropUrl_FocalPointTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 100, Height = 100 }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=100&height=100", urlString); + } + + [Test] + public void GetCropUrlFurtherOptionsTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 200, Height = 300, FurtherOptions = "&filter=comic&roundedcorners=radius-26|bgcolor-fff" }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); + } + + /// + /// Test that if a crop alias has been specified that doesn't exist the method returns null + /// + [Test] + public void GetCropUrlNullTest() + { + var urlString = Generator.GetImageUrl(null); + Assert.AreEqual(null, urlString); + } + + /// + /// Test that if a crop alias has been specified that doesn't exist the method returns null + /// + [Test] + public void GetCropUrlEmptyTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(null)); + Assert.AreEqual("?mode=crop", urlString); + } + + /// + /// Test the GetCropUrl method on the ImageCropDataSet Model + /// + [Test] + public void GetBaseCropUrlFromModelTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(null) { Crop = Crop, Width = 100, Height = 100 }); + Assert.AreEqual("?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + } + + /// + /// Test the height ratio mode with predefined crop dimensions + /// + [Test] + public void GetCropUrl_CropAliasHeightRatioModeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { Crop = Crop, Width = 100, HeightRatio = 1 }); + Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&heightratio=1&width=100", urlString); + } + + /// + /// Test the height ratio mode with manual width/height dimensions + /// + [Test] + public void GetCropUrl_WidthHeightRatioModeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Width = 300, HeightRatio = 0.5m }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&heightratio=0.5&width=300", urlString); + } + + /// + /// Test the height ratio mode with width/height dimensions + /// + [Test] + public void GetCropUrl_HeightWidthRatioModeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus1, Height = 150, WidthRatio = 2 }); + Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&widthratio=2&height=150", urlString); + } + + /// + /// Test that if Crop mode is specified as anything other than Crop the image doesn't use the crop + /// + [Test] + public void GetCropUrl_SpecifiedCropModeTest() + { + var urlStringMin = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Min", Width = 300, Height = 150 }); + var urlStringBoxPad = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "BoxPad", Width = 300, Height = 150 }); + var urlStringPad = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Pad", Width = 300, Height = 150 }); + var urlStringMax = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Max", Width = 300, Height = 150 }); + var urlStringStretch = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Stretch", Width = 300, Height = 150 }); + + Assert.AreEqual(MediaPath + "?mode=min&width=300&height=150", urlStringMin); + Assert.AreEqual(MediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); + Assert.AreEqual(MediaPath + "?mode=pad&width=300&height=150", urlStringPad); + Assert.AreEqual(MediaPath + "?mode=max&width=300&height=150", urlStringMax); + Assert.AreEqual(MediaPath + "?mode=stretch&width=300&height=150", urlStringStretch); + } + + /// + /// Test for upload property type + /// + [Test] + public void GetCropUrl_UploadTypeTest() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Crop", ImageCropAnchor = "Center", Width = 100, Height = 270 }); + Assert.AreEqual(MediaPath + "?mode=crop&anchor=center&width=100&height=270", urlString); + } + + /// + /// Test for preferFocalPoint when focal point is centered + /// + [Test] + public void GetCropUrl_PreferFocalPointCenter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Width = 300, Height = 150 }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString); + } + + /// + /// Test to check if height ratio is returned for a predefined crop without coordinates and focal point in centre when a width parameter is passed + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidth() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + } + + /// + /// Test to check if height ratio is returned for a predefined crop without coordinates and focal point is custom when a width parameter is passed + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPoint() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus2, Width = 200, HeightRatio = 0.5962962962962962962962962963m }); + Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + } + + /// + /// Test to check if crop ratio is ignored if useCropDimensions is true + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithWidthAndFocalPointIgnore() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { FocalPoint = Focus2, Width = 270, Height = 161 }); + Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&width=270&height=161", urlString); + } + + /// + /// Test to check if width ratio is returned for a predefined crop without coordinates and focal point in centre when a height parameter is passed + /// + [Test] + public void GetCropUrl_PreDefinedCropNoCoordinatesWithHeight() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Height = 200, WidthRatio = 1.6770186335403726708074534161m }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&widthratio=1.6770186335403726708074534161&height=200", urlString); + } + + /// + /// Test to check result when only a width parameter is passed, effectivly a resize only + /// + [Test] + public void GetCropUrl_WidthOnlyParameter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Width = 200 }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=200", urlString); + } + + /// + /// Test to check result when only a height parameter is passed, effectivly a resize only + /// + [Test] + public void GetCropUrl_HeightOnlyParameter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { DefaultCrop = true, Height = 200 }); + Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&height=200", urlString); + } + + /// + /// Test to check result when using a background color with padding + /// + [Test] + public void GetCropUrl_BackgroundColorParameter() + { + var urlString = Generator.GetImageUrl(new ImageUrlGenerationOptions(MediaPath) { ImageCropMode = "Pad", Width = 400, Height = 400, FurtherOptions = "&bgcolor=fff" }); + Assert.AreEqual(MediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); + } + } +} diff --git a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs index 6f215f4a35..9eca06d4e0 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MacroRepositoryTest.cs @@ -38,7 +38,6 @@ namespace Umbraco.Tests.Persistence.Repositories var repository = new MacroRepository((IScopeAccessor) provider, AppCaches.Disabled, Mock.Of()); var macro = new Macro("test1", "Test", "~/views/macropartials/test.cshtml", MacroTypes.PartialView); - ; Assert.Throws(() => repository.Save(macro)); } @@ -56,7 +55,7 @@ namespace Umbraco.Tests.Persistence.Repositories var macro = repository.Get(1); macro.Alias = "test2"; - + Assert.Throws(() => repository.Save(macro)); } diff --git a/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs new file mode 100644 index 0000000000..ae6f80b32d --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -0,0 +1,223 @@ +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PropertyEditors; +using static Umbraco.Core.Models.Property; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class DataValueReferenceFactoryCollectionTests + { + [Test] + public void GetAllReferences_All_Variants_With_IDataValueReferenceFactory() + { + var collection = new DataValueReferenceFactoryCollection(new TestDataValueReferenceFactory().Yield()); + + // label does not implement IDataValueReference + var labelEditor = new LabelPropertyEditor(Mock.Of()); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(labelEditor.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(new DataType(labelEditor)) + { + Variations = ContentVariation.CultureAndSegment + }) + { + Values = new List + { + // Ignored (no culture) + new PropertyValue + { + EditedValue = trackedUdi1 + }, + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + // Ignored (no culture) + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Culture = "en-US", + Segment = "B", + EditedValue = trackedUdi3 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi2, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi3, result.ElementAt(1).Udi.ToString()); + } + + [Test] + public void GetAllReferences_All_Variants_With_IDataValueReference_Editor() + { + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty()); + + // mediaPicker does implement IDataValueReference + var mediaPicker = new MediaPickerPropertyEditor(Mock.Of()); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(mediaPicker.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(new DataType(mediaPicker)) + { + Variations = ContentVariation.CultureAndSegment + }) + { + Values = new List + { + // Ignored (no culture) + new PropertyValue + { + EditedValue = trackedUdi1 + }, + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + // Ignored (no culture) + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Culture = "en-US", + Segment = "B", + EditedValue = trackedUdi3 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi2, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi3, result.ElementAt(1).Udi.ToString()); + } + + [Test] + public void GetAllReferences_Invariant_With_IDataValueReference_Editor() + { + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty()); + + // mediaPicker does implement IDataValueReference + var mediaPicker = new MediaPickerPropertyEditor(Mock.Of()); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(mediaPicker.Yield())); + var trackedUdi1 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi2 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi3 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var trackedUdi4 = Udi.Create(Constants.UdiEntityType.Media, Guid.NewGuid()).ToString(); + var property = new Property(new PropertyType(new DataType(mediaPicker)) + { + Variations = ContentVariation.Nothing | ContentVariation.Segment + }) + { + Values = new List + { + new PropertyValue + { + EditedValue = trackedUdi1 + }, + // Ignored (has culture) + new PropertyValue + { + Culture = "en-US", + EditedValue = trackedUdi2 + }, + // Ignored (has culture) + new PropertyValue + { + Culture = "en-US", + Segment = "A", + EditedValue = trackedUdi3 + }, + new PropertyValue + { + Segment = "A", + EditedValue = trackedUdi4 + }, + // duplicate + new PropertyValue + { + Segment = "B", + EditedValue = trackedUdi4 + } + } + }; + var properties = new PropertyCollection + { + property + }; + var result = collection.GetAllReferences(properties, propertyEditors); + + Assert.AreEqual(2, result.Count()); + Assert.AreEqual(trackedUdi1, result.ElementAt(0).Udi.ToString()); + Assert.AreEqual(trackedUdi4, result.ElementAt(1).Udi.ToString()); + } + + private class TestDataValueReferenceFactory : IDataValueReferenceFactory + { + public IDataValueReference GetDataValueReference() => new TestMediaDataValueReference(); + + public bool IsForEditor(IDataEditor dataEditor) => dataEditor.Alias == Constants.PropertyEditors.Aliases.Label; + + private class TestMediaDataValueReference : IDataValueReference + { + public IEnumerable GetReferences(object value) + { + // This is the same as the media picker, it will just try to parse the value directly as a UDI + + var asString = value is string str ? str : value?.ToString(); + + if (string.IsNullOrEmpty(asString)) yield break; + + if (Udi.TryParse(asString, out var udi)) + yield return new UmbracoEntityReference(udi); + } + } + } + } +} diff --git a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs index 433ba64b38..c5c2b4e61f 100644 --- a/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs +++ b/src/Umbraco.Tests/PropertyEditors/ImageCropperTest.cs @@ -5,9 +5,7 @@ using Newtonsoft.Json; using NUnit.Framework; using Newtonsoft.Json.Linq; using Umbraco.Core; -using Umbraco.Core.Cache; using Umbraco.Core.Composing; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -21,6 +19,7 @@ using Umbraco.Tests.TestHelpers; using Umbraco.Web.Models; using Umbraco.Web; using Umbraco.Web.PropertyEditors; +using System.Text; namespace Umbraco.Tests.PropertyEditors { @@ -110,8 +109,8 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_CropAliasTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true); - Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true); + Assert.AreEqual(MediaPath + "?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&w=100&h=100", urlString); } /// @@ -120,29 +119,29 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_CropAliasIgnoreWidthHeightTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, width: 50, height: 50); - Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, width: 50, height: 50); + Assert.AreEqual(MediaPath + "?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&w=100&h=100", urlString); } [Test] public void GetCropUrl_WidthHeightTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 200, height: 300); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 200, height: 300); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&w=200&h=300", urlString); } [Test] public void GetCropUrl_FocalPointTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "thumb", preferFocalPoint: true, useCropDimensions: true); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=100&height=100", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "thumb", preferFocalPoint: true, useCropDimensions: true); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&w=100&h=100", urlString); } [Test] public void GetCropUrlFurtherOptionsTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 200, height: 300, furtherOptions: "&filter=comic&roundedcorners=radius-26|bgcolor-fff"); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=200&height=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 200, height: 300, furtherOptions: "&filter=comic&roundedcorners=radius-26|bgcolor-fff"); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&w=200&h=300&filter=comic&roundedcorners=radius-26|bgcolor-fff", urlString); } /// @@ -151,7 +150,7 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrlNullTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Banner", useCropDimensions: true); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Banner", useCropDimensions: true); Assert.AreEqual(null, urlString); } @@ -162,8 +161,8 @@ namespace Umbraco.Tests.PropertyEditors public void GetBaseCropUrlFromModelTest() { var cropDataSet = CropperJson1.DeserializeImageCropperValue(); - var urlString = cropDataSet.GetCropUrl("thumb"); - Assert.AreEqual("?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&height=100", urlString); + var urlString = cropDataSet.GetCropUrl("thumb", new TestImageUrlGenerator()); + Assert.AreEqual("?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&w=100&h=100", urlString); } /// @@ -172,8 +171,8 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_CropAliasHeightRatioModeTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, ratioMode:ImageCropRatioMode.Height); - Assert.AreEqual(MediaPath + "?crop=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&cropmode=percentage&width=100&heightratio=1", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, cropAlias: "Thumb", useCropDimensions: true, ratioMode:ImageCropRatioMode.Height); + Assert.AreEqual(MediaPath + "?c=0.58729977382575338,0.055768992440203169,0,0.32457553600198386&hr=1&w=100", urlString); } /// @@ -182,8 +181,8 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_WidthHeightRatioModeTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode:ImageCropRatioMode.Height); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&width=300&heightratio=0.5", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode:ImageCropRatioMode.Height); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&hr=0.5&w=300", urlString); } /// @@ -192,8 +191,8 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_HeightWidthRatioModeTest() { - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode: ImageCropRatioMode.Width); - Assert.AreEqual(MediaPath + "?center=0.80827067669172936,0.96&mode=crop&height=150&widthratio=2", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, ratioMode: ImageCropRatioMode.Width); + Assert.AreEqual(MediaPath + "?f=0.80827067669172936x0.96&wr=2&h=150", urlString); } /// @@ -202,17 +201,17 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_SpecifiedCropModeTest() { - var urlStringMin = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Min); - var urlStringBoxPad = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.BoxPad); - var urlStringPad = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Pad); - var urlString = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode:ImageCropMode.Max); - var urlStringStretch = MediaPath.GetCropUrl(imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); + var urlStringMin = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Min); + var urlStringBoxPad = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.BoxPad); + var urlStringPad = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Pad); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode:ImageCropMode.Max); + var urlStringStretch = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: CropperJson1, width: 300, height: 150, imageCropMode: ImageCropMode.Stretch); - Assert.AreEqual(MediaPath + "?mode=min&width=300&height=150", urlStringMin); - Assert.AreEqual(MediaPath + "?mode=boxpad&width=300&height=150", urlStringBoxPad); - Assert.AreEqual(MediaPath + "?mode=pad&width=300&height=150", urlStringPad); - Assert.AreEqual(MediaPath + "?mode=max&width=300&height=150", urlString); - Assert.AreEqual(MediaPath + "?mode=stretch&width=300&height=150", urlStringStretch); + Assert.AreEqual(MediaPath + "?m=min&w=300&h=150", urlStringMin); + Assert.AreEqual(MediaPath + "?m=boxpad&w=300&h=150", urlStringBoxPad); + Assert.AreEqual(MediaPath + "?m=pad&w=300&h=150", urlStringPad); + Assert.AreEqual(MediaPath + "?m=max&w=300&h=150", urlString); + Assert.AreEqual(MediaPath + "?m=stretch&w=300&h=150", urlStringStretch); } /// @@ -221,8 +220,8 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void GetCropUrl_UploadTypeTest() { - var urlString = MediaPath.GetCropUrl(width: 100, height: 270, imageCropMode: ImageCropMode.Crop, imageCropAnchor: ImageCropAnchor.Center); - Assert.AreEqual(MediaPath + "?mode=crop&anchor=center&width=100&height=270", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), width: 100, height: 270, imageCropMode: ImageCropMode.Crop, imageCropAnchor: ImageCropAnchor.Center); + Assert.AreEqual(MediaPath + "?m=crop&a=center&w=100&h=270", urlString); } /// @@ -233,8 +232,8 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, width: 300, height: 150, preferFocalPoint:true); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=300&height=150", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, width: 300, height: 150, preferFocalPoint:true); + Assert.AreEqual(MediaPath + "?m=defaultcrop&w=300&h=150", urlString); } /// @@ -245,8 +244,8 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200); + Assert.AreEqual(MediaPath + "?m=defaultcrop&hr=0.5962962962962962962962962963&w=200", urlString); } /// @@ -257,8 +256,8 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200); - Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&heightratio=0.5962962962962962962962962963&width=200", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200); + Assert.AreEqual(MediaPath + "?f=0.41x0.4275&hr=0.5962962962962962962962962963&w=200", urlString); } /// @@ -269,8 +268,8 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.4275,\"top\": 0.41},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", width: 200, useCropDimensions: true); - Assert.AreEqual(MediaPath + "?center=0.41,0.4275&mode=crop&width=270&height=161", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", width: 200, useCropDimensions: true); + Assert.AreEqual(MediaPath + "?f=0.41x0.4275&w=270&h=161", urlString); } /// @@ -281,8 +280,8 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, cropAlias: "home", height: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&widthratio=1.6770186335403726708074534161&height=200", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, cropAlias: "home", height: 200); + Assert.AreEqual(MediaPath + "?m=defaultcrop&wr=1.6770186335403726708074534161&h=200", urlString); } /// @@ -293,8 +292,8 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, width: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&width=200", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, width: 200); + Assert.AreEqual(MediaPath + "?m=defaultcrop&w=200", urlString); } /// @@ -305,8 +304,8 @@ namespace Umbraco.Tests.PropertyEditors { const string cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(imageCropperValue: cropperJson, height: 200); - Assert.AreEqual(MediaPath + "?anchor=center&mode=crop&height=200", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), imageCropperValue: cropperJson, height: 200); + Assert.AreEqual(MediaPath + "?m=defaultcrop&h=200", urlString); } /// @@ -317,8 +316,55 @@ namespace Umbraco.Tests.PropertyEditors { var cropperJson = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"" + MediaPath + "\",\"crops\": [{\"alias\": \"home\",\"width\": 270,\"height\": 161}]}"; - var urlString = MediaPath.GetCropUrl(400, 400, cropperJson, imageCropMode: ImageCropMode.Pad, furtherOptions: "&bgcolor=fff"); - Assert.AreEqual(MediaPath + "?mode=pad&width=400&height=400&bgcolor=fff", urlString); + var urlString = MediaPath.GetCropUrl(new TestImageUrlGenerator(), 400, 400, cropperJson, imageCropMode: ImageCropMode.Pad, furtherOptions: "&bgcolor=fff"); + Assert.AreEqual(MediaPath + "?m=pad&w=400&h=400&bgcolor=fff", urlString); + } + + internal class TestImageUrlGenerator : IImageUrlGenerator + { + public string GetImageUrl(ImageUrlGenerationOptions options) + { + var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty); + + if (options.FocalPoint != null) + { + imageProcessorUrl.Append("?f="); + imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("x"); + imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); + } + else if (options.Crop != null) + { + imageProcessorUrl.Append("?c="); + imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); + } + else if (options.DefaultCrop) + { + imageProcessorUrl.Append("?m=defaultcrop"); + } + else + { + imageProcessorUrl.Append("?m=" + options.ImageCropMode.ToString().ToLower()); + if (options.ImageCropAnchor != null)imageProcessorUrl.Append("&a=" + options.ImageCropAnchor.ToString().ToLower()); + } + + var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&f="); + if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&q=" + options.Quality); + if (options.HeightRatio != null) imageProcessorUrl.Append("&hr=" + options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.WidthRatio != null) imageProcessorUrl.Append("&wr=" + options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.Width != null) imageProcessorUrl.Append("&w=" + options.Width); + if (options.Height != null) imageProcessorUrl.Append("&h=" + options.Height); + if (options.UpScale == false) imageProcessorUrl.Append("&u=no"); + if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&apm=" + options.AnimationProcessMode); + if (options.FurtherOptions != null) imageProcessorUrl.Append(options.FurtherOptions); + if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&q=" + options.Quality); + if (options.CacheBusterValue != null) imageProcessorUrl.Append("&r=").Append(options.CacheBusterValue); + + return imageProcessorUrl.ToString(); + } } } } diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index a96cad4076..59791fc645 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -12,6 +12,7 @@ using Umbraco.Web.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web; using Umbraco.Web.Templates; +using Umbraco.Web.Models; namespace Umbraco.Tests.PublishedContent { @@ -46,7 +47,7 @@ namespace Umbraco.Tests.PublishedContent var pastedImages = new RichTextEditorPastedImages(umbracoContextAccessor, logger, Mock.Of(), Mock.Of()); var localLinkParser = new HtmlLocalLinkParser(umbracoContextAccessor); var dataTypeService = new TestObjects.TestDataTypeService( - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages)) { Id = 1 }); + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, Mock.Of())) { Id = 1 }); var publishedContentTypeFactory = new PublishedContentTypeFactory(Mock.Of(), converters, dataTypeService); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index be63e3b5da..998fc92380 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -22,6 +22,7 @@ using Umbraco.Tests.Testing; using Umbraco.Web.Models.PublishedContent; using Umbraco.Web.PropertyEditors; using Umbraco.Web.Templates; +using Umbraco.Web.Models; namespace Umbraco.Tests.PublishedContent { @@ -53,7 +54,7 @@ namespace Umbraco.Tests.PublishedContent var dataTypeService = new TestObjects.TestDataTypeService( new DataType(new VoidEditor(logger)) { Id = 1 }, new DataType(new TrueFalsePropertyEditor(logger)) { Id = 1001 }, - new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser, pastedImages)) { Id = 1002 }, + new DataType(new RichTextPropertyEditor(logger, umbracoContextAccessor, imageSourceParser, linkParser, pastedImages, Mock.Of())) { Id = 1002 }, new DataType(new IntegerPropertyEditor(logger)) { Id = 1003 }, new DataType(new TextboxPropertyEditor(logger)) { Id = 1004 }, new DataType(new MediaPickerPropertyEditor(logger)) { Id = 1005 }); diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index 860b9b9179..a7b6d3d18a 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -199,6 +199,7 @@ namespace Umbraco.Tests.PublishedContent public DateTime UpdateDate { get; set; } public Guid Version { get; set; } public int Level { get; set; } + [Obsolete("Use the Url() extension instead")] public string Url { get; set; } public PublishedItemType ItemType => PublishedItemType.Content; diff --git a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs index 7ab329b9a0..9157a76773 100644 --- a/src/Umbraco.Tests/Runtimes/StandaloneTests.cs +++ b/src/Umbraco.Tests/Runtimes/StandaloneTests.cs @@ -97,6 +97,7 @@ namespace Umbraco.Tests.Runtimes composition.Register(Lifetime.Singleton); composition.Register(Lifetime.Singleton); composition.Register(_ => Mock.Of(), Lifetime.Singleton); + composition.Register(_ => Mock.Of(), Lifetime.Singleton); composition.RegisterUnique(f => new DistributedCache()); composition.WithCollectionBuilder().Append(); composition.RegisterUnique(); diff --git a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs index 4f7de2d230..468d4e8b03 100644 --- a/src/Umbraco.Tests/Testing/UmbracoTestBase.cs +++ b/src/Umbraco.Tests/Testing/UmbracoTestBase.cs @@ -43,6 +43,7 @@ using Current = Umbraco.Core.Composing.Current; using FileSystems = Umbraco.Core.IO.FileSystems; using Umbraco.Web.Templates; using Umbraco.Web.PropertyEditors; +using Umbraco.Core.Models; namespace Umbraco.Tests.Testing { @@ -248,6 +249,7 @@ namespace Umbraco.Tests.Testing var runtimeStateMock = new Mock(); runtimeStateMock.Setup(x => x.Level).Returns(RuntimeLevel.Run); Composition.RegisterUnique(f => runtimeStateMock.Object); + Composition.Register(_ => Mock.Of()); // ah... Composition.WithCollectionBuilder(); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 2d980461d2..dfb238b20c 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -140,12 +140,14 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg b/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg index 6c0515906f..75bf0d52af 100644 Binary files a/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg and b/src/Umbraco.Web.UI.Client/src/assets/img/installer.jpg differ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index b8ee797c82..a33796ab6d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -133,6 +133,17 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se } }; + scope.currentSectionInOverflow = function () { + if (scope.overflowingSections === 0) { + return false; + } + + var currentSection = scope.sections.filter(s => s.alias === scope.currentSection); + + return (scope.sections.indexOf(currentSection[0]) >= scope.maxSections); + + }; + loadSections(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js index 2dc0ebdf93..381ecc76c3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js @@ -18,9 +18,9 @@ function link(scope, element, attrs, ctrl) { - scope.close = function() { - if(scope.onClose) { - scope.onClose(); + scope.close = function () { + if (scope.onClose) { + scope.onClose(); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 7429f60e0d..91b0ba8754 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -42,7 +42,7 @@ $scope.page.isNew = Object.toBoolean(newVal); //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (content.parentId && content.parentId !== -1) { + if (content.parentId && content.parentId !== -1 && content.parentId !== -20) { loadBreadcrumb(); if (!watchingCulture) { $scope.$watch('culture', @@ -287,6 +287,8 @@ $scope.page.showSaveButton = true; // add ellipsis to the save button if it opens the variant overlay $scope.page.saveButtonEllipsis = content.variants && content.variants.length > 1 ? "true" : "false"; + } else { + $scope.page.showSaveButton = false; } // create the pubish combo button 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 0ed1b12000..95b2a520d1 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 @@ -328,6 +328,9 @@ // invariant nodes scope.currentUrls = scope.node.urls; } + + // figure out if multiple cultures apply across the content urls + scope.currentUrlsHaveMultipleCultures = _.keys(_.groupBy(scope.currentUrls, url => url.culture)).length > 1; } // load audit trail and redirects when on the info tab diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js index a33fd4be53..96ce8735eb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbchildselector.directive.js @@ -99,13 +99,18 @@ Use this directive to render a ui component for selecting child items to a paren @param {string} parentName (binding): The parent name. @param {string} parentIcon (binding): The parent icon. @param {number} parentId (binding): The parent id. -@param {callback} onRemove (binding): Callback when the remove button is clicked on an item. +@param {callback} onRemove (binding): Callback when removing an item.

The callback returns:

  • child: The selected item.
  • $index: The selected item index.
-@param {callback} onAdd (binding): Callback when the add button is clicked. +@param {callback} onAdd (binding): Callback when adding an item. +

The callback returns:

+
    +
  • $event: The select event.
  • +
+@param {callback} onSort (binding): Callback when sorting an item.

The callback returns:

  • $event: The select event.
  • @@ -174,16 +179,15 @@ Use this directive to render a ui component for selecting child items to a paren eventBindings.push(scope.$watch('parentName', function(newValue, oldValue){ if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } + if (oldValue === undefined || newValue === undefined) { return; } syncParentName(); - })); eventBindings.push(scope.$watch('parentIcon', function(newValue, oldValue){ if (newValue === oldValue) { return; } - if ( oldValue === undefined || newValue === undefined) { return; } + if (oldValue === undefined || newValue === undefined) { return; } syncParentIcon(); })); @@ -191,6 +195,7 @@ Use this directive to render a ui component for selecting child items to a paren // sortable options for allowed child content types scope.sortableOptions = { axis: "y", + cancel: ".unsortable", containment: "parent", distance: 10, opacity: 0.7, @@ -199,7 +204,7 @@ Use this directive to render a ui component for selecting child items to a paren zIndex: 6000, update: function (e, ui) { if(scope.onSort) { - scope.onSort(); + scope.onSort(); } } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 0369b4bd2e..149c2b5087 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -24,8 +24,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { var hasError = false; //create properties on our custom scope so we can use it in our template - scope.errorMsg = ""; - + scope.errorMsg = ""; //the property form controller api var formCtrl = ctrl[0]; @@ -34,11 +33,15 @@ function valPropertyMsg(serverValidationManager, localizationService) { //the property controller api var umbPropCtrl = ctrl[2]; //the variants controller api - var umbVariantCtrl = ctrl[3]; + var umbVariantCtrl = ctrl[3]; var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; + var currentCulture = currentProperty.culture; + + // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) + var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; var labels = {}; localizationService.localize("errors_propertyHasErrors").then(function (data) { @@ -91,23 +94,25 @@ function valPropertyMsg(serverValidationManager, localizationService) { if (!watcher) { watcher = scope.$watch("currentProperty.value", function (newValue, oldValue) { - if (angular.equals(newValue, oldValue)) { return; } var errCount = 0; + for (var e in formCtrl.$error) { if (angular.isArray(formCtrl.$error[e])) { errCount++; } - } + } //we are explicitly checking for valServer errors here, since we shouldn't auto clear // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg // is the only one, then we'll clear. - if (errCount === 0 || (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { + if (errCount === 0 + || (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) + || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { scope.errorMsg = ""; formCtrl.$setValidity('valPropertyMsg', true); } else if (showValidation && scope.errorMsg === "") { @@ -136,6 +141,21 @@ function valPropertyMsg(serverValidationManager, localizationService) { } //if there are any errors in the current property form that are not valPropertyMsg else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) { + + // errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared + if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) { + hasError = false; + showValidation = false; + scope.errorMsg = ""; + + // if there's no value, the controls can be reset, which clears the error state on formCtrl + for (let control of formCtrl.$getControls()) { + control.$setValidity(); + } + + return; + } + hasError = true; //update the validation message if we don't already have one assigned. if (showValidation && scope.errorMsg === "") { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/imageurlgenerator.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/imageurlgenerator.resource.js new file mode 100644 index 0000000000..a937cd2675 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/imageurlgenerator.resource.js @@ -0,0 +1,36 @@ +/** + * @ngdoc service + * @name umbraco.resources.imageUrlGeneratorResource + * @function + * + * @description + * Used by the various controllers to get an image URL formatted correctly for the current image URL generator + */ +(function () { + 'use strict'; + + function imageUrlGeneratorResource($http, umbRequestHelper) { + + function getCropUrl(mediaPath, width, height, imageCropMode, animationProcessMode) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "imageUrlGeneratorApiBaseUrl", + "GetCropUrl", + { mediaPath, width, height, imageCropMode, animationProcessMode })), + 'Failed to get crop URL'); + } + + + var resource = { + getCropUrl: getCropUrl + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('imageUrlGeneratorResource', imageUrlGeneratorResource); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index d481205657..5bfe66b25f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -525,7 +525,7 @@ When building a custom infinite editor view you can use the same components as a function rollback(editor) { editor.view = "views/common/infiniteeditors/rollback/rollback.html"; - if (!editor.size) editor.size = "small"; + if (!editor.size) editor.size = "medium"; open(editor); } @@ -784,7 +784,7 @@ When building a custom infinite editor view you can use the same components as a * @methodOf umbraco.services.editorService * * @description - * Opens the user group picker in infinite editing, the submit callback returns the saved template + * Opens the template editor in infinite editing, the submit callback returns the saved template * @param {Object} editor rendering options * @param {String} editor.id The template id * @param {Callback} editor.submit Submits the editor diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less index 456601a7bd..ff4122b258 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-toggle.less @@ -103,14 +103,21 @@ - +.umb-toggle.umb-toggle--disabled.umb-toggle--checked, .umb-toggle.umb-toggle--disabled { .umb-toggle__toggle { cursor: not-allowed; - border-color: @gray-5; + background-color: @gray-9; + border-color: @gray-9; + } + .umb-toggle__icon--left { + color: @gray-6; + } + .umb-toggle__icon--right { + color: @gray-6; } .umb-toggle__handler { - background-color: @gray-5; + background-color: @gray-10; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less index da690663d0..937f746c56 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-child-selector.less @@ -16,23 +16,24 @@ .umb-child-selector__child.-placeholder { border: 1px dashed @gray-8; background: none; - cursor: pointer; text-align: center; justify-content: center; - - color:@ui-action-type; + width: 100%; + color: @ui-action-type; + &:hover { - color:@ui-action-type-hover; - border-color:@ui-action-type-hover; - text-decoration:none; + color: @ui-action-type-hover; + border-color: @ui-action-type-hover; + text-decoration: none; } } .umb-child-selector__children-container { - margin-left: 30px; - .umb-child-selector__child { - cursor: move; - } + margin-left: 30px; + + .umb-child-selector__child.ui-sortable-handle { + cursor: move; + } } .umb-child-selector__child-description { @@ -65,5 +66,6 @@ } .umb-child-selector__child-remove { - cursor: pointer; + background: none; + border: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index b9efd8f7d8..1c5c275642 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -162,5 +162,6 @@ &.umb-form-check--disabled { cursor: not-allowed !important; opacity: 0.5; + pointer-events: none; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less index 68cb9b2e58..ec598c17eb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-search.less @@ -23,6 +23,7 @@ background-color: transparent; border-color: @ui-action-discreet-border; transition: background-color .1s linear, border-color .1s linear, color .1s linear, width .1s ease-in-out, padding-left .1s ease-in-out; + cursor: pointer; } &:focus-within, &:hover { @@ -39,6 +40,7 @@ background-color: white; color: @ui-action-discreet-border-hover; border-color: @ui-action-discreet-border-hover; + cursor: unset; } input:focus, &:focus-within input, &.--has-value input { 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 4168ab3c39..716693c778 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 @@ -135,6 +135,8 @@ .umb-nested-content__header-bar:hover .umb-nested-content__icons, +.umb-nested-content__header-bar:focus .umb-nested-content__icons, +.umb-nested-content__header-bar:focus-within .umb-nested-content__icons, .umb-nested-content__item--active > .umb-nested-content__header-bar .umb-nested-content__icons { opacity: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index f754a09368..939fd79826 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -29,7 +29,8 @@ .umb-node-preview__icon { display: flex; width: 25px; - height: 25px; + min-height: 25px; + height: 100%; justify-content: center; align-items: center; font-size: 20px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less index 3f0b981ac6..17c4bf1a55 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less @@ -61,6 +61,7 @@ opacity: 0; transition: opacity 120ms; } +.umb-property:focus-within .umb-property-actions__toggle, .umb-property:hover .umb-property-actions__toggle, .umb-property .umb-property-actions__toggle:focus { opacity: 1; 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 index 08b1a1b5e1..9550acfb1b 100644 --- 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 @@ -1,6 +1,7 @@ .umb-property-file-upload { .umb-upload-button-big { + max-width: (@propertyEditorLimitedWidth - 40); display: block; padding: 20px; opacity: 1; diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index efc0178ca2..f5c499cbfc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -418,7 +418,7 @@ // -------------------------------------------------- // Limit width of specific property editors .umb-property-editor--limit-width { - max-width: 800px; + max-width: @propertyEditorLimitedWidth; } // Horizontal dividers 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 c25941514e..764b73c593 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -722,6 +722,10 @@ // // File upload // -------------------------------------------------- +.umb-fileupload { + display: flex; +} + .umb-fileupload .preview { border-radius: 5px; border: 1px solid @gray-6; diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index d6d38f540a..0e43f428ae 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -164,4 +164,13 @@ .mce-fullscreen { position: absolute; + + .mce-in { + position: fixed; + top: 35px !important; + } + + umb-editor__overlay, .umb-editor { + position: fixed; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 27b11b1c3b..1f19786b3c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -55,7 +55,7 @@ ul.sections { transition: opacity .1s linear, box-shadow .1s; } - &.current a { + &.current > a { color: @ui-active; &::after { @@ -76,6 +76,13 @@ ul.sections { transition: opacity .1s linear; } + &.current { + i { + opacity: 1; + background: @ui-active; + } + } + &:hover i { opacity: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 5f2c6f91f6..503c5f9b11 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -244,6 +244,7 @@ @paddingSmall: 2px 10px; // 26px @paddingMini: 0 6px; // 22px +@propertyEditorLimitedWidth: 800px; // Disabled this to keep consistency throughout the backoffice UI. Untill a better solution is thought up, this will do. @baseBorderRadius: 3px; // 2px; diff --git a/src/Umbraco.Web.UI.Client/src/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/navigation.controller.js index 194c45afe6..281be2d331 100644 --- a/src/Umbraco.Web.UI.Client/src/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/navigation.controller.js @@ -257,6 +257,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar evts.push(eventsService.on("app.ready", function (evt, data) { $scope.authenticated = true; ensureInit(); + ensureMainCulture(); })); // event for infinite editors @@ -279,8 +280,22 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } })); - - + /** + * For multi language sites, this ensures that mculture is set to either the last selected language or the default one + */ + function ensureMainCulture() { + if ($location.search().mculture) { + return; + } + var language = lastLanguageOrDefault(); + if (!language) { + return; + } + // trigger a language selection in the next digest cycle + $timeout(function () { + $scope.selectLanguage(language); + }); + } /** * Based on the current state of the application, this configures the scope variables that control the main tree and language drop down @@ -385,28 +400,19 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar if ($scope.languages.length > 1) { //if there's already one set, check if it exists - var currCulture = null; + var language = null; var mainCulture = $location.search().mculture; if (mainCulture) { - currCulture = _.find($scope.languages, function (l) { + language = _.find($scope.languages, function (l) { return l.culture.toLowerCase() === mainCulture.toLowerCase(); }); } - if (!currCulture) { - // 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; - } + if (!language) { + language = lastLanguageOrDefault(); + + if (language) { + $location.search("mculture", language.culture); } - $location.search("mculture", defaultCulture ? defaultCulture : null); } } @@ -431,6 +437,25 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar }); }); } + + function lastLanguageOrDefault() { + if (!$scope.languages || $scope.languages.length <= 1) { + return null; + } + // see if we can find a culture in the cookie set when changing language + var lastCulture = $cookies.get("UMB_MCULTURE"); + var language = lastCulture ? _.find($scope.languages, function (l) { + return l.culture.toLowerCase() === lastCulture.toLowerCase(); + }) : null; + if (!language) { + // no luck, look for the default language + language = _.find($scope.languages, function (l) { + return l.isDefault; + }); + } + return language; + } + function nodeExpandedHandler(args) { //store the reference to the expanded node path if (args.node) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js index b8581d28d0..9640f2eba2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js @@ -56,7 +56,8 @@ "validation_validateAsEmail", "validation_validateAsNumber", "validation_validateAsUrl", - "validation_enterCustomValidation" + "validation_enterCustomValidation", + "validation_fieldIsMandatory" ]; localizationService.localizeMany(labels) @@ -66,6 +67,7 @@ vm.labels.validateAsNumber = data[1]; vm.labels.validateAsUrl = data[2]; vm.labels.customValidation = data[3]; + vm.labels.fieldIsMandatory = data[4]; vm.validationTypes = [ { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 4474390199..77ee276e3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -84,14 +84,15 @@
    - - + on-click="vm.toggleValidation()" + label-on="{{vm.labels.fieldIsMandatory}}" + label-off="{{vm.labels.fieldIsMandatory}}" + show-labels="true" + label-position="right" focus-when="{{vm.focusOnMandatoryField}}" + class="mb1"> -
  • +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html index e358d75b9e..064dcf6945 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -70,7 +70,7 @@
    - +

    Oh, we got lost!

    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html index a2caff4ff2..9e161d6b4e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html @@ -1,8 +1,7 @@
    -
    -
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html index 93801f14b8..d4e75908bd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umb-mini-search.html @@ -9,6 +9,7 @@ ng-model="vm.model" ng-change="vm.onChange()" ng-keydown="vm.onKeyDown($event)" + ng-blur="vm.onBlur($event)" prevent-enter-submit no-dirty-check> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js index 994129708f..d7aee744e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-search/umbminisearch.component.js @@ -10,7 +10,8 @@ bindings: { model: "=", onStartTyping: "&?", - onSearch: "&?" + onSearch: "&?", + onBlur: "&?" } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html index 32aa5ed1a2..014297ae51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/users/umb-user-preview.html @@ -1,12 +1,11 @@
    - +
    @@ -15,7 +14,6 @@
    - Remove -
    - -
    \ No newline at end of file + +
    + diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 64f601f8b7..1a1254fcf1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -22,10 +22,14 @@ function contentCreateController($scope, function initialize() { $scope.loading = true; $scope.allowedTypes = null; - $scope.countTypes = contentTypeResource.getCount; var getAllowedTypes = contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); + if ($scope.allowedTypes.length === 0) { + contentTypeResource.getCount().then(function(count) { + $scope.countTypes = count; + }); + } }); var getCurrentUser = authResource.getCurrentUser().then(function (currentUser) { if (currentUser.allowedSections.indexOf("settings") > -1) { diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js index a35a404c24..cdabb05fd4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js @@ -220,7 +220,7 @@ function startupLatestEditsController($scope) { } angular.module("umbraco").controller("Umbraco.Dashboard.StartupLatestEditsController", startupLatestEditsController); -function MediaFolderBrowserDashboardController($rootScope, $scope, $location, contentTypeResource, userService) { +function MediaFolderBrowserDashboardController($scope, $routeParams, $location, contentTypeResource, userService) { var currentUser = {}; @@ -251,6 +251,8 @@ function MediaFolderBrowserDashboardController($rootScope, $scope, $location, co view: dt.view }; + // tell the list view to list content at root + $routeParams.id = -1; }); } else if (currentUser.startMediaIds.length > 0){ diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html index a7f7d45087..f1388753a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/members/membersdashboardvideos.html @@ -4,7 +4,7 @@ -

    Hours of Umbraco training videos are only a click away

    +

    Hours of Umbraco training videos are only a click away

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index a5884c2355..a7ce67fc0b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -73,14 +73,14 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, var content = $scope.content; - // we need to check wether an app is present in the current data, if not we will present the default app. + // we need to check whether an app is present in the current data, if not we will present the default app. var isAppPresent = false; // on first init, we dont have any apps. but if we are re-initializing, we do, but ... if ($scope.app) { // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) - _.forEach(content.apps, function(app) { + content.apps.forEach(app => { if (app === $scope.app) { isAppPresent = true; } @@ -88,7 +88,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { - _.forEach(content.apps, function(app) { + content.apps.forEach(app => { if (app.alias === $scope.app.alias) { isAppPresent = true; app.active = true; @@ -182,24 +182,26 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, formHelper.resetForm({ scope: $scope }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - editorState.set($scope.content); - - syncTreeNode($scope.content, data.path); - - init(); - - $scope.page.saveButtonState = "success"; - // close the editor if it's infinite mode + // submit function manages rebinding changes if(infiniteMode && $scope.model.submit) { $scope.model.mediaNode = $scope.content; $scope.model.submit($scope.model); + } else { + // if not infinite mode, rebind changed props etc + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + + editorState.set($scope.content); + + syncTreeNode($scope.content, data.path); + + $scope.page.saveButtonState = "success"; + + init(); } }, function(err) { @@ -245,7 +247,7 @@ function mediaEditController($scope, $routeParams, $q, appState, mediaResource, syncTreeNode($scope.content, data.path, true); } - if ($scope.content.parentId && $scope.content.parentId != -1) { + if ($scope.content.parentId && $scope.content.parentId !== -1 && $scope.content.parentId !== -21) { //We fetch all ancestors of the node to generate the footer breadcrump navigation entityResource.getAncestors(nodeId, "media") .then(function (anc) { 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 3c954b9316..139a2515b5 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 @@ -276,6 +276,9 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time } $scope.reloadView = function (id, reloadActiveNode) { + if (!id) { + return; + } $scope.viewLoaded = false; $scope.folders = []; @@ -713,45 +716,12 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time } function initView() { - //default to root id if the id is undefined var id = $routeParams.id; if (id === undefined) { - id = -1; + // no ID found in route params - don't list anything as we don't know for sure where we are + return; } - getContentTypesCallback(id).then(function (listViewAllowedTypes) { - $scope.listViewAllowedTypes = listViewAllowedTypes; - - var blueprints = false; - _.each(listViewAllowedTypes, function (allowedType) { - if (_.isEmpty(allowedType.blueprints)) { - // this helps the view understand that there are no blueprints available - allowedType.blueprints = null; - } - else { - blueprints = true; - // turn the content type blueprints object into an array of sortable objects for the view - allowedType.blueprints = _.map(_.pairs(allowedType.blueprints || {}), function (pair) { - return { - id: pair[0], - name: pair[1] - }; - }); - } - }); - - if (listViewAllowedTypes.length === 1 && blueprints === false) { - $scope.createAllowedButtonSingle = true; - } - if (listViewAllowedTypes.length === 1 && blueprints === true) { - $scope.createAllowedButtonSingleWithBlueprints = true; - } - if (listViewAllowedTypes.length > 1) { - $scope.createAllowedButtonMultiWithBlueprints = true; - } - }); - - $scope.contentId = id; $scope.isTrashed = editorState.current ? editorState.current.trashed : id === "-20" || id === "-21"; @@ -765,6 +735,40 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time $scope.options.allowBulkMove || $scope.options.allowBulkDelete; + if ($scope.isTrashed === false) { + getContentTypesCallback(id).then(function (listViewAllowedTypes) { + $scope.listViewAllowedTypes = listViewAllowedTypes; + + var blueprints = false; + _.each(listViewAllowedTypes, function (allowedType) { + if (_.isEmpty(allowedType.blueprints)) { + // this helps the view understand that there are no blueprints available + allowedType.blueprints = null; + } + else { + blueprints = true; + // turn the content type blueprints object into an array of sortable objects for the view + allowedType.blueprints = _.map(_.pairs(allowedType.blueprints || {}), function (pair) { + return { + id: pair[0], + name: pair[1] + }; + }); + } + }); + + if (listViewAllowedTypes.length === 1 && blueprints === false) { + $scope.createAllowedButtonSingle = true; + } + if (listViewAllowedTypes.length === 1 && blueprints === true) { + $scope.createAllowedButtonSingleWithBlueprints = true; + } + if (listViewAllowedTypes.length > 1) { + $scope.createAllowedButtonMultiWithBlueprints = true; + } + }); + } + $scope.reloadView($scope.contentId); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 8c6194d638..cde99d7b9e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -48,49 +48,45 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // This is done by remapping the int/guid ids into a new array of items, where we create "Deleted item" placeholders // when there is no match for a selected id. This will ensure that the values being set on save, are the same as before. - medias = _.map(ids, - function (id) { - var found = _.find(medias, - function (m) { - // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and - // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() - // compares and be completely sure it works. - return m.udi.toString() === id.toString() || m.id.toString() === id.toString(); - }); - if (found) { - return found; - } else { - return { - name: vm.labels.deletedItem, - id: $scope.model.config.idType !== "udi" ? id : null, - udi: $scope.model.config.idType === "udi" ? id : null, - icon: "icon-picture", - thumbnail: null, - trashed: true - }; - } - }); + medias = ids.map(id => { + var found = medias.find(m => + // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and + // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() + // compares and be completely sure it works. + m.udi.toString() === id.toString() || m.id.toString() === id.toString()); + + if (found) { + return found; + } else { + return { + name: vm.labels.deletedItem, + id: $scope.model.config.idType !== "udi" ? id : null, + udi: $scope.model.config.idType === "udi" ? id : null, + icon: "icon-picture", + thumbnail: null, + trashed: true + }; + } + }); - _.each(medias, - function (media, i) { + medias.forEach(media => { + if (!media.extension && media.id && media.metaData) { + media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); + } - if (!media.extension && media.id && media.metaData) { - media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); - } + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + } - // if there is no thumbnail, try getting one if the media is not a placeholder item - if (!media.thumbnail && media.id && media.metaData) { - media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); - } + $scope.mediaItems.push(media); - $scope.mediaItems.push(media); - - if ($scope.model.config.idType === "udi") { - $scope.ids.push(media.udi); - } else { - $scope.ids.push(media.id); - } - }); + if ($scope.model.config.idType === "udi") { + $scope.ids.push(media.udi); + } else { + $scope.ids.push(media.id); + } + }); sync(); }); @@ -100,7 +96,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl function sync() { $scope.model.value = $scope.ids.join(); removeAllEntriesAction.isDisabled = $scope.ids.length === 0; - }; + } function setDirty() { angularHelper.getCurrentForm($scope).$setDirty(); @@ -111,18 +107,17 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // reload. We only reload the images that is already picked but has been updated. // We have to get the entities from the server because the media // can be edited without being selected - _.each($scope.images, - function (image, i) { - if (updatedMediaNodes.indexOf(image.udi) !== -1) { - image.loading = true; - entityResource.getById(image.udi, "media") - .then(function (mediaEntity) { - angular.extend(image, mediaEntity); - image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); - image.loading = false; - }); - } - }); + $scope.mediaItems.forEach(media => { + if (updatedMediaNodes.indexOf(media.udi) !== -1) { + media.loading = true; + entityResource.getById(media.udi, "Media") + .then(function (mediaEntity) { + angular.extend(media, mediaEntity); + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); + media.loading = false; + }); + } + }); } function init() { @@ -177,20 +172,20 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // the media picker is using media entities so we get the // entity so we easily can format it for use in the media grid if (model && model.mediaNode) { - entityResource.getById(model.mediaNode.id, "media") + entityResource.getById(model.mediaNode.id, "Media") .then(function (mediaEntity) { // if an image is selecting more than once // we need to update all the media items - angular.forEach($scope.images, function (image) { - if (image.id === model.mediaNode.id) { - angular.extend(image, mediaEntity); - image.thumbnail = mediaHelper.resolveFileFromEntity(image, true); + $scope.mediaItems.forEach(media => { + if (media.id === model.mediaNode.id) { + angular.extend(media, mediaEntity); + media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } }); }); } }, - close: function (model) { + close: function () { editorService.close(); } }; @@ -210,7 +205,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl editorService.close(); - _.each(model.selection, function (media, i) { + model.selection.forEach(media => { // if there is no thumbnail, try getting one if the media is not a placeholder item if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); @@ -280,16 +275,14 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl disabled: !multiPicker, items: "li:not(.add-wrapper)", cancel: ".unsortable", - update: function (e, ui) { + update: function () { setDirty(); $timeout(function() { // TODO: Instead of doing this with a timeout would be better to use a watch like we do in the // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. - $scope.ids = _.map($scope.mediaItems, - function (item) { - return $scope.model.config.idType === "udi" ? item.udi : item.id; - }); + $scope.ids = $scope.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id); + sync(); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index b17906272d..c4dba4d373 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -36,16 +36,16 @@
    - -
  • -
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index 2e4313ec76..172f9b2249 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -147,7 +147,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en _.each($scope.model.value, function (item){ // we must reload the "document" link URLs to match the current editor culture - if (item.udi.indexOf("/document/") > 0) { + if (item.udi && item.udi.indexOf("/document/") > 0) { item.url = null; entityResource.getUrlByUdi(item.udi).then(function (data) { item.url = data; 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 7de3a5b567..f2538c66c1 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 @@ -221,6 +221,7 @@ if (vm.overlayMenu.availableItems.length === 1 && vm.overlayMenu.pasteItems.length === 0) { // only one scaffold type - no need to display the picker addNode(vm.scaffolds[0].contentTypeAlias); + vm.overlayMenu = null; return; } @@ -276,6 +277,9 @@ }; vm.getName = function (idx) { + if (!model.value || !model.value.length) { + return ""; + } var name = ""; @@ -325,6 +329,10 @@ }; vm.getIcon = function (idx) { + if (!model.value || !model.value.length) { + return ""; + } + var scaffold = getScaffold(model.value[idx].ncContentTypeAlias); return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder"; } @@ -480,10 +488,12 @@ } // Enforce min items if we only have one scaffold type + var modelWasChanged = false; if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { for (var i = vm.nodes.length; i < model.config.minItems; i++) { addNode(vm.scaffolds[0].contentTypeAlias); } + modelWasChanged = true; } // If there is only one item, set it as current node @@ -495,6 +505,10 @@ vm.inited = true; + if (modelWasChanged) { + updateModel(); + } + updatePropertyActionStates(); checkAbilityToPasteContent(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js index a20289d076..f5f0f7f2a2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js @@ -56,7 +56,7 @@ return value.toFixed(stepDecimalPlaces); }, from: function (value) { - return value; + return Number(value); } }, "range": { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.html b/src/Umbraco.Web.UI.Client/src/views/users/group.html index 0244819655..eae6dbd75c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.html @@ -40,13 +40,12 @@ on-remove="vm.removeSelectedItem($index, vm.userGroup.sections)"> - + @@ -61,14 +60,14 @@ on-remove="vm.clearStartNode('content')"> - + + @@ -85,14 +84,14 @@ on-remove="vm.clearStartNode('media')"> - + + @@ -127,13 +126,12 @@ on-edit="vm.setPermissionsForNode(node)"> - + @@ -155,13 +153,11 @@ on-remove="vm.removeSelectedItem($index, vm.userGroup.users)"> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index e0cf09da50..bb3efaede2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -75,13 +75,11 @@ on-remove="model.removeSelectedItem($index, model.user.userGroups)"> - + @@ -100,13 +98,12 @@ name="model.labels.noStartNodes"> - + @@ -125,13 +122,12 @@ name="model.labels.noStartNodes"> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 935b1bdfb1..f3c28fdb9d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -14,7 +14,7 @@ vm.userStates = []; vm.selection = []; vm.newUser = {}; - vm.usersOptions = {filter:null}; + vm.usersOptions = {}; vm.userSortData = [ { label: "Name (A-Z)", key: "Name", direction: "Ascending" }, { label: "Name (Z-A)", key: "Name", direction: "Descending" }, @@ -112,6 +112,7 @@ vm.selectAll = selectAll; vm.areAllSelected = areAllSelected; vm.searchUsers = searchUsers; + vm.onBlurSearch = onBlurSearch; vm.getFilterName = getFilterName; vm.setUserStatesFilter = setUserStatesFilter; vm.setUserGroupFilter = setUserGroupFilter; @@ -150,10 +151,12 @@ function initViewOptions() { // Start with default view options. + vm.usersOptions.filter = ""; vm.usersOptions.orderBy = "Name"; vm.usersOptions.orderDirection = "Ascending"; // Update from querystring if available. + initViewOptionFromQueryString("filter"); initViewOptionFromQueryString("orderBy"); initViewOptionFromQueryString("orderDirection"); initViewOptionFromQueryString("pageNumber"); @@ -451,7 +454,8 @@ var search = _.debounce(function () { $scope.$apply(function () { - changePageNumber(1); + vm.usersOptions.pageNumber = 1; + getUsers(); }); }, 500); @@ -459,6 +463,10 @@ search(); } + function onBlurSearch() { + updateLocation("filter", vm.usersOptions.filter); + } + function getFilterName(array) { var name = vm.labels.all; var found = false; @@ -547,6 +555,7 @@ } function updateLocation(key, value) { + $location.search("filter", vm.usersOptions.filter);// update filter, but first when something else requests a url update. $location.search(key, value); } @@ -657,7 +666,8 @@ function usersOptionsAsQueryString() { var qs = "?orderBy=" + vm.usersOptions.orderBy + "&orderDirection=" + vm.usersOptions.orderDirection + - "&pageNumber=" + vm.usersOptions.pageNumber; + "&pageNumber=" + vm.usersOptions.pageNumber + + "&filter=" + vm.usersOptions.filter; qs += addUsersOptionsFilterCollectionToQueryString("userStates", vm.usersOptions.userStates); qs += addUsersOptionsFilterCollectionToQueryString("userGroups", vm.usersOptions.userGroups); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html index 638e6376c3..bb53413060 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.html @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml index 5aa09a3cb3..64be8eb60c 100755 --- a/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/PartialViewMacros/Templates/Gallery.cshtml @@ -29,13 +29,13 @@ @* a single image *@ if (media.IsDocumentType("Image")) { - @Render(media); + @Render(media) } @* a folder with images under it *@ foreach (var image in media.Children()) { - @Render(image); + @Render(image) } } diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml index defe59d808..131b0515ae 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml @@ -2,31 +2,31 @@ @using Umbraco.Web.Templates @using Newtonsoft.Json.Linq -@* +@* Razor helpers located at the bottom of this file *@ @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; - +
    @if (oneColumn) { foreach (var section in Model.sections) {
    @foreach (var row in section.rows) { - @renderRow(row); + @renderRow(row) }
    - } - }else { + } + }else {
    @foreach (var s in Model.sections) {
    @foreach (var row in s.rows) { - @renderRow(row); + @renderRow(row) }
    @@ -85,4 +85,4 @@ return new MvcHtmlString(string.Join(" ", attrs)); } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index 9333628ed6..23fee33043 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -12,7 +12,7 @@ foreach (var section in Model.sections) {
    @foreach (var row in section.rows) { - @renderRow(row, true); + @renderRow(row, true) }
    } @@ -23,7 +23,7 @@
    @foreach (var row in s.rows) { - @renderRow(row, false); + @renderRow(row, false) }
    diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml index ea79ce41ad..e20b717ed5 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml @@ -2,20 +2,24 @@ @using Umbraco.Web.Templates @if (Model.value != null) -{ +{ var url = Model.value.image; if(Model.editor.config != null && Model.editor.config.size != null){ - url += "?width=" + Model.editor.config.size.width; - url += "&height=" + Model.editor.config.size.height; - - if(Model.value.focalPoint != null){ - url += "¢er=" + Model.value.focalPoint.top +"," + Model.value.focalPoint.left; - url += "&mode=crop"; - } + url = ImageCropperTemplateExtensions.GetCropUrl(url, + width: Model.editor.config.size.width, + height: Model.editor.config.size.height, + cropDataSet: Model.value.focalPoint == null ? null : new Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue + { + FocalPoint = new Umbraco.Core.PropertyEditors.ValueConverters.ImageCropperValue.ImageCropperFocalPoint + { + Top = Model.value.focalPoint.top, + Left = Model.value.focalPoint.left + } + }); } var altText = Model.value.altText ?? Model.value.caption ?? string.Empty; - + @altText if (Model.value.caption != null) diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs index 24479b8415..0cecba7b7b 100644 --- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs @@ -53,7 +53,7 @@ namespace Umbraco.Web.Cache { macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); } - }; + } base.Refresh(json); } diff --git a/src/Umbraco.Web/Controllers/UmbLoginController.cs b/src/Umbraco.Web/Controllers/UmbLoginController.cs index 2ff80e2668..88bc17abff 100644 --- a/src/Umbraco.Web/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web/Controllers/UmbLoginController.cs @@ -46,7 +46,7 @@ namespace Umbraco.Web.Controllers // if it's not a local url we'll redirect to the root of the current site return Redirect(Url.IsLocalUrl(model.RedirectUrl) ? model.RedirectUrl - : CurrentPage.AncestorOrSelf(1).Url); + : CurrentPage.AncestorOrSelf(1).Url()); } //redirect to current page by default diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index dc46a97df4..89413db84d 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -314,6 +314,10 @@ namespace Umbraco.Web.Editors "tinyMceApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.UploadImage()) }, + { + "imageUrlGeneratorApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetCropUrl(null, null, null, null, null)) + }, } }, { diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index f5d72894ca..521626f666 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -869,7 +869,7 @@ namespace Umbraco.Web.Editors return true; } - + /// /// Helper method to perform the saving of the content and add the notifications to the result @@ -1161,14 +1161,14 @@ namespace Umbraco.Web.Editors //validate if we can publish based on the mandatory language requirements var canPublish = ValidatePublishingMandatoryLanguages( cultureErrors, - contentItem, cultureVariants, mandatoryCultures, + contentItem, cultureVariants, mandatoryCultures, mandatoryVariant => mandatoryVariant.Publish); //Now check if there are validation errors on each variant. //If validation errors are detected on a variant and it's state is set to 'publish', then we //need to change it to 'save'. //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. - + foreach (var variant in contentItem.Variants) { if (cultureErrors.Contains(variant.Culture)) @@ -1656,14 +1656,14 @@ namespace Umbraco.Web.Editors [HttpPost] public DomainSave PostSaveLanguageAndDomains(DomainSave model) { - foreach(var domain in model.Domains) + foreach (var domain in model.Domains) { try { var uri = DomainUtilities.ParseUriFromDomainName(domain.Name, Request.RequestUri); } catch (UriFormatException) - { + { var response = Request.CreateValidationErrorResponse(Services.TextService.Localize("assignDomain/invalidDomain")); throw new HttpResponseException(response); } @@ -1829,7 +1829,7 @@ namespace Umbraco.Web.Editors base.HandleInvalidModelState(display); } - + /// /// Maps the dto property values and names to the persisted model /// @@ -1842,7 +1842,7 @@ namespace Umbraco.Web.Editors var culture = property.PropertyType.VariesByCulture() ? variant.Culture : null; var segment = property.PropertyType.VariesBySegment() ? variant.Segment : null; return (culture, segment); - } + } var variantIndex = 0; @@ -1884,15 +1884,15 @@ namespace Umbraco.Web.Editors (save, property) => { // Get property value - (var culture, var segment) = PropertyCultureAndSegment(property, variant); - return property.GetValue(culture, segment); + (var culture, var segment) = PropertyCultureAndSegment(property, variant); + return property.GetValue(culture, segment); }, (save, property, v) => { // Set property value (var culture, var segment) = PropertyCultureAndSegment(property, variant); - property.SetValue(v, culture, segment); - }, + property.SetValue(v, culture, segment); + }, variant.Culture); variantIndex++; @@ -2172,7 +2172,10 @@ namespace Umbraco.Web.Editors /// private ContentItemDisplay MapToDisplay(IContent content) { - var display = Mapper.Map(content); + var display = Mapper.Map(content, context => + { + context.Items["CurrentUser"] = Security.CurrentUser; + }); display.AllowPreview = display.AllowPreview && content.Trashed == false && content.ContentType.IsElement == false; return display; } diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 5f5f5104cb..ad92d40ecf 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -427,7 +427,7 @@ namespace Umbraco.Web.Editors { var propertyEditor = propertyEditors.SingleOrDefault(x => x.Alias == dataType.Alias); if (propertyEditor != null) - dataType.HasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); ; + dataType.HasPrevalues = propertyEditor.GetConfigurationEditor().Fields.Any(); } var grouped = dataTypes diff --git a/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs new file mode 100644 index 0000000000..87d7e29619 --- /dev/null +++ b/src/Umbraco.Web/Editors/ImageUrlGeneratorController.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Models; +using Umbraco.Web.Models; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller used for getting URLs for images with parameters + /// + /// + /// + /// This controller allows for retrieving URLs for processed images, such as resized, cropped, + /// or otherwise altered. These can be different based on the IImageUrlGenerator + /// implementation in use, and so the BackOffice could should not rely on hard-coded string + /// building to generate correct URLs + /// + /// + public class ImageUrlGeneratorController : UmbracoAuthorizedJsonController + { + private readonly IImageUrlGenerator _imageUrlGenerator; + + public ImageUrlGeneratorController(IImageUrlGenerator imageUrlGenerator) + { + _imageUrlGenerator = imageUrlGenerator; + } + + public string GetCropUrl(string mediaPath, int? width = null, int? height = null, ImageCropMode? imageCropMode = null, string animationProcessMode = null) + { + return mediaPath.GetCropUrl(_imageUrlGenerator, null, width: width, height: height, imageCropMode: imageCropMode, animationProcessMode: animationProcessMode); + } + } +} diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index db6706d1bb..f3682a5b43 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -2,8 +2,10 @@ using System.IO; using System.Net; using System.Net.Http; +using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; +using Umbraco.Core.Models; using Umbraco.Web.Media; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi; @@ -19,11 +21,17 @@ namespace Umbraco.Web.Editors { private readonly IMediaFileSystem _mediaFileSystem; private readonly IContentSection _contentSection; + private readonly IImageUrlGenerator _imageUrlGenerator; - public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection) + [Obsolete("This constructor will be removed in a future release. Please use the constructor with the IImageUrlGenerator overload")] + public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection) : this (mediaFileSystem, contentSection, Current.ImageUrlGenerator) + { + } + public ImagesController(IMediaFileSystem mediaFileSystem, IContentSection contentSection, IImageUrlGenerator imageUrlGenerator) { _mediaFileSystem = mediaFileSystem; _contentSection = contentSection; + _imageUrlGenerator = imageUrlGenerator; } /// @@ -75,12 +83,10 @@ namespace Umbraco.Web.Editors // so ignore and we won't set a last modified date. } - // TODO: When we abstract imaging for netcore, we are actually just going to be abstracting a URI builder for images, this - // is one of those places where this can be used. + var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null; + var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd }); - var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : string.Empty; - - response.Headers.Location = new Uri($"{imagePath}?upscale=false&width={width}&animationprocessmode=first&mode=max{rnd}", UriKind.RelativeOrAbsolute); + response.Headers.Location = new Uri(imageUrl, UriKind.RelativeOrAbsolute); return response; } diff --git a/src/Umbraco.Web/Editors/PackageInstallController.cs b/src/Umbraco.Web/Editors/PackageInstallController.cs index 6b2bd07fbd..1030498734 100644 --- a/src/Umbraco.Web/Editors/PackageInstallController.cs +++ b/src/Umbraco.Web/Editors/PackageInstallController.cs @@ -102,8 +102,8 @@ namespace Umbraco.Web.Editors model.LicenseUrl = ins.LicenseUrl; model.Readme = ins.Readme; model.ConflictingMacroAliases = ins.Warnings.ConflictingMacros.ToDictionary(x => x.Name, x => x.Alias); - model.ConflictingStyleSheetNames = ins.Warnings.ConflictingStylesheets.ToDictionary(x => x.Name, x => x.Alias); ; - model.ConflictingTemplateAliases = ins.Warnings.ConflictingTemplates.ToDictionary(x => x.Name, x => x.Alias); ; + model.ConflictingStyleSheetNames = ins.Warnings.ConflictingStylesheets.ToDictionary(x => x.Name, x => x.Alias); + model.ConflictingTemplateAliases = ins.Warnings.ConflictingTemplates.ToDictionary(x => x.Name, x => x.Alias); model.ContainsUnsecureFiles = ins.Warnings.UnsecureFiles.Any(); model.Url = ins.Url; model.Version = ins.Version; diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs new file mode 100644 index 0000000000..7ac5578d75 --- /dev/null +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -0,0 +1,379 @@ +using System; +using Newtonsoft.Json.Linq; +using System.Globalization; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.Models; + +namespace Umbraco.Web +{ + public static class ImageCropperTemplateCoreExtensions + { + /// + /// Gets the ImageProcessor Url by the crop alias (from the "umbracoFile" property alias) on the IPublishedContent item + /// + /// + /// The IPublishedContent item. + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// The ImageProcessor.Web Url. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); + } + + /// + /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The property alias of the property containing the Json data e.g. umbracoFile + /// + /// + /// The crop alias e.g. thumbnail + /// + /// + /// The ImageProcessor.Web Url. + /// + public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias, IImageUrlGenerator imageUrlGenerator) + { + return mediaItem.GetCropUrl(imageUrlGenerator, propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); + } + + /// + /// Gets the ImageProcessor Url from the IPublishedContent item. + /// + /// + /// The IPublishedContent item. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// Property alias of the property containing the Json data. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point, to generate an output image using the focal point instead of the predefined crop + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters. + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this IPublishedContent mediaItem, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string propertyAlias = Constants.Conventions.Media.File, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + bool cacheBuster = true, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (mediaItem == null) throw new ArgumentNullException("mediaItem"); + + var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; + + if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) + return string.Empty; + + var mediaItemUrl = mediaItem.MediaUrl(propertyAlias: propertyAlias); + + //get the default obj from the value converter + var cropperValue = mediaItem.Value(propertyAlias); + + //is it strongly typed? + var stronglyTyped = cropperValue as ImageCropperValue; + if (stronglyTyped != null) + { + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //this shouldn't be the case but we'll check + var jobj = cropperValue as JObject; + if (jobj != null) + { + stronglyTyped = jobj.ToObject(); + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + //it's a single string + return GetCropUrl( + mediaItemUrl, imageUrlGenerator, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, + cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The Json data from the Umbraco Core Image Cropper property editor + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + int? width = null, + int? height = null, + string imageCropperValue = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true) + { + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageCropperValue cropDataSet = null; + if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + cropDataSet = imageCropperValue.DeserializeImageCropperValue(); + } + return GetCropUrl( + imageUrl, imageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); + } + + /// + /// Gets the ImageProcessor Url from the image path. + /// + /// + /// The image url. + /// + /// + /// The generator that will process all the options and the image URL to return a full image urls with all processing options appended + /// + /// + /// + /// The width of the output image. + /// + /// + /// The height of the output image. + /// + /// + /// The crop alias. + /// + /// + /// Quality percentage of the output image. + /// + /// + /// The image crop mode. + /// + /// + /// The image crop anchor. + /// + /// + /// Use focal point to generate an output image using the focal point instead of the predefined crop if there is one + /// + /// + /// Use crop dimensions to have the output image sized according to the predefined crop sizes, this will override the width and height parameters + /// + /// + /// Add a serialized date of the last edit of the item to ensure client cache refresh when updated + /// + /// + /// These are any query string parameters (formatted as query strings) that ImageProcessor supports. For example: + /// + /// + /// + /// + /// + /// Use a dimension as a ratio + /// + /// + /// If the image should be upscaled to requested dimensions + /// + /// + /// The . + /// + public static string GetCropUrl( + this string imageUrl, + IImageUrlGenerator imageUrlGenerator, + ImageCropperValue cropDataSet, + int? width = null, + int? height = null, + string cropAlias = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = false, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + string animationProcessMode = null) + { + if (string.IsNullOrEmpty(imageUrl)) return string.Empty; + + ImageUrlGenerationOptions options; + + if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) + { + var crop = cropDataSet.GetCrop(cropAlias); + + // if a crop was specified, but not found, return null + if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) + return null; + + options = cropDataSet.GetCropBaseOptions(imageUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); + + if (crop != null & useCropDimensions) + { + width = crop.Width; + height = crop.Height; + } + + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) + { + options.HeightRatio = (decimal)crop.Height / crop.Width; + } + + // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width + if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) + { + options.WidthRatio = (decimal)crop.Width / crop.Height; + } + } + else + { + options = new ImageUrlGenerationOptions (imageUrl) + { + ImageCropMode = (imageCropMode ?? ImageCropMode.Pad).ToString().ToLowerInvariant(), + ImageCropAnchor = imageCropAnchor?.ToString().ToLowerInvariant() + }; + } + + options.Quality = quality; + options.Width = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Width ? null : width; + options.Height = ratioMode != null && ratioMode.Value == ImageCropRatioMode.Height ? null : height; + options.AnimationProcessMode = animationProcessMode; + + if (ratioMode == ImageCropRatioMode.Width && height != null) + { + // if only height specified then assume a square + if (width == null) width = height; + options.WidthRatio = (decimal)width / (decimal)height; + } + + if (ratioMode == ImageCropRatioMode.Height && width != null) + { + // if only width specified then assume a square + if (height == null) height = width; + options.HeightRatio = (decimal)height / (decimal)width; + } + + options.UpScale = upScale; + options.FurtherOptions = furtherOptions; + options.CacheBusterValue = cacheBusterValue; + + return imageUrlGenerator.GetImageUrl(options); + } + } +} diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index 656f1e05a2..f3833ba42a 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -29,10 +29,7 @@ namespace Umbraco.Web /// /// The ImageProcessor.Web Url. /// - public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) - { - return mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true); - } + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); /// /// Gets the ImageProcessor Url by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. @@ -49,10 +46,7 @@ namespace Umbraco.Web /// /// The ImageProcessor.Web Url. /// - public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) - { - return mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true); - } + public static string GetCropUrl(this IPublishedContent mediaItem, string propertyAlias, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, propertyAlias, cropAlias, Current.ImageUrlGenerator); /// /// Gets the ImageProcessor Url from the IPublishedContent item. @@ -121,44 +115,7 @@ namespace Umbraco.Web bool cacheBuster = true, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (mediaItem == null) throw new ArgumentNullException("mediaItem"); - - var cacheBusterValue = cacheBuster ? mediaItem.UpdateDate.ToFileTimeUtc().ToString(CultureInfo.InvariantCulture) : null; - - if (mediaItem.HasProperty(propertyAlias) == false || mediaItem.HasValue(propertyAlias) == false) - return string.Empty; - - var mediaItemUrl = mediaItem.MediaUrl(propertyAlias: propertyAlias); - - //get the default obj from the value converter - var cropperValue = mediaItem.Value(propertyAlias); - - //is it strongly typed? - var stronglyTyped = cropperValue as ImageCropperValue; - if (stronglyTyped != null) - { - return GetCropUrl( - mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //this shouldn't be the case but we'll check - var jobj = cropperValue as JObject; - if (jobj != null) - { - stronglyTyped = jobj.ToObject(); - return GetCropUrl( - mediaItemUrl, stronglyTyped, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } - - //it's a single string - return GetCropUrl( - mediaItemUrl, width, height, mediaItemUrl, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, - cacheBusterValue, furtherOptions, ratioMode, upScale); - } + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); /// /// Gets the ImageProcessor Url from the image path. @@ -227,19 +184,7 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (string.IsNullOrEmpty(imageUrl)) return string.Empty; - - ImageCropperValue cropDataSet = null; - if (string.IsNullOrEmpty(imageCropperValue) == false && imageCropperValue.DetectIsJson() && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - cropDataSet = imageCropperValue.DeserializeImageCropperValue(); - } - return GetCropUrl( - imageUrl, cropDataSet, width, height, cropAlias, quality, imageCropMode, - imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); - } + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, width, height, imageCropperValue, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); /// /// Gets the ImageProcessor Url from the image path. @@ -306,129 +251,8 @@ namespace Umbraco.Web string cacheBusterValue = null, string furtherOptions = null, ImageCropRatioMode? ratioMode = null, - bool upScale = true) - { - if (string.IsNullOrEmpty(imageUrl) == false) - { - var imageProcessorUrl = new StringBuilder(); + bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(imageUrl, Current.ImageUrlGenerator, cropDataSet, width, height, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, upScale); - if (cropDataSet != null && (imageCropMode == ImageCropMode.Crop || imageCropMode == null)) - { - var crop = cropDataSet.GetCrop(cropAlias); - - // if a crop was specified, but not found, return null - if (crop == null && !string.IsNullOrWhiteSpace(cropAlias)) - return null; - - imageProcessorUrl.Append(imageUrl); - cropDataSet.AppendCropBaseUrl(imageProcessorUrl, crop, string.IsNullOrWhiteSpace(cropAlias), preferFocalPoint); - - if (crop != null & useCropDimensions) - { - width = crop.Width; - height = crop.Height; - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a width parameter has been passed we can get the crop ratio for the height - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width != null && height == null) - { - var heightRatio = (decimal)crop.Height / (decimal)crop.Width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - // If a predefined crop has been specified & there are no coordinates & no ratio mode, but a height parameter has been passed we can get the crop ratio for the width - if (crop != null && string.IsNullOrEmpty(cropAlias) == false && crop.Coordinates == null && ratioMode == null && width == null && height != null) - { - var widthRatio = (decimal)crop.Width / (decimal)crop.Height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } - } - else - { - imageProcessorUrl.Append(imageUrl); - - if (imageCropMode == null) - { - imageCropMode = ImageCropMode.Pad; - } - - imageProcessorUrl.Append("?mode=" + imageCropMode.ToString().ToLower()); - - if (imageCropAnchor != null) - { - imageProcessorUrl.Append("&anchor=" + imageCropAnchor.ToString().ToLower()); - } - } - - var hasFormat = furtherOptions != null && furtherOptions.InvariantContains("&format="); - - //Only put quality here, if we don't have a format specified. - //Otherwise we need to put quality at the end to avoid it being overridden by the format. - if (quality != null && hasFormat == false) - { - imageProcessorUrl.Append("&quality=" + quality); - } - - if (width != null && ratioMode != ImageCropRatioMode.Width) - { - imageProcessorUrl.Append("&width=" + width); - } - - if (height != null && ratioMode != ImageCropRatioMode.Height) - { - imageProcessorUrl.Append("&height=" + height); - } - - if (ratioMode == ImageCropRatioMode.Width && height != null) - { - // if only height specified then assume a square - if (width == null) - { - width = height; - } - - var widthRatio = (decimal)width / (decimal)height; - imageProcessorUrl.Append("&widthratio=" + widthRatio.ToString(CultureInfo.InvariantCulture)); - } - - if (ratioMode == ImageCropRatioMode.Height && width != null) - { - // if only width specified then assume a square - if (height == null) - { - height = width; - } - - var heightRatio = (decimal)height / (decimal)width; - imageProcessorUrl.Append("&heightratio=" + heightRatio.ToString(CultureInfo.InvariantCulture)); - } - - if (upScale == false) - { - imageProcessorUrl.Append("&upscale=false"); - } - - if (furtherOptions != null) - { - imageProcessorUrl.Append(furtherOptions); - } - - //If furtherOptions contains a format, we need to put the quality after the format. - if (quality != null && hasFormat) - { - imageProcessorUrl.Append("&quality=" + quality); - } - - if (cacheBusterValue != null) - { - imageProcessorUrl.Append("&rnd=").Append(cacheBusterValue); - } - - return imageProcessorUrl.ToString(); - } - - return string.Empty; - } internal static ImageCropperValue DeserializeImageCropperValue(this string json) { diff --git a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index b5fdea32b7..17191d2521 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -13,7 +13,7 @@ using Umbraco.Web.Security; namespace Umbraco.Web.Install.InstallSteps { [InstallSetupStep(InstallationType.NewInstall | InstallationType.Upgrade, - "UmbracoVersion", 50, "Installation is complete!, get ready to be redirected to your new CMS.", + "UmbracoVersion", 50, "Installation is complete! Get ready to be redirected to your new CMS.", PerformsAppRestart = true)] internal class SetUmbracoVersionStep : InstallSetupStep { diff --git a/src/Umbraco.Web/JavaScript/AssetInitialization.cs b/src/Umbraco.Web/JavaScript/AssetInitialization.cs index 00605ece1f..42c750ffd3 100644 --- a/src/Umbraco.Web/JavaScript/AssetInitialization.cs +++ b/src/Umbraco.Web/JavaScript/AssetInitialization.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Web; using ClientDependency.Core; using ClientDependency.Core.Config; +using Umbraco.Core; using Umbraco.Web.Composing; using Umbraco.Web.PropertyEditors; @@ -35,7 +36,7 @@ namespace Umbraco.Web.JavaScript var requestUrl = httpContext.Request.Url; if (requestUrl == null) throw new ArgumentException("HttpContext.Request.Url is null.", nameof(httpContext)); - var dependencies = assets.Select(x => + var dependencies = assets.Where(x => x.IsNullOrWhiteSpace() == false).Select(x => { // most declarations with be made relative to the /umbraco folder, so things // like lib/blah/blah.js so we need to turn them into absolutes here diff --git a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs index 41f0e2fb65..3bbc4a793b 100644 --- a/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs +++ b/src/Umbraco.Web/Macros/PublishedContentHashtableConverter.cs @@ -248,8 +248,10 @@ namespace Umbraco.Web.Macros public string UrlSegment => throw new NotImplementedException(); + [Obsolete("Use WriterName(IUserService) extension instead")] public string WriterName { get; } + [Obsolete("Use CreatorName(IUserService) extension instead")] public string CreatorName { get; } public int WriterId => _inner.WriterId; diff --git a/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs b/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs index 86bd12c15e..1dcae62acd 100644 --- a/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs +++ b/src/Umbraco.Web/Media/Exif/ExifBitConverter.cs @@ -12,7 +12,7 @@ namespace Umbraco.Web.Media.Exif public ExifBitConverter(ByteOrder from, ByteOrder to) : base(from, to) { - ; + } #endregion diff --git a/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs b/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs index 63c1ce3365..8889a13e1d 100644 --- a/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs +++ b/src/Umbraco.Web/Media/Exif/ExifExtendedProperty.cs @@ -28,7 +28,7 @@ namespace Umbraco.Web.Media.Exif public ExifEnumProperty(ExifTag tag, T value) : this(tag, value, false) { - ; + } public override ExifInterOperability Interoperability @@ -210,13 +210,13 @@ namespace Umbraco.Web.Media.Exif public ExifPointSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifPointSubjectArea(ExifTag tag, ushort x, ushort y) - : base(tag, new ushort[] { x, y }) + : base(tag, new ushort[] {x, y}) { - ; + } } @@ -239,13 +239,13 @@ namespace Umbraco.Web.Media.Exif public ExifCircularSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifCircularSubjectArea(ExifTag tag, ushort x, ushort y, ushort d) : base(tag, new ushort[] { x, y, d }) { - ; + } } @@ -269,13 +269,13 @@ namespace Umbraco.Web.Media.Exif public ExifRectangularSubjectArea(ExifTag tag, ushort[] value) : base(tag, value) { - ; + } public ExifRectangularSubjectArea(ExifTag tag, ushort x, ushort y, ushort w, ushort h) : base(tag, new ushort[] { x, y, w, h }) { - ; + } } @@ -303,13 +303,13 @@ namespace Umbraco.Web.Media.Exif public GPSLatitudeLongitude(ExifTag tag, MathEx.UFraction32[] value) : base(tag, value) { - ; + } public GPSLatitudeLongitude(ExifTag tag, float d, float m, float s) : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(d), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) { - ; + } } @@ -331,13 +331,13 @@ namespace Umbraco.Web.Media.Exif public GPSTimeStamp(ExifTag tag, MathEx.UFraction32[] value) : base(tag, value) { - ; + } public GPSTimeStamp(ExifTag tag, float h, float m, float s) : base(tag, new MathEx.UFraction32[] { new MathEx.UFraction32(h), new MathEx.UFraction32(m), new MathEx.UFraction32(s) }) { - ; + } } diff --git a/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs b/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs index 94b255f4d1..24b3ac74be 100644 --- a/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs +++ b/src/Umbraco.Web/Media/Exif/JFIFExtendedProperty.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.Media.Exif public JFIFVersion(ExifTag tag, ushort value) : base(tag, value) { - ; + } public override string ToString() diff --git a/src/Umbraco.Web/Media/Exif/JPEGSection.cs b/src/Umbraco.Web/Media/Exif/JPEGSection.cs index a1bc420fe4..78565d2bfa 100644 --- a/src/Umbraco.Web/Media/Exif/JPEGSection.cs +++ b/src/Umbraco.Web/Media/Exif/JPEGSection.cs @@ -45,7 +45,7 @@ public JPEGSection(JPEGMarker marker) : this(marker, new byte[0], new byte[0]) { - ; + } #endregion diff --git a/src/Umbraco.Web/Media/Exif/MathEx.cs b/src/Umbraco.Web/Media/Exif/MathEx.cs index 735358c40a..94cbccfbda 100644 --- a/src/Umbraco.Web/Media/Exif/MathEx.cs +++ b/src/Umbraco.Web/Media/Exif/MathEx.cs @@ -403,37 +403,37 @@ namespace Umbraco.Web.Media.Exif public Fraction32(int numerator, int denominator) : this(numerator, denominator, 0) { - ; + } public Fraction32(int numerator) : this(numerator, (int)1) { - ; + } public Fraction32(Fraction32 f) : this(f.Numerator, f.Denominator, f.Error) { - ; + } public Fraction32(float value) : this((double)value) { - ; + } public Fraction32(double value) : this(FromDouble(value)) { - ; + } public Fraction32(string s) : this(FromString(s)) { - ; + } #endregion @@ -1033,37 +1033,37 @@ namespace Umbraco.Web.Media.Exif public UFraction32(uint numerator, uint denominator) : this(numerator, denominator, 0) { - ; + } public UFraction32(uint numerator) : this(numerator, (uint)1) { - ; + } public UFraction32(UFraction32 f) : this(f.Numerator, f.Denominator, f.Error) { - ; + } public UFraction32(float value) : this((double)value) { - ; + } public UFraction32(double value) : this(FromDouble(value)) { - ; + } public UFraction32(string s) : this(FromString(s)) { - ; + } #endregion diff --git a/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs new file mode 100644 index 0000000000..e471e9aa4a --- /dev/null +++ b/src/Umbraco.Web/Models/ImageProcessorImageUrlGenerator.cs @@ -0,0 +1,64 @@ +using System.Globalization; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Models; + +namespace Umbraco.Web.Models +{ + internal class ImageProcessorImageUrlGenerator : IImageUrlGenerator + { + public string GetImageUrl(ImageUrlGenerationOptions options) + { + if (options == null) return null; + + var imageProcessorUrl = new StringBuilder(options.ImageUrl ?? string.Empty); + + if (options.FocalPoint != null) AppendFocalPoint(imageProcessorUrl, options); + else if (options.Crop != null) AppendCrop(imageProcessorUrl, options); + else if (options.DefaultCrop) imageProcessorUrl.Append("?anchor=center&mode=crop"); + else + { + imageProcessorUrl.Append("?mode=").Append((options.ImageCropMode ?? "crop").ToLower()); + + if (options.ImageCropAnchor != null) imageProcessorUrl.Append("&anchor=").Append(options.ImageCropAnchor.ToLower()); + } + + var hasFormat = options.FurtherOptions != null && options.FurtherOptions.InvariantContains("&format="); + + //Only put quality here, if we don't have a format specified. + //Otherwise we need to put quality at the end to avoid it being overridden by the format. + if (options.Quality != null && hasFormat == false) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (options.HeightRatio != null) imageProcessorUrl.Append("&heightratio=").Append(options.HeightRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.WidthRatio != null) imageProcessorUrl.Append("&widthratio=").Append(options.WidthRatio.Value.ToString(CultureInfo.InvariantCulture)); + if (options.Width != null) imageProcessorUrl.Append("&width=").Append(options.Width); + if (options.Height != null) imageProcessorUrl.Append("&height=").Append(options.Height); + if (options.UpScale == false) imageProcessorUrl.Append("&upscale=false"); + if (options.AnimationProcessMode != null) imageProcessorUrl.Append("&animationprocessmode=").Append(options.AnimationProcessMode); + if (options.FurtherOptions != null) imageProcessorUrl.Append(options.FurtherOptions); + + //If furtherOptions contains a format, we need to put the quality after the format. + if (options.Quality != null && hasFormat) imageProcessorUrl.Append("&quality=").Append(options.Quality); + if (options.CacheBusterValue != null) imageProcessorUrl.Append("&rnd=").Append(options.CacheBusterValue); + + return imageProcessorUrl.ToString(); + } + + private void AppendFocalPoint(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options) + { + imageProcessorUrl.Append("?center="); + imageProcessorUrl.Append(options.FocalPoint.Top.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.FocalPoint.Left.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&mode=crop"); + } + + private void AppendCrop(StringBuilder imageProcessorUrl, ImageUrlGenerationOptions options) + { + imageProcessorUrl.Append("?crop="); + imageProcessorUrl.Append(options.Crop.X1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y1.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.X2.ToString(CultureInfo.InvariantCulture)).Append(","); + imageProcessorUrl.Append(options.Crop.Y2.ToString(CultureInfo.InvariantCulture)); + imageProcessorUrl.Append("&cropmode=percentage"); + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index c811c236d4..c25c932b64 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -5,6 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Routing; @@ -27,6 +28,7 @@ namespace Umbraco.Web.Models.Mapping private readonly ILocalizationService _localizationService; private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IEntityService _entityService; private readonly TabsAndPropertiesMapper _tabsAndPropertiesMapper; private readonly ContentSavedStateMapper _stateMapper; private readonly ContentBasicSavedStateMapper _basicStateMapper; @@ -34,7 +36,7 @@ namespace Umbraco.Web.Models.Mapping public ContentMapDefinition(CommonMapper commonMapper, ILocalizedTextService localizedTextService, IContentService contentService, IContentTypeService contentTypeService, IFileService fileService, IUmbracoContextAccessor umbracoContextAccessor, IPublishedRouter publishedRouter, ILocalizationService localizationService, ILogger logger, - IUserService userService) + IUserService userService, IEntityService entityService) { _commonMapper = commonMapper; _localizedTextService = localizedTextService; @@ -46,7 +48,7 @@ namespace Umbraco.Web.Models.Mapping _localizationService = localizationService; _logger = logger; _userService = userService; - + _entityService = entityService; _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); @@ -80,7 +82,7 @@ namespace Umbraco.Web.Models.Mapping target.Icon = source.ContentType.Icon; target.Id = source.Id; target.IsBlueprint = source.Blueprint; - target.IsChildOfListView = DetermineIsChildOfListView(source); + target.IsChildOfListView = DetermineIsChildOfListView(source, context); target.IsContainer = source.ContentType.IsContainer; target.IsElement = source.ContentType.IsElement; target.Key = source.Key; @@ -211,13 +213,66 @@ namespace Umbraco.Web.Models.Mapping return source.CultureInfos.TryGetValue(culture, out var name) && !name.Name.IsNullOrWhiteSpace() ? name.Name : $"({source.Name})"; } - private bool DetermineIsChildOfListView(IContent source) + /// + /// Checks if the content item is a descendant of a list view + /// + /// + /// + /// + /// Returns true if the content item is a descendant of a list view and where the content is + /// not a current user's start node. + /// + /// + /// We must check if it's the current user's start node because in that case we will actually be + /// rendering the tree node underneath the list view to visually show context. In this case we return + /// false because the item is technically not being rendered as part of a list view but instead as a + /// real tree node. If we didn't perform this check then tree syncing wouldn't work correctly. + /// + private bool DetermineIsChildOfListView(IContent source, MapperContext context) { - // map the IsChildOfListView (this is actually if it is a descendant of a list view!) + var userStartNodes = Array.Empty(); + + // In cases where a user's start node is below a list view, we will actually render + // out the tree to that start node and in that case for that start node, we want to return + // false here. + if (context.HasItems && context.Items.TryGetValue("CurrentUser", out var usr) && usr is IUser currentUser) + { + userStartNodes = currentUser.CalculateContentStartNodeIds(_entityService); + if (!userStartNodes.Contains(Constants.System.Root)) + { + // return false if this is the user's actual start node, the node will be rendered in the tree + // regardless of if it's a list view or not + if (userStartNodes.Contains(source.Id)) + return false; + } + } + var parent = _contentService.GetParent(source); - return parent != null && (parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(parent.Path)); + + if (parent == null) + return false; + + var pathParts = parent.Path.Split(',').Select(x => int.TryParse(x, out var i) ? i : 0).ToList(); + + // reduce the path parts so we exclude top level content items that + // are higher up than a user's start nodes + foreach (var n in userStartNodes) + { + var index = pathParts.IndexOf(n); + if (index != -1) + { + // now trim all top level start nodes to the found index + for (var i = 0; i < index; i++) + { + pathParts.RemoveAt(0); + } + } + } + + return parent.ContentType.IsContainer || _contentTypeService.HasContainerInPath(pathParts.ToArray()); } + private DateTime? GetScheduledDate(IContent source, ContentScheduleAction action, MapperContext context) { var culture = context.GetCulture() ?? string.Empty; diff --git a/src/Umbraco.Web/Models/PartialViewMacroModel.cs b/src/Umbraco.Web/Models/PartialViewMacroModel.cs index 9964877cba..f3272644f4 100644 --- a/src/Umbraco.Web/Models/PartialViewMacroModel.cs +++ b/src/Umbraco.Web/Models/PartialViewMacroModel.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.Models /// /// The model used when rendering Partial View Macros /// - public class PartialViewMacroModel + public class PartialViewMacroModel : IContentModel { public PartialViewMacroModel(IPublishedContent page, diff --git a/src/Umbraco.Web/Models/PublishedContentBase.cs b/src/Umbraco.Web/Models/PublishedContentBase.cs index 148bab11c0..d6a57a66d7 100644 --- a/src/Umbraco.Web/Models/PublishedContentBase.cs +++ b/src/Umbraco.Web/Models/PublishedContentBase.cs @@ -54,6 +54,7 @@ namespace Umbraco.Web.Models public abstract int CreatorId { get; } /// + [Obsolete("Use CreatorName(IUserService) extension instead")] public abstract string CreatorName { get; } /// @@ -63,12 +64,14 @@ namespace Umbraco.Web.Models public abstract int WriterId { get; } /// + [Obsolete("Use WriterName(IUserService) extension instead")] public abstract string WriterName { get; } /// public abstract DateTime UpdateDate { get; } /// + [Obsolete("Use the Url() extension instead")] public virtual string Url => this.Url(); /// diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index 7134fe8703..28cc70f776 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -30,18 +31,31 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlImageSourceParser _imageSourceParser; private readonly RichTextEditorPastedImages _pastedImages; private readonly HtmlLocalLinkParser _localLinkParser; + private readonly IImageUrlGenerator _imageUrlGenerator; + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] public GridPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages, HtmlLocalLinkParser localLinkParser) + : this(logger, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, Current.ImageUrlGenerator) + { + } + + public GridPropertyEditor(ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; _localLinkParser = localLinkParser; + _imageUrlGenerator = imageUrlGenerator; } public override IPropertyIndexValueFactory PropertyIndexValueFactory => new GridPropertyIndexValueFactory(); @@ -50,7 +64,7 @@ namespace Umbraco.Web.PropertyEditors /// Overridden to ensure that the value is validated /// /// - protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser); + protected override IDataValueEditor CreateValueEditor() => new GridPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _pastedImages, _localLinkParser, _imageUrlGenerator); protected override IConfigurationEditor CreateConfigurationEditor() => new GridConfigurationEditor(); @@ -61,19 +75,32 @@ namespace Umbraco.Web.PropertyEditors private readonly RichTextEditorPastedImages _pastedImages; private readonly RichTextPropertyEditor.RichTextPropertyValueEditor _richTextPropertyValueEditor; private readonly MediaPickerPropertyEditor.MediaPickerPropertyValueEditor _mediaPickerPropertyValueEditor; + private readonly IImageUrlGenerator _imageUrlGenerator; + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] public GridPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, RichTextEditorPastedImages pastedImages, HtmlLocalLinkParser localLinkParser) + : this(attribute, umbracoContextAccessor, imageSourceParser, pastedImages, localLinkParser, Current.ImageUrlGenerator) + { + } + + public GridPropertyValueEditor(DataEditorAttribute attribute, + IUmbracoContextAccessor umbracoContextAccessor, + HtmlImageSourceParser imageSourceParser, + RichTextEditorPastedImages pastedImages, + HtmlLocalLinkParser localLinkParser, + IImageUrlGenerator imageUrlGenerator) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _pastedImages = pastedImages; - _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages); + _richTextPropertyValueEditor = new RichTextPropertyEditor.RichTextPropertyValueEditor(attribute, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, _imageUrlGenerator); _mediaPickerPropertyValueEditor = new MediaPickerPropertyEditor.MediaPickerPropertyValueEditor(attribute); + _imageUrlGenerator = imageUrlGenerator; } /// @@ -108,7 +135,7 @@ namespace Umbraco.Web.PropertyEditors // Parse the HTML var html = rte.Value?.ToString(); - var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId); + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(html, mediaParentId, userId, _imageUrlGenerator); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); rte.Value = editorValueWithMediaUrlsRemoved; diff --git a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs index 0b2a607f8b..67ff82a3e1 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId) + internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator) { // Find all img's that has data-tmpimg attribute // Use HTML Agility Pack - https://html-agility-pack.net @@ -101,7 +101,7 @@ namespace Umbraco.Web.PropertyEditors if (mediaTyped == null) throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available."); - var location = mediaTyped.Url; + var location = mediaTyped.Url(); // Find the width & height attributes as we need to set the imageprocessor QueryString var width = img.GetAttributeValue("width", int.MinValue); @@ -109,7 +109,7 @@ namespace Umbraco.Web.PropertyEditors if (width != int.MinValue && height != int.MinValue) { - location = $"{location}?width={width}&height={height}&mode=max"; + location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location) { ImageCropMode = "max", Width = width, Height = height }); } img.SetAttributeValue("src", location); diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 427e36b37a..42777f11ad 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -30,25 +31,36 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; + private readonly IImageUrlGenerator _imageUrlGenerator; /// /// The constructor will setup the property editor based on the attribute if one is found /// + [Obsolete("Use the constructor which takes an IImageUrlGenerator")] public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) + : this(logger, umbracoContextAccessor, imageSourceParser, localLinkParser, pastedImages, Current.ImageUrlGenerator) + { + } + + /// + /// The constructor will setup the property editor based on the attribute if one is found + /// + public RichTextPropertyEditor(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) : base(logger) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; _pastedImages = pastedImages; + _imageUrlGenerator = imageUrlGenerator; } /// /// Create a custom value editor /// /// - protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages); + protected override IDataValueEditor CreateValueEditor() => new RichTextPropertyValueEditor(Attribute, _umbracoContextAccessor, _imageSourceParser, _localLinkParser, _pastedImages, _imageUrlGenerator); protected override IConfigurationEditor CreateConfigurationEditor() => new RichTextConfigurationEditor(); @@ -63,14 +75,16 @@ namespace Umbraco.Web.PropertyEditors private readonly HtmlImageSourceParser _imageSourceParser; private readonly HtmlLocalLinkParser _localLinkParser; private readonly RichTextEditorPastedImages _pastedImages; + private readonly IImageUrlGenerator _imageUrlGenerator; - public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages) + public RichTextPropertyValueEditor(DataEditorAttribute attribute, IUmbracoContextAccessor umbracoContextAccessor, HtmlImageSourceParser imageSourceParser, HtmlLocalLinkParser localLinkParser, RichTextEditorPastedImages pastedImages, IImageUrlGenerator imageUrlGenerator) : base(attribute) { _umbracoContextAccessor = umbracoContextAccessor; _imageSourceParser = imageSourceParser; _localLinkParser = localLinkParser; _pastedImages = pastedImages; + _imageUrlGenerator = imageUrlGenerator; } /// @@ -124,7 +138,7 @@ namespace Umbraco.Web.PropertyEditors var mediaParent = config?.MediaParentId; var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid; - var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId); + var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator); var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs index c62a79d283..5c027ed9b6 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MarkdownEditorValueConverter.cs @@ -15,6 +15,12 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters private readonly HtmlLocalLinkParser _localLinkParser; private readonly HtmlUrlParser _urlParser; + [Obsolete("Use ctor defining all dependencies instead")] + public MarkdownEditorValueConverter() + : this(Current.Factory.GetInstance(), Current.Factory.GetInstance()) + { + } + public MarkdownEditorValueConverter(HtmlLocalLinkParser localLinkParser, HtmlUrlParser urlParser) { _localLinkParser = localLinkParser; @@ -25,7 +31,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters => Constants.PropertyEditors.Aliases.MarkdownEditor == propertyType.EditorAlias; public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IHtmlString); + => typeof(IHtmlString); public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs index 3c3c3ac54f..d80affbfa9 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs @@ -163,6 +163,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override int CreatorId => _contentNode.CreatorId; /// + [Obsolete("Use CreatorName(IUserService) extension instead")] public override string CreatorName => GetProfileNameById(_contentNode.CreatorId); /// @@ -172,6 +173,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public override int WriterId => ContentData.WriterId; /// + [Obsolete("Use WriterName(IUserService) extension instead")] public override string WriterName => GetProfileNameById(ContentData.WriterId); /// diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 833b0a3665..a33d9ee427 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1290,7 +1290,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var member = args.Entity; // refresh the edited data - OnRepositoryRefreshed(db, member, true); + OnRepositoryRefreshed(db, member, false); } private void OnRepositoryRefreshed(IUmbracoDatabase db, IContentBase content, bool published) diff --git a/src/Umbraco.Web/PublishedCache/PublishedMember.cs b/src/Umbraco.Web/PublishedCache/PublishedMember.cs index 6e9ec61c62..e7cdb65f2e 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedMember.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedMember.cs @@ -140,9 +140,11 @@ namespace Umbraco.Web.PublishedCache public override string UrlSegment => throw new NotSupportedException(); // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current + [Obsolete("Use WriterName(IUserService) extension instead")] public override string WriterName => _member.GetCreatorProfile().Name; // TODO: ARGH! need to fix this - this is not good because it uses ApplicationContext.Current + [Obsolete("Use CreatorName(IUserService) extension instead")] public override string CreatorName => _member.GetCreatorProfile().Name; public override int WriterId => _member.CreatorId; diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index 061422859c..8d85c4615a 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -28,6 +28,20 @@ namespace Umbraco.Web private static UmbracoContext UmbracoContext => Current.UmbracoContext; private static ISiteDomainHelper SiteDomainHelper => Current.Factory.GetInstance(); + #region Creator/Writer Names + + public static string CreatorName(this IPublishedContent content, IUserService userService) + { + return userService.GetProfileById(content.CreatorId)?.Name; + } + + public static string WriterName(this IPublishedContent content, IUserService userService) + { + return userService.GetProfileById(content.WriterId)?.Name; + } + + #endregion + #region IsComposedOf /// diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 79b24175ea..59c394182a 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -38,6 +38,8 @@ using Umbraco.Web.Trees; using Umbraco.Web.WebApi; using Current = Umbraco.Web.Composing.Current; using Umbraco.Web.PropertyEditors; +using Umbraco.Core.Models; +using Umbraco.Web.Models; namespace Umbraco.Web.Runtime { @@ -191,6 +193,8 @@ namespace Umbraco.Web.Runtime composition.MediaUrlProviders() .Append(); + composition.RegisterUnique(); + composition.RegisterUnique(); composition.ContentFinders() diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index f796985d39..f08abfaf12 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -52,6 +52,6 @@ namespace Umbraco.Web.Templates [Obsolete("Use " + nameof(HtmlImageSourceParser) + "." + nameof(RichTextEditorPastedImages.FindAndPersistPastedTempImages) + " instead")] internal static string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, ILogger logger) - => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId); + => Current.Factory.GetInstance().FindAndPersistPastedTempImages(html, mediaParentFolder, userId, Current.Factory.GetInstance()); } } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 1069df0ec4..6d156e3fc8 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -364,7 +364,12 @@ namespace Umbraco.Web.Trees var startNodes = Services.EntityService.GetAll(UmbracoObjectType, UserStartNodes); //if any of these start nodes' parent is current, then we need to render children normally so we need to switch some logic and tell // the UI that this node does have children and that it isn't a container - if (startNodes.Any(x => x.ParentId == e.Id)) + + if (startNodes.Any(x => + { + var pathParts = x.Path.Split(','); + return pathParts.Contains(e.Id.ToInvariantString()); + })) { renderChildren = true; } diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index e8f8fb9f75..ece16229b2 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -69,15 +69,18 @@ namespace Umbraco.Web.Trees .OrderBy(entity => entity.Name) .Select(dt => { + // get the content type here so we can get the icon from it to use when we create the tree node + // and we can enrich the result with content type data that's not available in the entity service output + var contentType = contentTypes[dt.Id]; + // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, Constants.Icons.ContentType, hasChildren); + var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, contentType?.Icon ?? Constants.Icons.ContentType, hasChildren); node.Path = dt.Path; - // enrich the result with content type data that's not available in the entity service output - var contentType = contentTypes[dt.Id]; + // now we can enrich the result with content type data that's not available in the entity service output node.Alias = contentType.Alias; node.AdditionalData["isElement"] = contentType.IsElement; diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 3d0046c319..7a9a80c8fc 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -25,10 +25,12 @@ namespace Umbraco.Web.Trees public class MediaTypeTreeController : TreeController, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher; + private readonly IMediaTypeService _mediaTypeService; - public MediaTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + public MediaTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, IMediaTypeService mediaTypeService) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _treeSearcher = treeSearcher; + _mediaTypeService = mediaTypeService; } protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) @@ -54,6 +56,8 @@ namespace Umbraco.Web.Trees // if the request is for folders only then just return if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + var mediaTypes = _mediaTypeService.GetAll(); + nodes.AddRange( Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.MediaType) .OrderBy(entity => entity.Name) @@ -62,7 +66,8 @@ namespace Umbraco.Web.Trees // since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to. // need this check to keep supporting sites where children have already been created. var hasChildren = dt.HasChildren; - var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, Constants.Icons.MediaType, hasChildren); + var mt = mediaTypes.FirstOrDefault(x => x.Id == dt.Id); + var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaType, id, queryStrings, mt?.Icon ?? Constants.Icons.MediaType, hasChildren); node.Path = dt.Path; return node; diff --git a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs index 2046baf2d3..5db9088f20 100644 --- a/src/Umbraco.Web/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTypeTreeController.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.Trees { return Services.MemberTypeService.GetAll() .OrderBy(x => x.Name) - .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, Constants.Icons.MemberType, false)); + .Select(dt => CreateTreeNode(dt, Constants.ObjectTypes.MemberType, id, queryStrings, dt?.Icon ?? Constants.Icons.MemberType, false)); } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8f53713cff..4c1a9ba9aa 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -150,11 +150,13 @@ + + @@ -223,6 +225,7 @@ +