diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f4e237a1f260..3432ac472aa7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -16,6 +16,7 @@ This project and everyone participating in it, is governed by the [our Code of C [Contributing code changes](#contributing-code-changes) * [Guidelines for contributions we welcome](#guidelines-for-contributions-we-welcome) + * [Ownership and copyright](#ownership-and-copyright) * [What can I start with?](#what-can-i-start-with) * [How do I begin?](#how-do-i-begin) * [Pull requests](#pull-requests) @@ -44,6 +45,17 @@ We have [documented what we consider small and large changes](CONTRIBUTION_GUIDE Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. +#### Ownership and copyright + +It is your responsibility to make sure that you're allowed to share the code you're providing us. +For example, you should have permission from your employer or customer to share code. + +Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS. + +If you're not sure, leave a note on your contribution and we will be happy to guide you. + +When your contribution has been accepted, it will be [MIT licensed](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/LICENSE.md) from that time onwards. + ### What can I start with? Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` issues](https://github.com/umbraco/Umbraco-CMS/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs+) diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml new file mode 100644 index 000000000000..04d1a0e04c31 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -0,0 +1,52 @@ +--- +name: 🐛 Bug Report +description: "File a bug report, if you've discovered a problem in Umbraco." +labels: "type/bug" +body: +- type: input + id: "version" + attributes: + label: "Which Umbraco version are you using?" + description: "Use the help icon in the Umbraco backoffice to find the version you're using" + validations: + required: true +- type: textarea + id: "summary" + attributes: + label: "Bug summary" + description: "Write a short summary of the bug." + placeholder: > + Try to pinpoint it as much as possible. + + Try to state the actual problem, and not just what you think the solution might be. + validations: + required: true +- type: textarea + attributes: + label: "Specifics" + id: "specifics" + description: "Remember that you can format code and logs nicely with the `<>` button" + placeholder: > + Mention the URL where this bug occurs, if applicable + + Please mention if you've checked it in other browsers as well + + Please include full error messages and screenshots, gifs or mp4 videos if applicable +- type: textarea + attributes: + label: "Steps to reproduce" + id: "reproduction" + description: "How can we reproduce the problem on a clean Umbraco install?" + placeholder: > + Please include screenshots, gifs or mp4 videos if applicable + validations: + required: true +- type: textarea + attributes: + label: "Expected result / actual result" + id: "result" + description: "What did you expect that would happen on your Umbraco site and what is the actual result of the above steps?" + placeholder: > + Describe the intended/desired outcome after you did the steps mentioned. + + Describe the behaviour of the bug diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.yml b/.github/ISSUE_TEMPLATE/02_feature_request.yml new file mode 100644 index 000000000000..5d53b2f12e5c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_feature_request.yml @@ -0,0 +1,33 @@ +--- +name: 📮 Feature Request +description: Open a feature request, if you want to propose a new feature. +labels: type/feature +body: +- type: dropdown + id: version + attributes: + label: Umbraco version + description: Which major Umbraco version are you proposing a feature for? + options: + - v8 + - v9 + validations: + required: true +- type: textarea + id: summary + attributes: + label: Description + description: Write a brief desciption of your proposed new feature. + validations: + required: true +- type: textarea + attributes: + label: How can you help? + id: help + description: Umbraco''s core team has limited available time, but maybe you can help? + placeholder: > + If we can not work on your suggestion, please don't take it personally. Most likely, it's either: + + - We think your idea is valid, but we can't find the time to work on it. + + - Your idea might be better suited as a package, if it's not suitable for the majority of users. diff --git a/.github/ISSUE_TEMPLATE/1_Bug.md b/.github/ISSUE_TEMPLATE/1_Bug.md deleted file mode 100644 index d388af0d3952..000000000000 --- a/.github/ISSUE_TEMPLATE/1_Bug.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -name: 🐛 Bug Report -about: File a bug report, if you've discovered a problem in Umbraco. ---- - -A brief description of the issue goes here. - - - -## Umbraco version - -I am seeing this issue on Umbraco version: - - -Reproduction ------------- - -If you're filing a bug, please describe how to reproduce it. Include as much -relevant information as possible, such as: - -### Bug summary - - - -### Specifics - - - -### Steps to reproduce - - - -### Expected result - - - -### Actual result - - diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md deleted file mode 100644 index 16ec2568dd7b..000000000000 --- a/.github/ISSUE_TEMPLATE/2_Feature_request.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: 📮 Feature Request -about: Open a feature request, if you want to propose a new feature. ---- - -A brief description of your feature request goes here. - - - - -How can you help? -------------------------------- - - diff --git a/.github/ISSUE_TEMPLATE/3_BugNetCore.md b/.github/ISSUE_TEMPLATE/3_BugNetCore.md deleted file mode 100644 index 989904d4d8a8..000000000000 --- a/.github/ISSUE_TEMPLATE/3_BugNetCore.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -name: 🌟 .Net Core Bug Report -about: For bugs specifically for the upcoming .NET Core release of Umbraco, don't use this if you're working with Umbraco version 7 or 8 -labels: project/net-core ---- - -ℹ️ If this bug **also** appears on the current version 8 of Umbraco then please [report it as a regular bug](https://github.com/umbraco/Umbraco-CMS/issues/new?template=1_Bug.md), fixes in version 8 will be merged to the .NET Core version. - -A brief description of the issue goes here. - - - - -Reproduction ------------- - -If you're filing a bug, please describe how to reproduce it. Include as much -relevant information as possible, such as: - -### Bug summary - - - -### Specifics - - - -### Steps to reproduce - - - -### Expected result - - - -### Actual result - - diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 37d1be915827..d5418ad27030 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,4 +1,4 @@ -blank_issues_enabled: true +blank_issues_enabled: false contact_links: - name: ⁉️ Support Question url: https://our.umbraco.com @@ -8,4 +8,4 @@ contact_links: about: Documentation issues should be reported on the Umbraco documentation repository. - name: 🔐 Security Issue url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/ - about: Discovered a Security Issue in Umbraco? \ No newline at end of file + about: Discovered a Security Issue in Umbraco? diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index d8815bab6336..b0758e6ae49c 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -28,7 +28,7 @@ - + diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 2a7386cb4528..91918498f9c8 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.13.0")] -[assembly: AssemblyInformationalVersion("8.13.0-rc")] +[assembly: AssemblyFileVersion("8.14.0")] +[assembly: AssemblyInformationalVersion("8.14.0-rc")] diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index 0e9a9a386295..642bef6d0c72 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -17,5 +17,8 @@ public static class CacheKeys public const string UserAllMediaStartNodesPrefix = "AllMediaStartNodes"; public const string UserMediaStartNodePathsPrefix = "MediaStartNodePaths"; public const string UserContentStartNodePathsPrefix = "ContentStartNodePaths"; + + public const string ContentRecycleBinCacheKey = "recycleBin_content"; + public const string MediaRecycleBinCacheKey = "recycleBin_media"; } } diff --git a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs index c11309c82733..94756ce9750f 100644 --- a/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs +++ b/src/Umbraco.Core/Cache/DefaultRepositoryCachePolicy.cs @@ -20,7 +20,7 @@ namespace Umbraco.Core.Cache internal class DefaultRepositoryCachePolicy : RepositoryCachePolicyBase where TEntity : class, IEntity { - private static readonly TEntity[] EmptyEntities = new TEntity[0]; // const + private static readonly TEntity[] s_emptyEntities = new TEntity[0]; // const private readonly RepositoryCachePolicyOptions _options; public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeAccessor, RepositoryCachePolicyOptions options) @@ -29,17 +29,25 @@ public DefaultRepositoryCachePolicy(IAppPolicyCache cache, IScopeAccessor scopeA _options = options ?? throw new ArgumentNullException(nameof(options)); } - protected string GetEntityCacheKey(object id) - { - if (id == null) throw new ArgumentNullException(nameof(id)); - return GetEntityTypeCacheKey() + id; - } + protected string GetEntityCacheKey(int id) => EntityTypeCacheKey + id; - protected string GetEntityTypeCacheKey() + protected string GetEntityCacheKey(TId id) { - return $"uRepo_{typeof (TEntity).Name}_"; + if (EqualityComparer.Default.Equals(id, default)) + { + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return EntityTypeCacheKey + id; + } + + return EntityTypeCacheKey + id.ToString().ToUpperInvariant(); } + protected string EntityTypeCacheKey { get; } = $"uRepo_{typeof(TEntity).Name}_"; + protected virtual void InsertEntity(string cacheKey, TEntity entity) { Cache.Insert(cacheKey, () => entity, TimeSpan.FromMinutes(5), true); @@ -52,7 +60,7 @@ protected virtual void InsertEntities(TId[] ids, TEntity[] entities) // getting all of them, and finding nothing. // if we can cache a zero count, cache an empty array, // for as long as the cache is not cleared (no expiration) - Cache.Insert(GetEntityTypeCacheKey(), () => EmptyEntities); + Cache.Insert(EntityTypeCacheKey, () => s_emptyEntities); } else { @@ -81,7 +89,7 @@ public override void Create(TEntity entity, Action persistNew) } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } catch { @@ -91,7 +99,7 @@ public override void Create(TEntity entity, Action persistNew) Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); throw; } @@ -113,7 +121,7 @@ public override void Update(TEntity entity, Action persistUpdated) } // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } catch { @@ -123,7 +131,7 @@ public override void Update(TEntity entity, Action persistUpdated) Cache.Clear(GetEntityCacheKey(entity.Id)); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); throw; } @@ -144,7 +152,7 @@ public override void Delete(TEntity entity, Action persistDeleted) var cacheKey = GetEntityCacheKey(entity.Id); Cache.Clear(cacheKey); // if there's a GetAllCacheAllowZeroCount cache, ensure it is cleared - Cache.Clear(GetEntityTypeCacheKey()); + Cache.Clear(EntityTypeCacheKey); } } @@ -195,7 +203,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe else { // get everything we have - var entities = Cache.GetCacheItemsByKeySearch(GetEntityTypeCacheKey()) + var entities = Cache.GetCacheItemsByKeySearch(EntityTypeCacheKey) .ToArray(); // no need for null checks, we are not caching nulls if (entities.Length > 0) @@ -218,7 +226,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe { // if none of them were in the cache // and we allow zero count - check for the special (empty) entry - var empty = Cache.GetCacheItem(GetEntityTypeCacheKey()); + var empty = Cache.GetCacheItem(EntityTypeCacheKey); if (empty != null) return empty; } } @@ -238,7 +246,7 @@ public override TEntity[] GetAll(TId[] ids, Func> pe /// public override void ClearAll() { - Cache.ClearByKey(GetEntityTypeCacheKey()); + Cache.ClearByKey(EntityTypeCacheKey); } } } diff --git a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs index 88eb61de7625..b333fa646d9b 100644 --- a/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs +++ b/src/Umbraco.Core/Composing/WeightedCollectionBuilderBase.cs @@ -16,6 +16,8 @@ public abstract class WeightedCollectionBuilderBase _customWeights = new Dictionary(); + /// /// Clears all types in the collection. /// @@ -107,6 +109,18 @@ public TBuilder Remove(Type type) return This; } + /// + /// Changes the default weight of an item + /// + /// The type of item + /// The new weight + /// + public TBuilder SetWeight(int weight) where T : TItem + { + _customWeights[typeof(T)] = weight; + return This; + } + protected override IEnumerable GetRegisteringTypes(IEnumerable types) { var list = types.ToList(); @@ -118,6 +132,8 @@ protected override IEnumerable GetRegisteringTypes(IEnumerable types protected virtual int GetWeight(Type type) { + if (_customWeights.ContainsKey(type)) + return _customWeights[type]; var attr = type.GetCustomAttributes(typeof(WeightAttribute), false).OfType().SingleOrDefault(); return attr?.Weight ?? DefaultWeight; } diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index c1d7103a1c6e..c8233c8d340f 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -118,6 +118,26 @@ public static class MediaTypes /// public const string Image = "Image"; + /// + /// MediaType alias for a video. + /// + public const string Video = "Video"; + + /// + /// MediaType alias for an audio. + /// + public const string Audio = "Audio"; + + /// + /// MediaType alias for an article. + /// + public const string Article = "Article"; + + /// + /// MediaType alias for vector graphics. + /// + public const string VectorGraphics = "VectorGraphics"; + /// /// MediaType alias indicating allowing auto-selection. /// diff --git a/src/Umbraco.Core/Constants-DataTypes.cs b/src/Umbraco.Core/Constants-DataTypes.cs index 673da8f9a360..f1af0ba99e6c 100644 --- a/src/Umbraco.Core/Constants-DataTypes.cs +++ b/src/Umbraco.Core/Constants-DataTypes.cs @@ -25,6 +25,10 @@ public static class DataTypes public const int DropDownSingle = -39; public const int DropDownMultiple = -42; public const int Upload = -90; + public const int UploadVideo = -100; + public const int UploadAudio = -101; + public const int UploadArticle = -102; + public const int UploadVectorGraphics = -103; public const int DefaultContentListView = -95; public const int DefaultMediaListView = -96; @@ -42,7 +46,7 @@ public static class ReservedPreValueKeys /// Defines the identifiers for Umbraco data types as constants for easy centralized access/management. /// public static class Guids - { + { /// /// Guid for Content Picker as string @@ -88,6 +92,49 @@ public static class Guids public static readonly Guid MultipleMediaPickerGuid = new Guid(MultipleMediaPicker); + /// + /// Guid for Media Picker v3 as string + /// + public const string MediaPicker3 = "4309A3EA-0D78-4329-A06C-C80B036AF19A"; + + /// + /// Guid for Media Picker v3 + /// + public static readonly Guid MediaPicker3Guid = new Guid(MediaPicker3); + + /// + /// Guid for Media Picker v3 multiple as string + /// + public const string MediaPicker3Multiple = "1B661F40-2242-4B44-B9CB-3990EE2B13C0"; + + /// + /// Guid for Media Picker v3 multiple + /// + public static readonly Guid MediaPicker3MultipleGuid = new Guid(MediaPicker3Multiple); + + + /// + /// Guid for Media Picker v3 single-image as string + /// + public const string MediaPicker3SingleImage = "AD9F0CF2-BDA2-45D5-9EA1-A63CFC873FD3"; + + /// + /// Guid for Media Picker v3 single-image + /// + public static readonly Guid MediaPicker3SingleImageGuid = new Guid(MediaPicker3SingleImage); + + + /// + /// Guid for Media Picker v3 multi-image as string + /// + public const string MediaPicker3MultipleImages = "0E63D883-B62B-4799-88C3-157F82E83ECC"; + + /// + /// Guid for Media Picker v3 multi-image + /// + public static readonly Guid MediaPicker3MultipleImagesGuid = new Guid(MediaPicker3MultipleImages); + + /// /// Guid for Related Links as string /// @@ -307,6 +354,46 @@ public static class Guids /// public static readonly Guid UploadGuid = new Guid(Upload); + /// + /// Guid for UploadVideo as string + /// + public const string UploadVideo = "70575fe7-9812-4396-bbe1-c81a76db71b5"; + + /// + /// Guid for UploadVideo + /// + public static readonly Guid UploadVideoGuid = new Guid(UploadVideo); + + /// + /// Guid for UploadAudio as string + /// + public const string UploadAudio = "8f430dd6-4e96-447e-9dc0-cb552c8cd1f3"; + + /// + /// Guid for UploadAudio + /// + public static readonly Guid UploadAudioGuid = new Guid(UploadAudio); + + /// + /// Guid for UploadArticle as string + /// + public const string UploadArticle = "bc1e266c-dac4-4164-bf08-8a1ec6a7143d"; + + /// + /// Guid for UploadArticle + /// + public static readonly Guid UploadArticleGuid = new Guid(UploadArticle); + + /// + /// Guid for UploadVectorGraphics as string + /// + public const string UploadVectorGraphics = "215cb418-2153-4429-9aef-8c0f0041191b"; + + /// + /// Guid for UploadVectorGraphics + /// + public static readonly Guid UploadVectorGraphicsGuid = new Guid(UploadVectorGraphics); + /// /// Guid for Label as string @@ -367,8 +454,8 @@ public static class Guids /// Guid for Label decimal /// public static readonly Guid LabelDecimalGuid = new Guid(LabelDecimal); - - + + } } } diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index d5cc37c9a58e..e15c1e162bea 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -59,6 +59,26 @@ public static class Icons /// public const string MediaFile = "icon-document"; + /// + /// System media video icon + /// + public const string MediaVideo = "icon-video"; + + /// + /// System media audio icon + /// + public const string MediaAudio = "icon-sound-waves"; + + /// + /// System media article icon + /// + public const string MediaArticle = "icon-article"; + + /// + /// System media vector icon + /// + public const string MediaVectorGraphics = "icon-picture"; + /// /// System media folder icon /// @@ -93,7 +113,7 @@ public static class Icons /// System packages icon /// public const string Packages = "icon-box"; - + /// /// System property editor icon /// diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 87739469d10b..f69570dc08ad 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -95,12 +95,17 @@ public static class Aliases /// ListView. /// public const string ListView = "Umbraco.ListView"; - + /// /// Media Picker. /// public const string MediaPicker = "Umbraco.MediaPicker"; + /// + /// Media Picker v.3. + /// + public const string MediaPicker3 = "Umbraco.MediaPicker3"; + /// /// Multiple Media Picker. /// diff --git a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs index d3402e69f894..20ada8c0f4a3 100644 --- a/src/Umbraco.Core/Constants-PropertyTypeGroups.cs +++ b/src/Umbraco.Core/Constants-PropertyTypeGroups.cs @@ -8,7 +8,7 @@ public static partial class Constants public static class PropertyTypeGroups { /// - /// Guid for a Image PropertyTypeGroup object. + /// Guid for an Image PropertyTypeGroup object. /// public const string Image = "79ED4D07-254A-42CF-8FA9-EBE1C116A596"; @@ -18,7 +18,27 @@ public static class PropertyTypeGroups public const string File = "50899F9C-023A-4466-B623-ABA9049885FE"; /// - /// Guid for a Image PropertyTypeGroup object. + /// Guid for a Video PropertyTypeGroup object. + /// + public const string Video = "2F0A61B6-CF92-4FF4-B437-751AB35EB254"; + + /// + /// Guid for an Audio PropertyTypeGroup object. + /// + public const string Audio = "335FB495-0A87-4E82-B902-30EB367B767C"; + + /// + /// Guid for an Article PropertyTypeGroup object. + /// + public const string Article = "9AF3BD65-F687-4453-9518-5F180D1898EC"; + + /// + /// Guid for a VectorGraphics PropertyTypeGroup object. + /// + public const string VectorGraphics = "F199B4D7-9E84-439F-8531-F87D9AF37711"; + + /// + /// Guid for a Membership PropertyTypeGroup object. /// public const string Membership = "0756729D-D665-46E3-B84A-37ACEAA614F8"; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 44de6113486d..264733e5b9a8 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -107,7 +107,11 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) InsertDataTypeNodeDto(Constants.DataTypes.LabelDateTime, 37, Constants.DataTypes.Guids.LabelDateTime, "Label (datetime)"); InsertDataTypeNodeDto(Constants.DataTypes.LabelTime, 38, Constants.DataTypes.Guids.LabelTime, "Label (time)"); InsertDataTypeNodeDto(Constants.DataTypes.LabelDecimal, 39, Constants.DataTypes.Guids.LabelDecimal, "Label (decimal)"); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Upload, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Upload}", SortOrder = 34, UniqueId = Constants.DataTypes.Guids.UploadGuid, Text = "Upload File", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadVideo, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadVideo}", SortOrder = 35, UniqueId = Constants.DataTypes.Guids.UploadVideoGuid, Text = "Upload Video", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadAudio, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadAudio}", SortOrder = 36, UniqueId = Constants.DataTypes.Guids.UploadAudioGuid, Text = "Upload Audio", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadArticle, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadArticle}", SortOrder = 37, UniqueId = Constants.DataTypes.Guids.UploadArticleGuid, Text = "Upload Article", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.UploadVectorGraphics, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.UploadVectorGraphics}", SortOrder = 38, UniqueId = Constants.DataTypes.Guids.UploadVectorGraphicsGuid, Text = "Upload Vector Graphics", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textarea, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textarea}", SortOrder = 33, UniqueId = Constants.DataTypes.Guids.TextareaGuid, Text = "Textarea", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Textbox, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Textbox}", SortOrder = 32, UniqueId = Constants.DataTypes.Guids.TextstringGuid, Text = "Textstring", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.RichtextEditor, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.RichtextEditor}", SortOrder = 4, UniqueId = Constants.DataTypes.Guids.RichtextEditorGuid, Text = "Richtext editor", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); @@ -126,6 +130,10 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1031, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1031", SortOrder = 2, UniqueId = new Guid("f38bd2d7-65d0-48e6-95dc-87ce06ec2d3d"), Text = Constants.Conventions.MediaTypes.Folder, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1032, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1032", SortOrder = 2, UniqueId = new Guid("cc07b313-0843-4aa8-bbda-871c8da728c8"), Text = Constants.Conventions.MediaTypes.Image, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1033, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1033", SortOrder = 2, UniqueId = new Guid("4c52d8ab-54e6-40cd-999c-7a5f24903e4d"), Text = Constants.Conventions.MediaTypes.File, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1034, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1034", SortOrder = 2, UniqueId = new Guid("f6c515bb-653c-4bdc-821c-987729ebe327"), Text = Constants.Conventions.MediaTypes.Video, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1035, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1035", SortOrder = 2, UniqueId = new Guid("a5ddeee0-8fd8-4cee-a658-6f1fcdb00de3"), Text = Constants.Conventions.MediaTypes.Audio, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1036, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1036", SortOrder = 2, UniqueId = new Guid("a43e3414-9599-4230-a7d3-943a21b20122"), Text = Constants.Conventions.MediaTypes.Article, NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1037, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1037", SortOrder = 2, UniqueId = new Guid("c4b1efcf-a9d5-41c4-9621-e9d273b52a9c"), Text = "Vector Graphics (SVG)", NodeObjectType = Constants.ObjectTypes.MediaType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.Tags, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.Tags}", SortOrder = 2, UniqueId = new Guid("b6b73142-b9c1-4bf8-a16d-e1c23320b549"), Text = "Tags", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = Constants.DataTypes.ImageCropper, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = $"-1,{Constants.DataTypes.ImageCropper}", SortOrder = 2, UniqueId = new Guid("1df9f033-e6d4-451f-b8d2-e0cbc50a836f"), Text = "Image Cropper", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1044, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1044", SortOrder = 0, UniqueId = new Guid("d59be02f-1df9-4228-aa1e-01917d806cda"), Text = Constants.Conventions.MemberTypes.DefaultAlias, NodeObjectType = Constants.ObjectTypes.MemberType, CreateDate = DateTime.Now }); @@ -133,9 +141,15 @@ void InsertDataTypeNodeDto(int id, int sortOrder, string uniqueId, string text) //New UDI pickers with newer Ids _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1046, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1046", SortOrder = 2, UniqueId = new Guid("FD1E0DA5-5606-4862-B679-5D0CF3A52A59"), Text = "Content Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1047, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1047", SortOrder = 2, UniqueId = new Guid("1EA2E01F-EBD8-4CE1-8D71-6B1149E63548"), Text = "Member Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); - _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1048, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1048", SortOrder = 2, UniqueId = new Guid("135D60E0-64D9-49ED-AB08-893C9BA44AE5"), Text = "Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1049, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1049", SortOrder = 2, UniqueId = new Guid("9DBBCBBB-2327-434A-B355-AF1B84E5010A"), Text = "Multiple Media Picker (old)", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1050, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1050", SortOrder = 2, UniqueId = new Guid("B4E3535A-1753-47E2-8568-602CF8CFEE6F"), Text = "Multi URL Picker", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1051, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1051", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3Guid, Text = "Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1052, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1052", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleGuid, Text = "Multiple Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1053, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1053", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3SingleImageGuid, Text = "Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + _database.Insert(Constants.DatabaseSchema.Tables.Node, "id", false, new NodeDto { NodeId = 1054, Trashed = false, ParentId = -1, UserId = -1, Level = 1, Path = "-1,1054", SortOrder = 2, UniqueId = Constants.DataTypes.Guids.MediaPicker3MultipleImagesGuid, Text = "Multiple Image Media Picker 3", NodeObjectType = Constants.ObjectTypes.DataType, CreateDate = DateTime.Now }); + } private void CreateLockData() @@ -160,6 +174,10 @@ private void CreateContentTypeData() _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = Constants.Icons.MediaFolder, Thumbnail = Constants.Icons.MediaFolder, IsContainer = false, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = Constants.Icons.MediaImage, Thumbnail = Constants.Icons.MediaImage, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = Constants.Icons.MediaFile, Thumbnail = Constants.Icons.MediaFile, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 540, NodeId = 1034, Alias = Constants.Conventions.MediaTypes.Video, Icon = Constants.Icons.MediaVideo, Thumbnail = Constants.Icons.MediaVideo, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 541, NodeId = 1035, Alias = Constants.Conventions.MediaTypes.Audio, Icon = Constants.Icons.MediaAudio, Thumbnail = Constants.Icons.MediaAudio, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 542, NodeId = 1036, Alias = Constants.Conventions.MediaTypes.Article, Icon = Constants.Icons.MediaArticle, Thumbnail = Constants.Icons.MediaArticle, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 543, NodeId = 1037, Alias = Constants.Conventions.MediaTypes.VectorGraphics, Icon = Constants.Icons.MediaVectorGraphics, Thumbnail = Constants.Icons.MediaVectorGraphics, AllowAtRoot = true, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.ContentType, "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = Constants.Icons.Member, Thumbnail = Constants.Icons.Member, Variations = (byte) ContentVariation.Nothing }); } @@ -207,20 +225,44 @@ private void CreatePropertyTypeGroupData() { _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Image) }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, ContentTypeNodeId = 1033, Text = "File", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.File) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, ContentTypeNodeId = 1034, Text = "Video", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Video) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, ContentTypeNodeId = 1035, Text = "Audio", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Audio) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, ContentTypeNodeId = 1036, Text = "Article", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Article) }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, ContentTypeNodeId = 1037, Text = "Vector Graphics", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.VectorGraphics) }); //membership property group _database.Insert(Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, ContentTypeNodeId = 1044, Text = "Membership", SortOrder = 1, UniqueId = new Guid(Constants.PropertyTypeGroups.Membership) }); } private void CreatePropertyTypeData() { - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Upload image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 6, UniqueId = 6.ToGuid(), DataTypeId = Constants.DataTypes.ImageCropper, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.File, Name = "Image", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 7, UniqueId = 7.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Width, Name = "Width", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 8, UniqueId = 8.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Height, Name = "Height", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in pixels", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 9, UniqueId = 9.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 10, UniqueId = 10.ToGuid(), DataTypeId = -92, ContentTypeId = 1032, PropertyTypeGroupId = 3, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "Upload file", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 24, UniqueId = 24.ToGuid(), DataTypeId = Constants.DataTypes.Upload, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.File, Name = "File", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 25, UniqueId = 25.ToGuid(), DataTypeId = -92, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 26, UniqueId = 26.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1033, PropertyTypeGroupId = 4, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 40, UniqueId = 40.ToGuid(), DataTypeId = Constants.DataTypes.UploadVideo, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.File, Name = "Video", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 41, UniqueId = 41.ToGuid(), DataTypeId = -92, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 42, UniqueId = 42.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1034, PropertyTypeGroupId = 52, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 43, UniqueId = 43.ToGuid(), DataTypeId = Constants.DataTypes.UploadAudio, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.File, Name = "Audio", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 44, UniqueId = 44.ToGuid(), DataTypeId = -92, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 45, UniqueId = 45.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1035, PropertyTypeGroupId = 53, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 46, UniqueId = 46.ToGuid(), DataTypeId = Constants.DataTypes.UploadArticle, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.File, Name = "Article", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 47, UniqueId = 47.ToGuid(), DataTypeId = -92, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 48, UniqueId = 48.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1036, PropertyTypeGroupId = 54, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 49, UniqueId = 49.ToGuid(), DataTypeId = Constants.DataTypes.UploadVectorGraphics, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.File, Name = "Vector Graphics", SortOrder = 0, Mandatory = true, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 50, UniqueId = 50.ToGuid(), DataTypeId = -92, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Extension, Name = "Type", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 51, UniqueId = 51.ToGuid(), DataTypeId = Constants.DataTypes.LabelBigint, ContentTypeId = 1037, PropertyTypeGroupId = 55, Alias = Constants.Conventions.Media.Bytes, Name = "Size", SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = "in bytes", Variations = (byte) ContentVariation.Nothing }); + + + + //membership property types _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 28, UniqueId = 28.ToGuid(), DataTypeId = Constants.DataTypes.Textarea, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.Comments, Name = Constants.Conventions.Member.CommentsLabel, SortOrder = 0, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 29, UniqueId = 29.ToGuid(), DataTypeId = Constants.DataTypes.LabelInt, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.FailedPasswordAttempts, Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, SortOrder = 1, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); @@ -244,6 +286,10 @@ private void CreateContentChildTypeData() _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1031 }); _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1032 }); _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1033 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1034 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1035 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1036 }); + _database.Insert(Constants.DatabaseSchema.Tables.ContentChildType, "Id", false, new ContentTypeAllowedContentTypeDto { Id = 1031, AllowedId = 1037 }); } private void CreateDataTypeData() @@ -307,6 +353,63 @@ void InsertDataTypeDto(int id, string editorAlias, string dbType, string configu _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1049, EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext", Configuration = "{\"multiPicker\":1}" }); _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1050, EditorAlias = Constants.PropertyEditors.Aliases.MultiUrlPicker, DbType = "Ntext" }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVideo, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp4\"}, {\"id\":1, \"value\":\"webm\"}, {\"id\":2, \"value\":\"ogv\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadAudio, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"mp3\"}, {\"id\":1, \"value\":\"weba\"}, {\"id\":2, \"value\":\"oga\"}, {\"id\":3, \"value\":\"opus\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadArticle, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"pdf\"}, {\"id\":1, \"value\":\"docx\"}, {\"id\":2, \"value\":\"doc\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto + { + NodeId = Constants.DataTypes.UploadVectorGraphics, + EditorAlias = Constants.PropertyEditors.Aliases.UploadField, + DbType = "Nvarchar", + Configuration = "{\"fileExtensions\":[{\"id\":0, \"value\":\"svg\"}]}" + }); + + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1051, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1052, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"multiple\": true}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1053, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + "\", \"multiple\": false, \"validationLimit\":{\"min\":0,\"max\":1}}" + }); + _database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { + NodeId = 1054, + EditorAlias = Constants.PropertyEditors.Aliases.MediaPicker3, + DbType = "Ntext", + Configuration = "{\"filter\":\"" + Constants.Conventions.MediaTypes.Image + "\", \"multiple\": true}" + }); } private void CreateRelationTypeData() diff --git a/src/Umbraco.Core/Models/DataTypeExtensions.cs b/src/Umbraco.Core/Models/DataTypeExtensions.cs index f460edbde70f..913aa4773ef2 100644 --- a/src/Umbraco.Core/Models/DataTypeExtensions.cs +++ b/src/Umbraco.Core/Models/DataTypeExtensions.cs @@ -62,6 +62,10 @@ public static T ConfigurationAs(this IDataType dataType) Constants.DataTypes.Guids.TextstringGuid, Constants.DataTypes.Guids.TextareaGuid, Constants.DataTypes.Guids.UploadGuid, + Constants.DataTypes.Guids.UploadArticleGuid, + Constants.DataTypes.Guids.UploadAudioGuid, + Constants.DataTypes.Guids.UploadVectorGraphicsGuid, + Constants.DataTypes.Guids.UploadVideoGuid, Constants.DataTypes.Guids.LabelStringGuid, Constants.DataTypes.Guids.LabelDecimalGuid, Constants.DataTypes.Guids.LabelDateTimeGuid, diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs index a1f4bad9a1b1..65aafafbcde0 100644 --- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs +++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs @@ -29,7 +29,7 @@ public virtual bool IsDirty() /// public virtual bool IsPropertyDirty(string propertyName) { - return _currentChanges != null && _currentChanges.Any(x => x.Key == propertyName); + return _currentChanges != null && _currentChanges.ContainsKey(propertyName); } /// @@ -61,7 +61,7 @@ public virtual bool WasDirty() /// public virtual bool WasPropertyDirty(string propertyName) { - return _savedChanges != null && _savedChanges.Any(x => x.Key == propertyName); + return _savedChanges != null && _savedChanges.ContainsKey(propertyName); } /// diff --git a/src/Umbraco.Core/Models/MediaWithCrops.cs b/src/Umbraco.Core/Models/MediaWithCrops.cs new file mode 100644 index 000000000000..ef3205bd9436 --- /dev/null +++ b/src/Umbraco.Core/Models/MediaWithCrops.cs @@ -0,0 +1,15 @@ +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Core.Models +{ + /// + /// Model used in Razor Views for rendering + /// + public class MediaWithCrops + { + public IPublishedContent MediaItem { get; set; } + + public ImageCropperValue LocalCrops { get; set; } + } +} diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs index e7a14a26e2e7..68036dab4bc3 100644 --- a/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/ContentTypeDto.cs @@ -11,7 +11,7 @@ internal class ContentTypeDto public const string TableName = Constants.DatabaseSchema.Tables.ContentType; [Column("pk")] - [PrimaryKeyColumn(IdentitySeed = 535)] + [PrimaryKeyColumn(IdentitySeed = 700)] public int PrimaryKey { get; set; } [Column("nodeId")] diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs index 572201c94a0e..f22e4453f420 100644 --- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs +++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Persistence.Dtos internal class PropertyTypeDto { [Column("id")] - [PrimaryKeyColumn(IdentitySeed = 50)] + [PrimaryKeyColumn(IdentitySeed = 100)] public int Id { get; set; } [Column("dataTypeId")] diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index 0971b2047a7c..6746d6a4295f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -73,5 +73,10 @@ public interface IDocumentRepository : IContentRepository, IReadR /// /// void AddOrUpdatePermissions(ContentPermissionSet permission); + + /// + /// Returns true if there is any content in the recycle bin + /// + bool RecycleBinSmells(); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs index d4ec08a0dfe0..6f36102a4002 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IMediaRepository.cs @@ -6,5 +6,6 @@ namespace Umbraco.Core.Persistence.Repositories public interface IMediaRepository : IContentRepository, IReadRepository { IMedia GetMediaByPath(string mediaPath); + bool RecycleBinSmells(); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs index 57d5dfa86493..d73b360677ba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ConsentRepository.cs @@ -86,7 +86,7 @@ protected override void PersistUpdatedItem(IConsent entity) Database.Update(dto); entity.ResetDirtyProperties(); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Id)); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs index 0b5866395279..0e3521f8bc48 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -130,7 +130,7 @@ protected override void PersistNewItem(IDictionaryItem entity) foreach (var translation in dictionaryItem.Translations) translation.Value = translation.Value.ToValidXmlString(); - + var dto = DictionaryItemFactory.BuildDto(dictionaryItem); var id = Convert.ToInt32(Database.Insert(dto)); @@ -152,7 +152,7 @@ protected override void PersistUpdatedItem(IDictionaryItem entity) foreach (var translation in entity.Translations) translation.Value = translation.Value.ToValidXmlString(); - + var dto = DictionaryItemFactory.BuildDto(entity); Database.Update(dto); @@ -174,8 +174,8 @@ protected override void PersistUpdatedItem(IDictionaryItem entity) entity.ResetDirtyProperties(); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); } protected override void PersistDeletedItem(IDictionaryItem entity) @@ -186,8 +186,8 @@ protected override void PersistDeletedItem(IDictionaryItem entity) Database.Delete("WHERE id = @Id", new { Id = entity.Key }); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.ItemKey)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(entity.Key)); entity.DeleteDate = DateTime.Now; } @@ -203,8 +203,8 @@ private void RecursiveDelete(Guid parentId) Database.Delete("WHERE id = @Id", new { Id = dto.UniqueId }); //Clear the cache entries that exist by uniqueid/item key - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); - IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.Key)); + IsolatedCache.Clear(RepositoryCacheKeys.GetKey(dto.UniqueId)); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 09d41a49a099..f5d993070c04 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -912,6 +912,15 @@ public bool IsPathPublished(IContent content) public override int RecycleBinId => Constants.System.RecycleBinContent; + public bool RecycleBinSmells() + { + var cache = _appCaches.RuntimeCache; + var cacheKey = CacheKeys.ContentRecycleBinCacheKey; + + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } + #endregion #region Read Repository implementation for Guid keys @@ -1154,7 +1163,7 @@ private IEnumerable MapDtosToContent(List dtos, if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.DocumentVersionDto.ContentVersionDto.Id) { content[i] = (Content)cached; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 02bef366cbf5..c456a0b2ad98 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -24,6 +24,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MediaRepository : ContentRepositoryBase, IMediaRepository { + private readonly AppCaches _cache; private readonly IMediaTypeRepository _mediaTypeRepository; private readonly ITagRepository _tagRepository; private readonly MediaByGuidReadRepository _mediaByGuidReadRepository; @@ -32,6 +33,7 @@ public MediaRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger lo Lazy propertyEditorCollection, DataValueReferenceFactoryCollection dataValueReferenceFactories) : base(scopeAccessor, cache, logger, languageRepository, relationRepository, relationTypeRepository, propertyEditorCollection, dataValueReferenceFactories) { + _cache = cache; _mediaTypeRepository = mediaTypeRepository ?? throw new ArgumentNullException(nameof(mediaTypeRepository)); _tagRepository = tagRepository ?? throw new ArgumentNullException(nameof(tagRepository)); _mediaByGuidReadRepository = new MediaByGuidReadRepository(this, scopeAccessor, cache, logger); @@ -369,6 +371,15 @@ protected override void PersistDeletedItem(IMedia entity) public override int RecycleBinId => Constants.System.RecycleBinMedia; + public bool RecycleBinSmells() + { + var cache = _cache.RuntimeCache; + var cacheKey = CacheKeys.MediaRecycleBinCacheKey; + + // always cache either true or false + return cache.GetCacheItem(cacheKey, () => CountChildren(RecycleBinId) > 0); + } + #endregion #region Read Repository implementation for Guid keys @@ -497,10 +508,10 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { - content[i] = (Models.Media) cached; + content[i] = (Models.Media)cached; continue; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 78dbbe317acd..15c707c624ff 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -331,7 +331,7 @@ protected override void PersistNewItem(IMember entity) } protected override void PersistUpdatedItem(IMember entity) - { + { // update entity.UpdatingEntity(); @@ -534,7 +534,7 @@ public void SetLastLogin(string username, DateTime date) var sqlSelectTemplateVersion = SqlContext.Templates.Get("Umbraco.Core.MemberRepository.SetLastLogin2", s => s .Select(x => x.Id) - .From() + .From() .InnerJoin().On((l, r) => l.NodeId == r.NodeId) .InnerJoin().On((l, r) => l.NodeId == r.NodeId) .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")) @@ -606,7 +606,7 @@ private IEnumerable MapDtosToContent(List dtos, bool withCac if (withCache) { // if the cache contains the (proper version of the) item, use it - var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); + var cached = IsolatedCache.GetCacheItem(RepositoryCacheKeys.GetKey(dto.NodeId)); if (cached != null && cached.VersionId == dto.ContentVersionDto.Id) { content[i] = (Member) cached; diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs index 09a7c021f81f..e2d0e26274ec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RepositoryCacheKeys.cs @@ -8,14 +8,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal static class RepositoryCacheKeys { - private static readonly Dictionary Keys = new Dictionary(); + private static readonly Dictionary s_keys = new Dictionary(); public static string GetKey() { var type = typeof(T); - return Keys.TryGetValue(type, out var key) ? key : (Keys[type] = "uRepo_" + type.Name + "_"); + return s_keys.TryGetValue(type, out var key) ? key : (s_keys[type] = "uRepo_" + type.Name + "_"); } - public static string GetKey(object id) => GetKey() + id; + public static string GetKey(TId id) + { + if (EqualityComparer.Default.Equals(id, default)) + { + return string.Empty; + } + + if (typeof(TId).IsValueType) + { + return GetKey() + id; + } + + return GetKey() + id.ToString().ToUpperInvariant(); + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs index c9f85c343c0b..9bc0bbeb4702 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/UserRepository.cs @@ -83,6 +83,14 @@ private string PasswordConfigJson protected override IUser PerformGet(int id) { + // This will never resolve to a user, yet this is asked + // for all of the time (especially in cases of members). + // Don't issue a SQL call for this, we know it will not exist. + if (id == default || id < -1) + { + return null; + } + var sql = SqlContext.Sql() .Select() .From() @@ -168,7 +176,7 @@ public IDictionary GetUserStates() } public Guid CreateLoginSession(int userId, string requestingIpAddress, bool cleanStaleSessions = true) - { + { var now = DateTime.UtcNow; var dto = new UserLoginDto { diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs index 270c8c3b0ba9..694ebfde2780 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/JsonValueConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Composing; @@ -18,6 +19,8 @@ public class JsonValueConverter : PropertyValueConverterBase { private readonly PropertyEditorCollection _propertyEditors; + string[] ExcludedPropertyEditors = new string[] { Constants.PropertyEditors.Aliases.MediaPicker3 }; + /// /// Initializes a new instance of the class. /// @@ -28,13 +31,16 @@ public JsonValueConverter(PropertyEditorCollection propertyEditors) /// /// It is a converter for any value type that is "JSON" + /// Unless it's in the Excluded Property Editors list + /// The new MediaPicker 3 stores JSON but we want to use its own ValueConvertor /// /// /// public override bool IsConverter(IPublishedPropertyType propertyType) { return _propertyEditors.TryGet(propertyType.EditorAlias, out var editor) - && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json); + && editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json) + && ExcludedPropertyEditors.Contains(propertyType.EditorAlias) == false; } public override Type GetPropertyValueType(IPublishedPropertyType propertyType) diff --git a/src/Umbraco.Core/Services/ContentServiceExtensions.cs b/src/Umbraco.Core/Services/ContentServiceExtensions.cs index 1d980b036b07..6ca894e16082 100644 --- a/src/Umbraco.Core/Services/ContentServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentServiceExtensions.cs @@ -89,25 +89,5 @@ public static void RemoveContentPermissions(this IContentService contentService, { contentService.SetPermissions(new EntityPermissionSet(contentId, new EntityPermissionCollection())); } - - /// - /// Returns true if there is any content in the recycle bin - /// - /// - /// - public static bool RecycleBinSmells(this IContentService contentService) - { - return contentService.CountChildren(Constants.System.RecycleBinContent) > 0; - } - - /// - /// Returns true if there is any media in the recycle bin - /// - /// - /// - public static bool RecycleBinSmells(this IMediaService mediaService) - { - return mediaService.CountChildren(Constants.System.RecycleBinMedia) > 0; - } } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 58279fb4daf4..c2915005334e 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -326,6 +326,11 @@ IEnumerable GetPagedOfTypes(int[] contentTypeIds, long pageIndex, int /// Optional Id of the User emptying the Recycle Bin OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId); + /// + /// Returns true if there is any content in the recycle bin + /// + bool RecycleBinSmells(); + /// /// Sorts documents. /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 3fecb2003565..5bbfb76735b3 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -172,6 +172,11 @@ IEnumerable GetPagedMediaInRecycleBin(long pageIndex, int pageSize, out /// Optional Id of the User emptying the Recycle Bin OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUserId); + /// + /// Returns true if there is any media in the recycle bin + /// + bool RecycleBinSmells(); + /// /// Deletes all media of specified type. All children of deleted media is moved to Recycle Bin. /// diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index e5363d0e2b11..dc914ca3ac93 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2118,6 +2118,15 @@ public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUser return OperationResult.Succeed(evtMsgs); } + public bool RecycleBinSmells() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.ContentTree); + return _documentRepository.RecycleBinSmells(); + } + } + #endregion #region Others diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index ec84c0738e29..59d6ae543c4d 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -1088,6 +1088,15 @@ public OperationResult EmptyRecycleBin(int userId = Constants.Security.SuperUser return OperationResult.Succeed(evtMsgs); } + public bool RecycleBinSmells() + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.ReadLock(Constants.Locks.MediaTree); + return _mediaRepository.RecycleBinSmells(); + } + } + #endregion #region Others diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 2ea5292d7327..0a453ad75f07 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -156,6 +156,7 @@ + diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 517edf354cc3..39fbe927d4c7 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -49,7 +49,7 @@ - + 1.0.0-beta2-19324-01 runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Umbraco.Examine/UmbracoExamineIndex.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs index 511d78db92d2..c9ab2cd44678 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -25,8 +25,7 @@ public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndex, IIndexDi // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call // context because they will fork a thread/task/whatever which should *not* capture our // call context (and the database it can contain)! - // TODO: FIX Examine to not flow the ExecutionContext so callers don't need to worry about this! - + /// /// Used to store the path of a content object /// @@ -99,13 +98,7 @@ protected override void PerformDeleteFromIndex(IEnumerable itemIds, Acti { if (CanInitialize()) { - // Use SafeCallContext to prevent the current CallContext flow to child - // tasks executed in the base class so we don't leak Scopes. - // TODO: See notes at the top of this class - using (new SafeCallContext()) - { - base.PerformDeleteFromIndex(itemIds, onComplete); - } + base.PerformDeleteFromIndex(itemIds, onComplete); } } @@ -113,13 +106,7 @@ protected override void PerformIndexItems(IEnumerable values, Action() + .Add() + .Add(); + builder.SetWeight(10); + + var factory = _composition.CreateFactory(); + var col = builder.CreateCollection(factory); + AssertCollection(col, typeof(Resolved1), typeof(Resolved2)); + } + #region Assertions private static void AssertCollection(IEnumerable col, params Type[] expected) diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index b0c57b685b83..d2bbf3e86529 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ public void Resolves_Types() public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(40, types.Count()); + Assert.AreEqual(41, types.Count()); } /// diff --git a/src/Umbraco.Tests/Integration/ContentEventsTests.cs b/src/Umbraco.Tests/Integration/ContentEventsTests.cs index af8ebe626e4f..fdf0b9870517 100644 --- a/src/Umbraco.Tests/Integration/ContentEventsTests.cs +++ b/src/Umbraco.Tests/Integration/ContentEventsTests.cs @@ -1097,11 +1097,12 @@ public void EmptyRecycleBinContent() ResetEvents(); ServiceContext.ContentService.EmptyRecycleBin(Constants.Security.SuperUserId); - Assert.AreEqual(2, _msgCount); - Assert.AreEqual(2, _events.Count); + Assert.AreEqual(3, _msgCount); + Assert.AreEqual(3, _events.Count); var i = 0; var m = 0; - Assert.AreEqual($"{m:000}: ContentRepository/Remove/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{Constants.System.RecycleBinContent}", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content.Id}", _events[i].ToString()); } @@ -1122,12 +1123,14 @@ public void EmptyRecycleBinContents() ResetEvents(); ServiceContext.ContentService.EmptyRecycleBin(Constants.Security.SuperUserId); - Assert.AreEqual(3, _msgCount); - Assert.AreEqual(4, _events.Count); + Assert.AreEqual(4, _msgCount); + Assert.AreEqual(5, _events.Count); var i = 0; var m = 0; Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content1.Id}", _events[i++].ToString()); - Assert.AreEqual($"{m:000}: ContentRepository/Remove/{content2.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content2.Id}", _events[i++].ToString()); + + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{Constants.System.RecycleBinContent}", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content1.Id}", _events[i++].ToString()); Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content2.Id}", _events[i].ToString()); @@ -1153,8 +1156,8 @@ public void EmptyRecycleBinBranch() ServiceContext.ContentService.EmptyRecycleBin(Constants.Security.SuperUserId); - Assert.AreEqual(14, _msgCount); - Assert.AreEqual(14, _events.Count); + Assert.AreEqual(15, _msgCount); + Assert.AreEqual(15, _events.Count); var i = 0; var m = 0; @@ -1170,7 +1173,9 @@ public void EmptyRecycleBinBranch() Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content2C[1].Id}", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content2C[0].Id}", _events[i++].ToString()); Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content1C[0].Id}", _events[i++].ToString()); - Assert.AreEqual($"{m:000}: ContentRepository/Remove/{content1.Id}", _events[i++].ToString()); + Assert.AreEqual($"{m++:000}: ContentRepository/Remove/{content1.Id}", _events[i++].ToString()); + + Assert.AreEqual($"{m:000}: ContentCacheRefresher/RefreshNode/{Constants.System.RecycleBinContent}", _events[i++].ToString()); m++; Assert.AreEqual($"{m:000}: ContentCacheRefresher/Remove/{content1.Id}", _events[i].ToString()); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs index ca8ee29ee3a7..339b3d4931b4 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs @@ -279,7 +279,7 @@ public void Can_Perform_GetAll_On_DataTypeDefinitionRepository() Assert.That(dataTypeDefinitions, Is.Not.Null); Assert.That(dataTypeDefinitions.Any(), Is.True); Assert.That(dataTypeDefinitions.Any(x => x == null), Is.False); - Assert.That(dataTypeDefinitions.Length, Is.EqualTo(29)); + Assert.That(dataTypeDefinitions.Length, Is.EqualTo(37)); } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs index bb3286daedad..e048886dbe4f 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -50,7 +50,7 @@ public void Can_Move() containerRepository.Save(container2); - var contentType = (IMediaType)MockedContentTypes.CreateVideoMediaType(); + var contentType = (IMediaType)MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container2.Id; repository.Save(contentType); @@ -133,7 +133,7 @@ public void Can_Create_Container_Containing_Media_Types() containerRepository.Save(container); - var contentType = MockedContentTypes.CreateVideoMediaType(); + var contentType = MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container.Id; repository.Save(contentType); @@ -155,7 +155,7 @@ public void Can_Delete_Container_Containing_Media_Types() containerRepository.Save(container); - IMediaType contentType = MockedContentTypes.CreateVideoMediaType(); + IMediaType contentType = MockedContentTypes.CreateNewMediaType(); contentType.ParentId = container.Id; repository.Save(contentType); @@ -183,7 +183,7 @@ public void Can_Perform_Add_On_MediaTypeRepository() var repository = CreateRepository(provider); // Act - var contentType = MockedContentTypes.CreateVideoMediaType(); + var contentType = MockedContentTypes.CreateNewMediaType(); repository.Save(contentType); @@ -210,7 +210,7 @@ public void Can_Perform_Update_On_MediaTypeRepository() { var repository = CreateRepository(provider); - var videoMediaType = MockedContentTypes.CreateVideoMediaType(); + var videoMediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(videoMediaType); @@ -249,7 +249,7 @@ public void Can_Perform_Delete_On_MediaTypeRepository() var repository = CreateRepository(provider); // Act - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); @@ -378,7 +378,7 @@ public void Can_Update_MediaType_With_PropertyType_Removed() { var repository = CreateRepository(provider); - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); @@ -406,7 +406,7 @@ public void Can_Verify_PropertyTypes_On_Video_MediaType() { var repository = CreateRepository(provider); - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); repository.Save(mediaType); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index f801d02c5bc0..bf8450383720 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -1,29 +1,27 @@ -using System.Web; -using System.Xml.Linq; -using System.Xml.XPath; +using Examine; using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Models; -using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; -using Umbraco.Tests.UmbracoExamine; -using Umbraco.Web; using System.Linq; using System.Threading; +using System.Web; using System.Xml; -using Examine; +using System.Xml.Linq; +using System.Xml.XPath; +using Umbraco.Core; using Umbraco.Core.Cache; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.Strings; -using Umbraco.Examine; -using Current = Umbraco.Web.Composing.Current; -using Umbraco.Tests.Testing; using Umbraco.Core.Composing; +using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Examine; using Umbraco.Tests.LegacyXmlPublishedCache; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Tests.Testing; using Umbraco.Tests.Testing.Objects.Accessors; +using Umbraco.Tests.UmbracoExamine; +using Umbraco.Web; namespace Umbraco.Tests.PublishedContent { @@ -94,6 +92,7 @@ public void Get_Property_Value_Uses_Converter() Name = "Rich Text", DataTypeId = -87 //tiny mce }); + var existing = ServiceContext.MediaTypeService.GetAll(); ServiceContext.MediaTypeService.Save(mType); var media = MockedMedia.CreateMediaImage(mType, -1); media.Properties["content"].SetValue("
This is some content
"); diff --git a/src/Umbraco.Tests/Services/MediaServiceTests.cs b/src/Umbraco.Tests/Services/MediaServiceTests.cs index 52f26ecb4d15..d5cec11211c1 100644 --- a/src/Umbraco.Tests/Services/MediaServiceTests.cs +++ b/src/Umbraco.Tests/Services/MediaServiceTests.cs @@ -163,7 +163,7 @@ public void Cannot_Save_Media_With_Empty_Name() { // Arrange var mediaService = ServiceContext.MediaService; - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); var media = mediaService.CreateMedia(string.Empty, -1, "video"); @@ -175,7 +175,7 @@ public void Cannot_Save_Media_With_Empty_Name() public void Ensure_Content_Xml_Created() { var mediaService = ServiceContext.MediaService; - var mediaType = MockedContentTypes.CreateVideoMediaType(); + var mediaType = MockedContentTypes.CreateNewMediaType(); ServiceContext.MediaTypeService.Save(mediaType); var media = mediaService.CreateMedia("Test", -1, "video"); diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index e3bb012dae0d..1b85787fee3a 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -378,13 +378,13 @@ public static ContentType CreateAllTypesContentType(string alias, string name) return contentType; } - public static MediaType CreateVideoMediaType() + public static MediaType CreateNewMediaType() { var mediaType = new MediaType(-1) { - Alias = "video", - Name = "Video", - Description = "ContentType used for videos", + Alias = "newMediaType", + Name = "New Media Type", + Description = "ContentType used for a new format", Icon = ".sprTreeDoc3", Thumbnail = "doc.png", SortOrder = 1, diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 9d4bc4294c25..242040836bf2 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -79,7 +79,7 @@ - + 1.8.14 diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 0e8f48084f01..3f53638fc6a8 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -15755,9 +15755,9 @@ "dev": true }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha1-BtzjSg5op7q8KbNluOdLiSUgOWE=" + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, "undertaker": { "version": "1.2.1", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 95f5bcef4999..6514f2f2178f 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -41,13 +41,13 @@ "lazyload-js": "1.0.0", "moment": "2.22.2", "ng-file-upload": "12.2.13", - "nouislider": "14.6.3", + "nouislider": "14.6.4", "npm": "^6.14.7", "signalr": "2.4.0", "spectrum-colorpicker2": "2.0.8", "tinymce": "4.9.11", "typeahead.js": "0.11.1", - "underscore": "1.9.1", + "underscore": "1.12.1", "wicg-inert": "^3.0.2" }, "devDependencies": { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js index bbda02806e90..fb58b4287dc3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbtoggle.directive.js @@ -129,6 +129,8 @@ replace: true, templateUrl: 'views/components/buttons/umb-toggle.html', scope: { + // TODO: This should have required ngModel so we can track and validate user input correctly + // https://docs.angularjs.org/api/ng/type/ngModel.NgModelController#custom-control-example checked: "=", disabled: "=", inputId: "@", 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 da93450522fd..bce797d5c819 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 @@ -442,7 +442,6 @@ // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish function performSave(args) { - //Used to check validility of nested form - coming from Content Apps mostly //Set them all to be invalid var fieldsToRollback = checkValidility(); @@ -455,7 +454,8 @@ create: $scope.page.isNew, action: args.action, showNotifications: args.showNotifications, - softRedirect: true + softRedirect: true, + skipValidation: args.skipValidation }).then(function (data) { //success init(); @@ -467,23 +467,24 @@ eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: true }); - resetNestedFieldValiation(fieldsToRollback); + if($scope.contentForm.$invalid !== true) { + resetNestedFieldValiation(fieldsToRollback); + } ensureDirtyIsSetIfAnyVariantIsDirty(); return $q.when(data); }, function (err) { - - syncTreeNode($scope.content, $scope.content.path); + if($scope.contentForm.$invalid !== true) { + resetNestedFieldValiation(fieldsToRollback); + } if (err && err.status === 400 && err.data) { // content was saved but is invalid. eventsService.emit("content.saved", { content: $scope.content, action: args.action, valid: false }); } - resetNestedFieldValiation(fieldsToRollback); - return $q.reject(err); }); } @@ -735,48 +736,48 @@ clearNotifications($scope.content); // TODO: Add "..." to save button label if there are more than one variant to publish - currently it just adds the elipses if there's more than 1 variant if (hasVariants($scope.content)) { - //before we launch the dialog we want to execute all client side validations first - if (formHelper.submitForm({ scope: $scope, action: "openSaveDialog" })) { - - var dialog = { - parentScope: $scope, - view: "views/content/overlays/save.html", - variants: $scope.content.variants, //set a model property for the dialog - skipFormValidation: true, //when submitting the overlay form, skip any client side validation - submitButtonLabelKey: "buttons_save", - submit: function (model) { - model.submitButtonState = "busy"; + var dialog = { + parentScope: $scope, + view: "views/content/overlays/save.html", + variants: $scope.content.variants, //set a model property for the dialog + skipFormValidation: true, //when submitting the overlay form, skip any client side validation + submitButtonLabelKey: "buttons_save", + submit: function (model) { + model.submitButtonState = "busy"; + clearNotifications($scope.content); + //we need to return this promise so that the dialog can handle the result and wire up the validation response + return performSave({ + saveMethod: $scope.saveMethod(), + action: "save", + showNotifications: false, + skipValidation: true + }).then(function (data) { + //show all notifications manually here since we disabled showing them automatically in the save method + formHelper.showNotifications(data); clearNotifications($scope.content); - //we need to return this promise so that the dialog can handle the result and wire up the validation response - return performSave({ - saveMethod: $scope.saveMethod(), - action: "save", - showNotifications: false - }).then(function (data) { - //show all notifications manually here since we disabled showing them automatically in the save method - formHelper.showNotifications(data); - clearNotifications($scope.content); - overlayService.close(); - return $q.when(data); - }, - function (err) { - clearDirtyState($scope.content.variants); - model.submitButtonState = "error"; - //re-map the dialog model since we've re-bound the properties - dialog.variants = $scope.content.variants; - handleHttpException(err); - }); - }, - close: function (oldModel) { overlayService.close(); - } - }; + return $q.when(data); + }, + function (err) { + clearDirtyState($scope.content.variants); + //model.submitButtonState = "error"; + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + model.submitButtonState = "success"; + } else { + model.submitButtonState = "error"; + } + //re-map the dialog model since we've re-bound the properties + dialog.variants = $scope.content.variants; + handleHttpException(err); + }); + }, + close: function (oldModel) { + overlayService.close(); + } + }; - overlayService.open(dialog); - } - else { - showValidationNotification(); - } + overlayService.open(dialog); } else { //ensure the flags are set @@ -784,11 +785,17 @@ $scope.page.saveButtonState = "busy"; return performSave({ saveMethod: $scope.saveMethod(), - action: "save" + action: "save", + skipValidation: true }).then(function () { $scope.page.saveButtonState = "success"; }, function (err) { - $scope.page.saveButtonState = "error"; + // Because this is the "save"-action, then we actually save though there was a validation error, therefor we will show success and display the validation errors politely. + if(err && err.data && err.data.ModelState && Object.keys(err.data.ModelState).length > 0) { + $scope.page.saveButtonState = "success"; + } else { + $scope.page.saveButtonState = "error"; + } handleHttpException(err); }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index c3b8a6c148e2..231ee4e86697 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -140,11 +140,11 @@ } // This directive allows for us to run a custom $compile for the view within the repeater which allows - // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the + // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the // infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model. function EditorRepeaterDirective($http, $templateCache, $compile, angularHelper) { - function link(scope, el, attr, ctrl) { - + function link(scope, el) { + var editor = scope && scope.$parent ? scope.$parent.model : null; if (!editor) { return; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js index 03d376e36a18..e1639dde26da 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbfocuslock.directive.js @@ -40,7 +40,9 @@ function getDomNodes(){ infiniteEditorsWrapper = document.querySelector('.umb-editors'); - infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor')); + if(infiniteEditorsWrapper) { + infiniteEditors = Array.from(infiniteEditorsWrapper.querySelectorAll('.umb-editor') || []); + } } function getFocusableElements(targetElm) { @@ -84,22 +86,24 @@ var defaultFocusedElement = getAutoFocusElement(focusableElements); var lastKnownElement; - // If an inifite editor is being closed then we reset the focus to the element that triggered the the overlay + // If an infinite editor is being closed then we reset the focus to the element that triggered the the overlay if(closingEditor){ - var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; - var editorInfo = infiniteEditors[0].querySelector('.editor-info'); // If there is only one editor open, search for the "editor-info" inside it and set focus on it // This is relevant when a property editor has been selected and the editor where we selected it from // is closed taking us back to the first layer // Otherwise set it to the last element in the lastKnownFocusedElements array - if(infiniteEditors.length === 1 && editorInfo !== null){ - lastKnownElement = editorInfo; - - // Clear the array - clearLastKnownFocusedElements(); + if(infiniteEditors && infiniteEditors.length === 1){ + var editorInfo = infiniteEditors[0].querySelector('.editor-info'); + if(infiniteEditors && infiniteEditors.length === 1 && editorInfo !== null) { + lastKnownElement = editorInfo; + + // Clear the array + clearLastKnownFocusedElements(); + } } else { + var lastItemIndex = $rootScope.lastKnownFocusableElements.length - 1; lastKnownElement = $rootScope.lastKnownFocusableElements[lastItemIndex]; // Remove the last item from the array so we always set the correct lastKnowFocus for each layer @@ -149,20 +153,24 @@ } function cleanupEventHandlers() { - var activeEditor = infiniteEditors[infiniteEditors.length - 1]; - var inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); - - if(inactiveEditors.length > 0) { - for (var index = 0; index < inactiveEditors.length; index++) { - var inactiveEditor = inactiveEditors[index]; - - // Remove event handlers from inactive editors - inactiveEditor.removeEventListener('keydown', handleKeydown); + //if we're in infinite editing mode + if(infiniteEditors.length > 0) { + var activeEditor = infiniteEditors[infiniteEditors.length - 1]; + var inactiveEditors = infiniteEditors.filter(editor => editor !== activeEditor); + + if(inactiveEditors.length > 0) { + for (var index = 0; index < inactiveEditors.length; index++) { + var inactiveEditor = inactiveEditors[index]; + + // Remove event handlers from inactive editors + inactiveEditor.removeEventListener('keydown', handleKeydown); + } + } + else { + // Why is this one only begin called if there is no other infinite editors, wouldn't it make sense always to clean this up? + // Remove event handlers from the active editor + activeEditor.removeEventListener('keydown', handleKeydown); } - } - else { - // Remove event handlers from the active editor - activeEditor.removeEventListener('keydown', handleKeydown); } } @@ -173,10 +181,7 @@ // Fetch the DOM nodes we need getDomNodes(); - // Cleanup event handlers if we're in infinite editing mode - if(infiniteEditors.length > 0){ - cleanupEventHandlers(); - } + cleanupEventHandlers(); getFocusableElements(targetElm); @@ -204,17 +209,19 @@ // Make sure to disconnect the observer so we potentially don't end up with having many active ones disconnectObserver = true; - // Pass the correct editor in order to find the focusable elements - var newTarget = infiniteEditors[infiniteEditors.length - 2]; + if(infiniteEditors && infiniteEditors.length > 1) { + // Pass the correct editor in order to find the focusable elements + var newTarget = infiniteEditors[infiniteEditors.length - 2]; - if(infiniteEditors.length > 1){ - // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes - // active - closingEditor = true; + if(infiniteEditors.length > 1) { + // Setting closing till true will let us re-apply the last known focus to then opened layer that then becomes + // active + closingEditor = true; - onInit(newTarget); + onInit(newTarget); - return; + return; + } } // Clear lastKnownFocusableElements diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js new file mode 100644 index 000000000000..63681a380a3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/validwhen.directive.js @@ -0,0 +1,12 @@ +angular.module("umbraco.directives").directive('validWhen', function () { + return { + require: 'ngModel', + restrict: 'A', + link: function (scope, element, attr, ngModel) { + + attr.$observe("validWhen", function (newValue) { + ngModel.$setValidity("validWhen", newValue === "true"); + }); + } + }; +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js index f1f2cb38e84c..744e4280db74 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagecrop.directive.js @@ -6,10 +6,14 @@ **/ angular.module("umbraco.directives") .directive('umbImageCrop', - function ($timeout, cropperHelper) { + function ($timeout, $window, cropperHelper) { + + const MAX_SCALE = 4; + return { restrict: 'E', replace: true, + transclude: true, templateUrl: 'views/components/imaging/umb-image-crop.html', scope: { src: '=', @@ -17,24 +21,29 @@ angular.module("umbraco.directives") height: '@', crop: "=", center: "=", - maxSize: '@' + maxSize: '@?', + alias: '@?', + forceUpdate: '@?' }, link: function (scope, element, attrs) { + var unsubscribe = []; let sliderRef = null; - scope.width = 400; - scope.height = 320; + scope.loaded = false; + scope.width = 0; + scope.height = 0; scope.dimensions = { + element: {}, image: {}, cropper: {}, viewport: {}, - margin: 20, + margin: {}, scale: { - min: 0, - max: 3, + min: 1, + max: MAX_SCALE, current: 1 } }; @@ -45,10 +54,10 @@ angular.module("umbraco.directives") "tooltips": [false], "format": { to: function (value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); }, from: function (value) { - return parseFloat(parseFloat(value).toFixed(3)); //Math.round(value); + return parseFloat(parseFloat(value).toFixed(3)); } }, "range": { @@ -59,18 +68,23 @@ angular.module("umbraco.directives") scope.setup = function (slider) { sliderRef = slider; + updateSlider(); + }; - // Set slider handle position - sliderRef.noUiSlider.set(scope.dimensions.scale.current); + function updateSlider() { + if(sliderRef) { + // Update slider range min/max + sliderRef.noUiSlider.updateOptions({ + "range": { + "min": scope.dimensions.scale.min, + "max": scope.dimensions.scale.max + } + }); - // Update slider range min/max - sliderRef.noUiSlider.updateOptions({ - "range": { - "min": scope.dimensions.scale.min, - "max": scope.dimensions.scale.max - } - }); - }; + // Set slider handle position + sliderRef.noUiSlider.set(scope.dimensions.scale.current); + } + } scope.slide = function (values) { if (values) { @@ -84,77 +98,108 @@ angular.module("umbraco.directives") } }; + function onScroll(event) { + // cross-browser wheel delta + var delta = Math.max(-50, Math.min(50, (event.wheelDelta || -event.detail))); + + if(sliderRef) { + var currentScale =sliderRef.noUiSlider.get(); + + var newScale = Math.min(Math.max(currentScale + delta*.001*scope.dimensions.image.ratio, scope.dimensions.scale.min), scope.dimensions.scale.max); + sliderRef.noUiSlider.set(newScale); + scope.$evalAsync(() => { + scope.dimensions.scale.current = newScale; + }); + + if(event.preventDefault) { + event.preventDefault(); + } + } + } + + //live rendering of viewport and image styles - scope.style = function () { - return { - 'height': (parseInt(scope.dimensions.viewport.height, 10)) + 'px', - 'width': (parseInt(scope.dimensions.viewport.width, 10)) + 'px' - }; + function updateStyles() { + scope.maskStyle = { + 'height': (parseInt(scope.dimensions.cropper.height, 10)) + 'px', + 'width': (parseInt(scope.dimensions.cropper.width, 10)) + 'px', + 'top': (parseInt(scope.dimensions.margin.top, 10)) + 'px', + 'left': (parseInt(scope.dimensions.margin.left, 10)) + 'px' + } }; + updateStyles(); //elements var $viewport = element.find(".viewport"); var $image = element.find("img"); var $overlay = element.find(".overlay"); - var $container = element.find(".crop-container"); + + $overlay.bind("focus", function () { + $overlay.bind("DOMMouseScroll mousewheel onmousewheel", onScroll); + }); + $overlay.bind("blur", function () { + $overlay.unbind("DOMMouseScroll mousewheel onmousewheel", onScroll); + }); + //default constraints for drag n drop - var constraints = { left: { max: scope.dimensions.margin, min: scope.dimensions.margin }, top: { max: scope.dimensions.margin, min: scope.dimensions.margin } }; + var constraints = { left: { max: 0, min: 0 }, top: { max: 0, min: 0 } }; scope.constraints = constraints; //set constaints for cropping drag and drop var setConstraints = function () { - constraints.left.min = scope.dimensions.margin + scope.dimensions.cropper.width - scope.dimensions.image.width; - constraints.top.min = scope.dimensions.margin + scope.dimensions.cropper.height - scope.dimensions.image.height; + constraints.left.min = scope.dimensions.cropper.width - scope.dimensions.image.width; + constraints.top.min = scope.dimensions.cropper.height - scope.dimensions.image.height; }; - var setDimensions = function (originalImage) { - originalImage.width("auto"); - originalImage.height("auto"); + var setDimensions = function () { - var image = {}; - image.originalWidth = originalImage.width(); - image.originalHeight = originalImage.height(); - - image.width = image.originalWidth; - image.height = image.originalHeight; - image.left = originalImage[0].offsetLeft; - image.top = originalImage[0].offsetTop; - - scope.dimensions.image = image; + scope.dimensions.image.width = scope.dimensions.image.originalWidth; + scope.dimensions.image.height = scope.dimensions.image.originalHeight; //unscaled editor size - //var viewPortW = $viewport.width(); - //var viewPortH = $viewport.height(); - var _viewPortW = parseInt(scope.width, 10); - var _viewPortH = parseInt(scope.height, 10); - - //if we set a constraint we will scale it down if needed - if (scope.maxSize) { - var ratioCalculation = cropperHelper.scaleToMaxSize( - _viewPortW, - _viewPortH, - scope.maxSize); - - //so if we have a max size, override the thumb sizes - _viewPortW = ratioCalculation.width; - _viewPortH = ratioCalculation.height; - } - - scope.dimensions.viewport.width = _viewPortW + 2 * scope.dimensions.margin; - scope.dimensions.viewport.height = _viewPortH + 2 * scope.dimensions.margin; - scope.dimensions.cropper.width = _viewPortW; // scope.dimensions.viewport.width - 2 * scope.dimensions.margin; - scope.dimensions.cropper.height = _viewPortH; // scope.dimensions.viewport.height - 2 * scope.dimensions.margin; + var _cropW = parseInt(scope.width, 10); + var _cropH = parseInt(scope.height, 10); + + var ratioCalculation = cropperHelper.scaleToMaxSize( + _cropW, + _cropH, + scope.dimensions.viewport.width - 40, + scope.dimensions.viewport.height - 40); + + //so if we have a max size, override the thumb sizes + _cropW = ratioCalculation.width; + _cropH = ratioCalculation.height; + + // set margins: + scope.dimensions.margin.left = (scope.dimensions.viewport.width - _cropW) * 0.5; + scope.dimensions.margin.top = (scope.dimensions.viewport.height - _cropH) * 0.5; + + scope.dimensions.cropper.width = _cropW; + scope.dimensions.cropper.height = _cropH; + updateStyles(); }; //resize to a given ratio var resizeImageToScale = function (ratio) { - //do stuff - var size = cropperHelper.calculateSizeToRatio(scope.dimensions.image.originalWidth, scope.dimensions.image.originalHeight, ratio); - scope.dimensions.image.width = size.width; - scope.dimensions.image.height = size.height; + + var prevWidth = scope.dimensions.image.width; + var prevHeight = scope.dimensions.image.height; + + scope.dimensions.image.width = scope.dimensions.image.originalWidth * ratio; + scope.dimensions.image.height = scope.dimensions.image.originalHeight * ratio; + + var difW = (scope.dimensions.image.width - prevWidth); + var difH = (scope.dimensions.image.height - prevHeight); + + // normalized focus point: + var focusNormX = (-scope.dimensions.image.left + scope.dimensions.cropper.width*.5) / prevWidth; + var focusNormY = (-scope.dimensions.image.top + scope.dimensions.cropper.height*.5) / prevHeight; + + scope.dimensions.image.left = scope.dimensions.image.left - difW * focusNormX; + scope.dimensions.image.top = scope.dimensions.image.top - difH * focusNormY; setConstraints(); validatePosition(scope.dimensions.image.left, scope.dimensions.image.top); @@ -163,10 +208,10 @@ angular.module("umbraco.directives") //resize the image to a predefined crop coordinate var resizeImageToCrop = function () { scope.dimensions.image = cropperHelper.convertToStyle( - scope.crop, + runtimeCrop, { width: scope.dimensions.image.originalWidth, height: scope.dimensions.image.originalHeight }, scope.dimensions.cropper, - scope.dimensions.margin); + 0); var ratioCalculation = cropperHelper.calculateAspectRatioFit( scope.dimensions.image.originalWidth, @@ -178,25 +223,19 @@ angular.module("umbraco.directives") scope.dimensions.scale.current = scope.dimensions.image.ratio; // Update min and max based on original width/height + // Here we update the slider to use the scala of the current setup, i dont know why its made in this way but this is how it is. scope.dimensions.scale.min = ratioCalculation.ratio; - scope.dimensions.scale.max = 2; + // TODO: Investigate wether we can limit users to not scale bigger than the amount of pixels in the source: + //scope.dimensions.scale.max = ratioCalculation.ratio * Math.min(MAX_SCALE, scope.dimensions.image.originalWidth/scope.dimensions.cropper.width); + scope.dimensions.scale.max = ratioCalculation.ratio * MAX_SCALE; + + updateSlider(); }; var validatePosition = function (left, top) { - if (left > constraints.left.max) { - left = constraints.left.max; - } - if (left <= constraints.left.min) { - left = constraints.left.min; - } - - if (top > constraints.top.max) { - top = constraints.top.max; - } - if (top <= constraints.top.min) { - top = constraints.top.min; - } + left = Math.min(Math.max(left, constraints.left.min), constraints.left.max); + top = Math.min(Math.max(top, constraints.top.min), constraints.top.max); if (scope.dimensions.image.left !== left) { scope.dimensions.image.left = left; @@ -209,36 +248,54 @@ angular.module("umbraco.directives") //sets scope.crop to the recalculated % based crop - var calculateCropBox = function () { - scope.crop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, scope.dimensions.margin); + function calculateCropBox() { + runtimeCrop = cropperHelper.pixelsToCoordinates(scope.dimensions.image, scope.dimensions.cropper.width, scope.dimensions.cropper.height, 0); }; + function saveCropBox() { + scope.crop = Utilities.copy(runtimeCrop); + } //Drag and drop positioning, using jquery ui draggable - var onStartDragPosition, top, left; + //var onStartDragPosition, top, left; + var dragStartPosition = {}; $overlay.draggable({ + start: function (event, ui) { + dragStartPosition.left = scope.dimensions.image.left; + dragStartPosition.top = scope.dimensions.image.top; + }, drag: function (event, ui) { scope.$apply(function () { - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); }); }, stop: function (event, ui) { scope.$apply(function () { //make sure that every validates one more time... - validatePosition(ui.position.left, ui.position.top); + validatePosition(dragStartPosition.left + (ui.position.left - ui.originalPosition.left), dragStartPosition.top + (ui.position.top - ui.originalPosition.top)); calculateCropBox(); - scope.dimensions.image.rnd = Math.random(); + saveCropBox(); }); } }); - var init = function (image) { - scope.loaded = false; + var runtimeCrop; + var init = function () { + + // store original size: + scope.dimensions.image.originalWidth = $image.width(); + scope.dimensions.image.originalHeight = $image.height(); + + // runtime Crop, should not be saved until we have interactions: + runtimeCrop = Utilities.copy(scope.crop); + + onViewportSizeChanged(); - //set dimensions on image, viewport, cropper etc - setDimensions(image); + scope.loaded = true; + }; + function setCrop() { //create a default crop if we haven't got one already var createDefaultCrop = !scope.crop; if (createDefaultCrop) { @@ -275,41 +332,67 @@ angular.module("umbraco.directives") resizeImageToCrop(); } } + } + + + function onViewportSizeChanged() { + scope.dimensions.viewport.width = $viewport.width(); + scope.dimensions.viewport.height = $viewport.height(); - //sets constaints for the cropper + setDimensions(); + setCrop(); setConstraints(); - scope.loaded = true; - }; + } // Watchers - scope.$watchCollection('[width, height]', function (newValues, oldValues) { + unsubscribe.push(scope.$watchCollection('[width, height, alias, forceUpdate]', function (newValues, oldValues) { // We have to reinit the whole thing if // one of the external params changes if (newValues !== oldValues) { - setDimensions($image); + runtimeCrop = Utilities.copy(scope.crop); + setDimensions(); + setCrop(); setConstraints(); } - }); + })); - var throttledResizing = _.throttle(function () { + var throttledScale = _.throttle(() => scope.$evalAsync(() => { resizeImageToScale(scope.dimensions.scale.current); calculateCropBox(); - }, 15); + saveCropBox(); + }), 16); // Happens when we change the scale - scope.$watch("dimensions.scale.current", function (newValue, oldValue) { + unsubscribe.push(scope.$watch("dimensions.scale.current", function (newValue, oldValue) { if (scope.loaded) { - throttledResizing(); + throttledScale(); } - }); + })); + // Init + + //if we have a max-size we will use it, to keep this backwards compatible. + // I dont see this max size begin usefull, as we should aim for responsive UI. + if (scope.maxSize) { + element.css("max-width", parseInt(scope.maxSize, 10) + "px"); + element.css("max-height", parseInt(scope.maxSize, 10) + "px"); + } + $image.on("load", function () { $timeout(function () { - init($image); + init(); }); }); + + windowResizeListener.register(onViewportSizeChanged); + + scope.$on('$destroy', function () { + $image.prop("src", ""); + windowResizeListener.unregister(onViewportSizeChanged); + unsubscribe.forEach(u => u()); + }) } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js index fd9a236f87f3..277848811b53 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js @@ -13,8 +13,8 @@ top: 0 }; - var htmlImage = null; //DOM element reference - var htmlOverlay = null; //DOM element reference + var imageElement = null; //DOM element reference + var focalPointElement = null; //DOM element reference var draggable = null; vm.loaded = false; @@ -22,33 +22,33 @@ vm.$onChanges = onChanges; vm.$postLink = postLink; vm.$onDestroy = onDestroy; - vm.style = style; + vm.style = {}; + vm.overlayStyle = {}; vm.setFocalPoint = setFocalPoint; /** Sets the css style for the Dot */ - function style() { - - if (vm.dimensions.width <= 0 || vm.dimensions.height <= 0) { - //this initializes the dimensions since when the image element first loads - //there will be zero dimensions - setDimensions(); - } - - return { + function updateStyle() { + vm.style = { 'top': vm.dimensions.top + 'px', 'left': vm.dimensions.left + 'px' }; + vm.overlayStyle = { + 'width': vm.dimensions.width + 'px', + 'height': vm.dimensions.height + 'px' + }; + }; - function setFocalPoint (event) { + function setFocalPoint(event) { $scope.$emit("imageFocalPointStart"); - var offsetX = event.offsetX - 10; - var offsetY = event.offsetY - 10; + // We do this to get the right position, no matter the focalPoint was clicked. + var viewportPosition = imageElement[0].getBoundingClientRect(); + var offsetX = event.clientX - viewportPosition.left; + var offsetY = event.clientY - viewportPosition.top; calculateGravity(offsetX, offsetY); - - lazyEndEvent(); + $scope.$emit("imageFocalPointStop"); }; /** Initializes the component */ @@ -61,33 +61,30 @@ /** Called when the component has linked everything and the DOM is available */ function postLink() { //elements - htmlImage = $element.find("img"); - htmlOverlay = $element.find(".overlay"); + imageElement = $element.find("img"); + focalPointElement = $element.find(".focalPoint"); //Drag and drop positioning, using jquery ui draggable - draggable = htmlOverlay.draggable({ + draggable = focalPointElement.draggable({ containment: "parent", start: function () { - $scope.$apply(function () { - $scope.$emit("imageFocalPointStart"); - }); + $scope.$emit("imageFocalPointStart"); }, - stop: function () { - $scope.$apply(function () { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; - calculateGravity(offsetX, offsetY); - }); - - lazyEndEvent(); + stop: function (event, ui) { + + var offsetX = ui.position.left; + var offsetY = ui.position.top; + + $scope.$evalAsync(calculateGravity(offsetX, offsetY)); + + $scope.$emit("imageFocalPointStop"); + } }); - $(window).on('resize.umbImageGravity', function () { - $scope.$apply(function () { - resized(); - }); - }); + window.addEventListener('resize.umbImageGravity', onResizeHandler); + window.addEventListener('resize', onResizeHandler); + //if any ancestor directive emits this event, we need to resize $scope.$on("editors.content.splitViewChanged", function () { @@ -95,12 +92,12 @@ }); //listen for the image DOM element loading - htmlImage.on("load", function () { + imageElement.on("load", function () { $timeout(function () { vm.isCroppable = true; vm.hasDimensions = true; - + if (vm.src) { if (vm.src.endsWith(".svg")) { vm.isCroppable = false; @@ -117,6 +114,8 @@ } setDimensions(); + updateStyle(); + vm.loaded = true; if (vm.onImageLoaded) { vm.onImageLoaded({ @@ -129,16 +128,19 @@ } function onDestroy() { - $(window).off('resize.umbImageGravity'); - if (htmlOverlay) { + window.removeEventListener('resize.umbImageGravity', onResizeHandler); + window.removeEventListener('resize', onResizeHandler); + /* + if (focalPointElement) { // TODO: This should be destroyed but this will throw an exception: // "cannot call methods on draggable prior to initialization; attempted to call method 'destroy'" // I've tried lots of things and cannot get this to work, we weren't destroying before so hopefully // there's no mem leaks? - //htmlOverlay.draggable("destroy"); + focalPointElement.draggable("destroy"); } - if (htmlImage) { - htmlImage.off("load"); + */ + if (imageElement) { + imageElement.off("load"); } } @@ -146,14 +148,21 @@ function resized() { $timeout(function () { setDimensions(); + updateStyle(); }); + /* // Make sure we can find the offset values for the overlay(dot) before calculating // fixes issue with resize event when printing the page (ex. hitting ctrl+p inside the rte) - if (htmlOverlay.is(':visible')) { - var offsetX = htmlOverlay[0].offsetLeft; - var offsetY = htmlOverlay[0].offsetTop; + if (focalPointElement.is(':visible')) { + var offsetX = focalPointElement[0].offsetLeft; + var offsetY = focalPointElement[0].offsetTop; calculateGravity(offsetX, offsetY); } + */ + } + + function onResizeHandler() { + $scope.$evalAsync(resized); } /** Watches the one way binding changes */ @@ -163,17 +172,18 @@ && !Utilities.equals(changes.center.currentValue, changes.center.previousValue)) { //when center changes update the dimensions setDimensions(); + updateStyle(); } } /** Sets the width/height/left/top dimentions based on the image size and the "center" value */ function setDimensions() { - if (vm.isCroppable && htmlImage && vm.center) { - vm.dimensions.width = htmlImage.width(); - vm.dimensions.height = htmlImage.height(); - vm.dimensions.left = vm.center.left * vm.dimensions.width - 10; - vm.dimensions.top = vm.center.top * vm.dimensions.height - 10; + if (vm.isCroppable && imageElement && vm.center) { + vm.dimensions.width = imageElement.width(); + vm.dimensions.height = imageElement.height(); + vm.dimensions.left = vm.center.left * vm.dimensions.width; + vm.dimensions.top = vm.center.top * vm.dimensions.height; } return vm.dimensions.width; @@ -185,31 +195,22 @@ * @param {any} offsetY */ function calculateGravity(offsetX, offsetY) { - vm.onValueChanged({ - left: (offsetX + 10) / vm.dimensions.width, - top: (offsetY + 10) / vm.dimensions.height + left: Math.min(Math.max(offsetX, 0), vm.dimensions.width) / vm.dimensions.width, + top: Math.min(Math.max(offsetY, 0), vm.dimensions.height) / vm.dimensions.height }); - - //vm.center.left = (offsetX + 10) / scope.dimensions.width; - //vm.center.top = (offsetY + 10) / scope.dimensions.height; }; - var lazyEndEvent = _.debounce(function () { - $scope.$apply(function () { - $scope.$emit("imageFocalPointStop"); - }); - }, 2000); - } var umbImageGravityComponent = { templateUrl: 'views/components/imaging/umb-image-gravity.html', bindings: { src: "<", - center: "<", + center: "<", onImageLoaded: "&?", - onValueChanged: "&" + onValueChanged: "&", + disableFocalPoint: " 0; + + if (data.items.length > 0) { + vm.hasContentReferences = data.items.length > 0; + activateWarning(); + } }); } @@ -97,7 +129,11 @@ return trackedReferencesResource.getPagedReferences(vm.id, vm.mediaOptions) .then(function (data) { vm.mediaReferences = data; - vm.hasMediaReferences = data.items.length > 0; + + if (data.items.length > 0) { + vm.hasMediaReferences = data.items.length > 0; + activateWarning(); + } }); } @@ -105,14 +141,22 @@ return trackedReferencesResource.getPagedReferences(vm.id, vm.memberOptions) .then(function (data) { vm.memberReferences = data; - vm.hasMemberReferences = data.items.length > 0; + + if (data.items.length > 0) { + vm.hasMemberReferences = data.items.length > 0; + activateWarning(); + } }); } function checkContentDescendantsUsage() { return trackedReferencesResource.hasReferencesInDescendants(vm.id, vm.contentOptions.entityType) - .then(function (data) { - vm.hasContentReferencesInDescendants = data; + .then(function (data) { + vm.hasContentReferencesInDescendants = data; + + if (vm.hasContentReferencesInDescendants) { + activateWarning(); + } }); } @@ -120,6 +164,10 @@ return trackedReferencesResource.hasReferencesInDescendants(vm.id, vm.mediaOptions.entityType) .then(function (data) { vm.hasMediaReferencesInDescendants = data; + + if (vm.hasMediaReferencesInDescendants) { + activateWarning(); + } }); } @@ -127,8 +175,18 @@ return trackedReferencesResource.hasReferencesInDescendants(vm.id, vm.memberOptions.entityType) .then(function (data) { vm.hasMemberReferencesInDescendants = data; + + if (vm.hasMemberReferencesInDescendants) { + activateWarning(); + } }); } + + function activateWarning() { + if (vm.onWarning) { + vm.onWarning(); + } + } } })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js new file mode 100644 index 000000000000..537a32900ecb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/references/umbtrackedreferencestable.component.js @@ -0,0 +1,32 @@ + +(function () { + 'use strict'; + + angular + .module('umbraco.directives') + .component('umbTrackedReferencesTable', { + transclude: true, + templateUrl: 'views/components/references/umb-tracked-references-table.html', + controller: UmbTrackedReferencesTableController, + controllerAs: 'vm', + bindings: { + pageNumber: "<", + totalPages : "<", + title: "<", + items : "<", + onPageChanged: "&" + } + }); + + function UmbTrackedReferencesTableController() + { + var vm = this; + + vm.changePageNumber = changePageNumber; + + function changePageNumber(pageNumber) { + vm.onPageChanged({ 'pageNumber' : pageNumber }); + } + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index 241f1e80e8d2..cc480efea505 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -118,9 +118,9 @@ Use this directive to generate a thumbnail grid of media items. var item = scope.items[i]; setItemData(item); setOriginalSize(item, itemMaxHeight); - + item.selectable = getSelectableState(item); - + // remove non images when onlyImages is set to true if (scope.onlyImages === "true" && !item.isFolder && !item.thumbnail){ scope.items.splice(i, 1); @@ -141,7 +141,7 @@ Use this directive to generate a thumbnail grid of media items. } } - + if (scope.items.length > 0) { setFlexValues(scope.items); } @@ -188,7 +188,7 @@ Use this directive to generate a thumbnail grid of media items. } } } - + /** * Returns wether a item should be selectable or not. */ @@ -203,9 +203,9 @@ Use this directive to generate a thumbnail grid of media items. } else { return scope.onlyFolders !== "true"; } - + return false; - + } function setOriginalSize(item, maxHeight) { @@ -255,7 +255,7 @@ Use this directive to generate a thumbnail grid of media items. } } - + function setFlexValues(mediaItems) { var flexSortArray = mediaItems; @@ -292,8 +292,11 @@ Use this directive to generate a thumbnail grid of media items. mediaItem.flexStyle = flexStyle; } } - + scope.clickItem = function(item, $event, $index) { + if (item.isFolder === true && item.filtered) { + scope.clickItemName(item, $event, $index); + } if (scope.onClick) { scope.onClick(item, $event, $index); $event.stopPropagation(); @@ -312,7 +315,7 @@ Use this directive to generate a thumbnail grid of media items. scope.onDetailsHover(item, $event, hover); } }; - + var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { if (Utilities.isArray(newValue)) { activate(); @@ -333,7 +336,7 @@ Use this directive to generate a thumbnail grid of media items. //change sort scope.setSort = function (col) { if (scope.sortColumn === col) { - scope.sortReverse = !scope.sortReverse; + scope.sortReverse = !scope.sortReverse; } else { scope.sortColumn = col; @@ -345,9 +348,9 @@ Use this directive to generate a thumbnail grid of media items. } } scope.sortDirection = scope.sortReverse ? "desc" : "asc"; - + } - // sort function + // sort function scope.sortBy = function (item) { if (scope.sortColumn === "updateDate") { return [-item['isFolder'],item['updateDate']]; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js index 783cd7f90ade..f7b634a71064 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js @@ -58,6 +58,9 @@ entityResource.getPagedChildren(miniListView.node.id, scope.entityType, miniListView.pagination) .then(function (data) { + if (!data.items) { + data.items = []; + } if (scope.onItemsLoaded) { scope.onItemsLoaded({items: data.items}); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js index 7f405eb28c24..79dfee059ee6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js @@ -20,7 +20,7 @@ TODO angular.module("umbraco.directives") .directive('umbFileDropzone', - function ($timeout, Upload, localizationService, umbRequestHelper, overlayService) { + function ($timeout, Upload, localizationService, umbRequestHelper, overlayService, mediaHelper, mediaTypeHelper) { return { restrict: 'E', replace: true, @@ -88,21 +88,12 @@ angular.module("umbraco.directives") }); scope.queue = []; } - // One allowed type - if (scope.acceptedMediatypes && scope.acceptedMediatypes.length === 1) { - // Standard setup - set alias to auto select to let the server best decide which media type to use - if (scope.acceptedMediatypes[0].alias === 'Image') { - scope.contentTypeAlias = "umbracoAutoSelect"; - } else { - scope.contentTypeAlias = scope.acceptedMediatypes[0].alias; - } + // If we have Accepted Media Types, we will ask to choose Media Type, if Choose Media Type returns false, it only had one choice and therefor no reason to + if (scope.acceptedMediatypes && _requestChooseMediaTypeDialog() === false) { + scope.contentTypeAlias = "umbracoAutoSelect"; _processQueueItem(); } - // More than one, open dialog - if (scope.acceptedMediatypes && scope.acceptedMediatypes.length > 1) { - _chooseMediaType(); - } } } @@ -146,8 +137,8 @@ angular.module("umbraco.directives") // set percentage property on file file.uploadProgress = progressPercentage; // set uploading status on file - file.uploadStatus = "uploading"; - } + file.uploadStatus = "uploading"; + } }) .success(function(data, status, headers, config) { if (data.notifications && data.notifications.length > 0) { @@ -195,35 +186,61 @@ angular.module("umbraco.directives") }); } - function _chooseMediaType() { + function _requestChooseMediaTypeDialog() { - const dialog = { - view: "itempicker", - filter: scope.acceptedMediatypes.length > 15, - availableItems: scope.acceptedMediatypes, - submit: function (model) { - scope.contentTypeAlias = model.selectedItem.alias; - _processQueueItem(); + if (scope.acceptedMediatypes.length === 1) { + // if only one accepted type, then we wont ask to choose. + return false; + } - overlayService.close(); - }, - close: function () { + var uploadFileExtensions = scope.queue.map(file => mediaHelper.getFileExtension(file.name)); - scope.queue.map(function (file) { - file.uploadStatus = "error"; - file.serverErrorMessage = "Cannot upload this file, no mediatype selected"; - scope.rejected.push(file); - }); - scope.queue = []; + var filteredMediaTypes = mediaTypeHelper.getTypeAcceptingFileExtensions(scope.acceptedMediatypes, uploadFileExtensions); - overlayService.close(); - } - }; + var mediaTypesNotFile = filteredMediaTypes.filter(mediaType => mediaType.alias !== "File"); - localizationService.localize("defaultdialogs_selectMediaType").then(value => { - dialog.title = value; + if (mediaTypesNotFile.length <= 1) { + // if only one or less accepted types when we have filtered type 'file' out, then we wont ask to choose. + return false; + } + + + localizationService.localizeMany(["defaultdialogs_selectMediaType", "mediaType_autoPickMediaType"]).then(function (translations) { + + filteredMediaTypes.push({ + alias: "umbracoAutoSelect", + name: translations[1], + icon: "icon-wand" + }); + + const dialog = { + view: "itempicker", + filter: filteredMediaTypes.length > 8, + availableItems: filteredMediaTypes, + submit: function (model) { + scope.contentTypeAlias = model.selectedItem.alias; + _processQueueItem(); + + overlayService.close(); + }, + close: function () { + + scope.queue.map(function (file) { + file.uploadStatus = "error"; + file.serverErrorMessage = "No files uploaded, no mediatype selected"; + scope.rejected.push(file); + }); + scope.queue = []; + + overlayService.close(); + } + }; + + dialog.title = translations[0]; overlayService.open(dialog); }); + + return true;// yes, we did open the choose-media dialog, therefor we return true. } scope.handleFiles = function(files, event) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js index db1e38adc626..5492fee1a0ab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbpropertyfileupload.directive.js @@ -85,7 +85,7 @@ /** Called when the component has linked all elements, this is when the form controller is available */ function postLink() { - + } function initialize() { @@ -186,7 +186,7 @@ }); } } - + } } @@ -325,7 +325,8 @@ */ onFilesChanged: "&", onInit: "&", - required: "=" + required: "=", + acceptFileExt: ""; + return { restrict: "E", scope: { - rebuild: "=" + rebuild: "=", + acceptFileExt: "", - link: function (scope, el, attrs) { + template: "
"+innerTemplate+"
", + link: function (scope, el) { scope.$watch("rebuild", function (newVal, oldVal) { if (newVal && newVal !== oldVal) { //recompile it! - el.html(""); + el.html(innerTemplate); $compile(el.contents())(scope); } }); @@ -30,4 +35,4 @@ function umbSingleFileUpload($compile) { }; } -angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); \ No newline at end of file +angular.module('umbraco.directives').directive("umbSingleFileUpload", umbSingleFileUpload); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index c7894da1719c..d15ad6af518c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -15,6 +15,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) { var SHOW_VALIDATION_CLASS_NAME = "show-validation"; + var SHOW_VALIDATION_Type_CLASS_NAME = "show-validation-type-"; var SAVING_EVENT_NAME = "formSubmitting"; var SAVED_EVENT_NAME = "formSubmitted"; @@ -25,7 +26,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location function ValFormManagerController($scope) { //This exposes an API for direct use with this directive - // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and + // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and // because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is // an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager // in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing) @@ -44,9 +45,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location this.isShowingValidation = () => $scope.showValidation === true; - this.notify = function () { - notify($scope); - } + this.getValidationMessageType = () => $scope.valMsgType; + + this.notify = notify; this.isValid = function () { return !$scope.formCtrl.$invalid; @@ -96,6 +97,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1); var subView = ctrls.length > 1 ? ctrls[2] : null; var labels = {}; + var valMsgType = 2;// error var labelKeys = [ "prompt_unsavedChanges", @@ -111,8 +113,48 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location labels.stayButton = values[3]; }); - //watch the list of validation errors to notify the application of any validation changes - scope.$watch(() => angularHelper.countAllFormErrors(formCtrl), + var lastValidationMessageType = null; + function setValidationMessageType(type) { + + removeValidationMessageType(); + scope.valMsgType = type; + + // overall a copy of message types from notifications.service: + var postfix = ""; + switch(type) { + case 0: + //save + break; + case 1: + //info + postfix = "info"; + break; + case 2: + //error + postfix = "error"; + break; + case 3: + //success + postfix = "success"; + break; + case 4: + //warning + postfix = "warning"; + break; + } + var cssClass = SHOW_VALIDATION_Type_CLASS_NAME+postfix; + element.addClass(cssClass); + lastValidationMessageType = cssClass; + } + function removeValidationMessageType() { + if(lastValidationMessageType) { + element.removeClass(lastValidationMessageType); + lastValidationMessageType = null; + } + } + + // watch the list of validation errors to notify the application of any validation changes + scope.$watch(() => formCtrl.$invalid, function (e) { notify(scope); @@ -139,6 +181,8 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.isShowingValidation())) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + var parentValMsgType = parentFormMgr ? parentFormMgr.getValidationMessageType() : 2; + setValidationMessageType(parentValMsgType || 2); notifySubView(); } @@ -146,8 +190,16 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //listen for the forms saving event unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) { + + var messageType = 2;//error + switch (args.action) { + case "save": + messageType = 4;//warning + break; + } element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; + setValidationMessageType(messageType); notifySubView(); //set the flag so we can check to see if we should display the error. isSavingNewItem = $routeParams.create; @@ -157,6 +209,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) { //remove validation class element.removeClass(SHOW_VALIDATION_CLASS_NAME); + removeValidationMessageType(); scope.showValidation = false; notifySubView(); })); 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 26c0403f8584..f9e2af584f4c 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 @@ -177,8 +177,9 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel watcher = scope.$watchCollection( () => formCtrl, function (updatedFormController) { - var ngModels = []; - collectAllNgModelControllersRecursively(updatedFormController.$getControls(), ngModels); + let childControls = updatedFormController.$getControls(); + let ngModels = []; + collectAllNgModelControllersRecursively(childControls, ngModels); ngModels.forEach(x => { if (!x.$validators.serverValidityResetter) { x.$validators.serverValidityResetter = resetServerValidityValidator(x); @@ -201,7 +202,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel hasError = false; formCtrl.$setValidity('valPropertyMsg', true); scope.errorMsg = ""; - + } // This deals with client side validation changes and is executed anytime validators change on the containing @@ -264,6 +265,8 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel //listen for form validation changes. //The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then // subscribing to this single watch. + // TODO: Really? Since valFormManager is watching a countof all errors which is more overhead than watching formCtrl.$invalid + // and there's a TODO there that it should just watch formCtrl.$invalid valFormManager.onValidationStatusChanged(function (evt, args) { checkValidationStatus(); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js index 5f8600c8c0fe..b07ab55436f8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -2,8 +2,8 @@ * @ngdoc directive * @name umbraco.directives.directive:valServerMatch * @restrict A - * @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data - * contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against + * @description A custom validator applied to a form/ng-form within an umbProperty that validates server side validation data + * contained within the serverValidationManager. The data can be matched on "exact", "prefix", "suffix" or "contains" matches against * a property validation key. The attribute value can be in multiple value types: * - STRING = The property validation key to have an exact match on. If matched, then the form will have a valServerMatch validator applied. * - OBJECT = A dictionary where the key is the match type: "contains", "prefix", "suffix" and the value is either: diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js index 7d976b3b9ecc..0da3c6e54fd9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/trackedreferences.resource.js @@ -40,7 +40,7 @@ function trackedReferencesResource($q, $http, umbRequestHelper) { getPagedReferences: function (id, args) { var defaults = { - pageSize: 25, + pageSize: 10, pageNumber: 1, entityType: "DOCUMENT" }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 83fd3d08c233..901e5fa93cf7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -17,6 +17,7 @@ function clipboardService($window, notificationsService, eventsService, localSto TYPES.ELEMENT_TYPE = "elementType"; TYPES.BLOCK = "block"; TYPES.RAW = "raw"; + TYPES.MEDIA = "media"; var clearPropertyResolvers = {}; var pastePropertyResolvers = {}; @@ -70,6 +71,9 @@ function clipboardService($window, notificationsService, eventsService, localSto propMethod(data[p], TYPES.RAW); } } + clipboardTypeResolvers[TYPES.MEDIA] = function(data, propMethod) { + // no resolving needed for this type currently. + } var STORAGE_KEY = "umbClipboardService"; @@ -147,6 +151,8 @@ function clipboardService($window, notificationsService, eventsService, localSto return entry.type === type && ( + allowedAliases === null + || (entry.alias && allowedAliases.filter(allowedAlias => allowedAlias === entry.alias).length > 0) || (entry.aliases && entry.aliases.filter(entryAlias => allowedAliases.filter(allowedAlias => allowedAlias === entryAlias).length > 0).length === entry.aliases.length) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 8524b960c68d..67e466af356f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -32,7 +32,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt return true; } - function showNotificationsForModelsState(ms) { + function showNotificationsForModelsState(ms, messageType) { + messageType = messageType || 2; for (const [key, value] of Object.entries(ms)) { var errorMsg = value[0]; @@ -42,12 +43,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt var idsToErrors = serverValidationManager.parseComplexEditorError(errorMsg, ""); idsToErrors.forEach(x => { if (x.modelState) { - showNotificationsForModelsState(x.modelState); + showNotificationsForModelsState(x.modelState, messageType); } }); } else if (value[0]) { - notificationsService.error("Validation", value[0]); + //notificationsService.error("Validation", value[0]); + console.log({type:messageType, header:"Validation", message:value[0]}) + notificationsService.showNotification({type:messageType, header:"Validation", message:value[0]}) } } } @@ -93,7 +96,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //we will use the default one for content if not specified var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; - if (formHelper.submitForm({ scope: args.scope, action: args.action })) { + var formSubmitOptions = { scope: args.scope, action: args.action }; + if(args.skipValidation === true) { + formSubmitOptions.skipValidation = true; + formSubmitOptions.keepServerValidation = true; + } + if (formHelper.submitForm(formSubmitOptions)) { return args.saveMethod(args.content, args.create, fileManager.getFiles(), args.showNotifications) .then(function (data) { @@ -124,6 +132,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt showNotifications: args.showNotifications, softRedirect: args.softRedirect, err: err, + action: args.action, rebindCallback: function () { // if the error contains data, we want to map that back as we want to continue editing this save. Especially important when the content is new as the returned data will contain ID etc. if(err.data) { @@ -639,9 +648,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //wire up the server validation errs formHelper.handleServerValidation(args.err.data.ModelState); + var messageType = 2;//error + if (args.action === "save") { + messageType = 4;//warning + } + //add model state errors to notifications if (args.showNotifications) { - showNotificationsForModelsState(args.err.data.ModelState); + showNotificationsForModelsState(args.err.data.ModelState, messageType); } if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) { @@ -775,10 +789,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt */ sortVariants: function (a, b) { const statesOrder = {'PublishedPendingChanges':1, 'Published': 1, 'Draft': 2, 'NotCreated': 3}; - const compareDefault = (a,b) => (!a.language.isDefault ? 1 : -1) - (!b.language.isDefault ? 1 : -1); + const compareDefault = (a,b) => (a.language && a.language.isDefault ? -1 : 1) - (b.language && b.language.isDefault ? -1 : 1); // Make sure mandatory variants goes on top, unless they are published, cause then they already goes to the top and then we want to mix them with other published variants. - const compareMandatory = (a,b) => (a.state === 'PublishedPendingChanges' || a.state === 'Published') ? 0 : (!a.language.isMandatory ? 1 : -1) - (!b.language.isMandatory ? 1 : -1); + const compareMandatory = (a,b) => (a.state === 'PublishedPendingChanges' || a.state === 'Published') ? 0 : (a.language && a.language.isMandatory ? -1 : 1) - (b.language && b.language.isMandatory ? -1 : 1); const compareState = (a, b) => (statesOrder[a.state] || 99) - (statesOrder[b.state] || 99); const compareName = (a, b) => a.displayName.localeCompare(b.displayName); @@ -799,17 +813,18 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt */ getSortedVariantsAndSegments: function (variantsAndSegments) { const sortedVariants = variantsAndSegments.filter(variant => !variant.segment).sort(this.sortVariants); - let segments = variantsAndSegments.filter(variant => variant.segment); + let variantsWithSegments = variantsAndSegments.filter(variant => variant.segment); let sortedAvailableVariants = []; sortedVariants.forEach((variant) => { - const sortedMatchedSegments = segments.filter(segment => segment.language.culture === variant.language.culture).sort(this.sortVariants); - segments = segments.filter(segment => segment.language.culture !== variant.language.culture); + const sortedMatchedSegments = variantsWithSegments.filter(segment => segment.language && variant.language && segment.language.culture === variant.language.culture).sort(this.sortVariants); + // remove variants for this culture + variantsWithSegments = variantsWithSegments.filter(segment => !segment.language || segment.language && variant.language && segment.language.culture !== variant.language.culture); sortedAvailableVariants = [...sortedAvailableVariants, ...[variant], ...sortedMatchedSegments]; }) - // if we have segments without a parent language variant we need to add the remaining segments to the array - sortedAvailableVariants = [...sortedAvailableVariants, ...segments.sort(this.sortVariants)]; + // if we have segments without a parent language variant we need to add the remaining variantsWithSegments to the array + sortedAvailableVariants = [...sortedAvailableVariants, ...variantsWithSegments.sort(this.sortVariants)]; return sortedAvailableVariants; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js index 256a1461db3f..1f860f237caa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/cropperhelper.service.js @@ -44,24 +44,23 @@ function cropperHelper(umbRequestHelper, $http) { return { width:srcWidth*ratio, height:srcHeight*ratio, ratio: ratio}; }, - scaleToMaxSize : function(srcWidth, srcHeight, maxSize) { - - var retVal = {height: srcHeight, width: srcWidth}; + scaleToMaxSize : function(srcWidth, srcHeight, maxWidth, maxHeight) { - if(srcWidth > maxSize ||srcHeight > maxSize){ - var ratio = [maxSize / srcWidth, maxSize / srcHeight ]; - ratio = Math.min(ratio[0], ratio[1]); - - retVal.height = srcHeight * ratio; - retVal.width = srcWidth * ratio; - } - - return retVal; + // fallback to maxHeight: + maxHeight = maxHeight || maxWidth; + + // get smallest ratio, if ratio exceeds 1 we will not scale(hence we parse 1 as the maximum allowed ratio) + var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight, 1); + + return { + width: srcWidth * ratio, + height:srcHeight * ratio + }; }, //returns a ng-style object with top,left,width,height pixel measurements //expects {left,right,top,bottom} - {width,height}, {width,height}, int - //offset is just to push the image position a number of pixels from top,left + //offset is just to push the image position a number of pixels from top,left convertToStyle : function(coordinates, originalSize, viewPort, offset){ var coordinates_px = service.coordinatesToPixels(coordinates, originalSize, offset); @@ -85,14 +84,14 @@ function cropperHelper(umbRequestHelper, $http) { return style; }, - + coordinatesToPixels : function(coordinates, originalSize, offset){ var coordinates_px = { x1: Math.floor(coordinates.x1 * originalSize.width), y1: Math.floor(coordinates.y1 * originalSize.height), x2: Math.floor(coordinates.x2 * originalSize.width), - y2: Math.floor(coordinates.y2 * originalSize.height) + y2: Math.floor(coordinates.y2 * originalSize.height) }; return coordinates_px; @@ -106,25 +105,18 @@ function cropperHelper(umbRequestHelper, $http) { var x2_px = image.width - (x1_px + width); var y2_px = image.height - (y1_px + height); - //crop coordinates in % var crop = {}; - crop.x1 = x1_px / image.width; - crop.y1 = y1_px / image.height; - crop.x2 = x2_px / image.width; - crop.y2 = y2_px / image.height; - - for(var coord in crop){ - if(crop[coord] < 0){ - crop[coord] = 0; - } - } + crop.x1 = Math.max(x1_px / image.width, 0); + crop.y1 = Math.max(y1_px / image.height, 0); + crop.x2 = Math.max(x2_px / image.width, 0); + crop.y2 = Math.max(y2_px / image.height, 0); return crop; }, alignToCoordinates : function(image, center, viewport){ - + var min_left = (image.width) - (viewport.width); var min_top = (image.height) - (viewport.height); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js index a347279fdbf9..f6ac16a9bc65 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediatypehelper.service.js @@ -23,15 +23,15 @@ function mediaTypeHelper(mediaTypeResource, $q) { getAllowedImagetypes: function (mediaId){ // TODO: This is horribly inneficient - why make one request per type!? - //This should make a call to c# to get exactly what it's looking for instead of returning every single media type and doing + //This should make a call to c# to get exactly what it's looking for instead of returning every single media type and doing //some filtering on the client side. - //This is also called multiple times when it's not needed! Example, when launching the media picker, this will be called twice + //This is also called multiple times when it's not needed! Example, when launching the media picker, this will be called twice //which means we'll be making at least 6 REST calls to fetch each media type // Get All allowedTypes return mediaTypeResource.getAllowedTypes(mediaId) .then(function(types){ - + var allowedQ = types.map(function(type){ return mediaTypeResource.getById(type.id); }); @@ -39,16 +39,8 @@ function mediaTypeHelper(mediaTypeResource, $q) { // Get full list return $q.all(allowedQ).then(function(fullTypes){ - // Find all the media types with an Image Cropper property editor - var filteredTypes = mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper']); - - // If there is only one media type with an Image Cropper we will return this one - if(filteredTypes.length === 1) { - return filteredTypes; - // If there is more than one Image cropper, custom media types have been added, and we return all media types with and Image cropper or UploadField - } else { - return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']); - } + // Find all the media types with an Image Cropper or Upload Field property editor + return mediaTypeHelperService.getTypeWithEditor(fullTypes, ['Umbraco.ImageCropper', 'Umbraco.UploadField']); }); }); @@ -68,6 +60,31 @@ function mediaTypeHelper(mediaTypeResource, $q) { } }); + }, + + getTypeAcceptingFileExtensions: function (mediaTypes, fileExtensions) { + return mediaTypes.filter(mediaType => { + var uploadProperty; + mediaType.groups.forEach(group => { + var foundProperty = group.properties.find(property => property.alias === "umbracoFile"); + if(foundProperty) { + uploadProperty = foundProperty; + } + }); + if(uploadProperty) { + var acceptedFileExtensions; + if(uploadProperty.editor === "Umbraco.ImageCropper") { + acceptedFileExtensions = Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes; + } else if(uploadProperty.editor === "Umbraco.UploadField") { + acceptedFileExtensions = (uploadProperty.config.fileExtensions && uploadProperty.config.fileExtensions.length > 0) ? uploadProperty.config.fileExtensions.map(x => x.value) : null; + } + if(acceptedFileExtensions && acceptedFileExtensions.length > 0) { + return fileExtensions.length === fileExtensions.filter(fileExt => acceptedFileExtensions.includes(fileExt)).length; + } + return true; + } + return false; + }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/less/alerts.less b/src/Umbraco.Web.UI.Client/src/less/alerts.less index 3539e2106413..94dcef6f25db 100644 --- a/src/Umbraco.Web.UI.Client/src/less/alerts.less +++ b/src/Umbraco.Web.UI.Client/src/less/alerts.less @@ -54,6 +54,15 @@ border-color: @errorBorder; color: @errorText; } + +.alert-warning() { + background-color: @warningBackground; + border-color: @warningBorder; + color: @warningText; +} +.alert-warning { + .alert-warning() +} .alert-danger h4, .alert-error h4 { color: @errorText; @@ -110,6 +119,14 @@ padding: 6px 16px 6px 12px; margin-bottom: 6px; + .show-validation-type-warning & { + .alert-warning(); + font-weight: bold; + &.alert-error::after { + border-top-color: @warningBackground; + } + } + &::after { content:''; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 359c3dd427ce..6f95608d7a8d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -215,6 +215,11 @@ @import "../views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less"; +@import "../views/components/mediacard/umb-media-card-grid.less"; +@import "../views/components/mediacard/umb-media-card.less"; +@import "../views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less"; +@import "../views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less"; +@import "../views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less"; // Utilities diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index ce9286e5f5da..4429990b4fce 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -50,6 +50,10 @@ button.umb-variant-switcher__toggle { font-weight: bold; background-color: @errorBackground; color: @errorText; + .show-validation-type-warning & { + background-color: @warningBackground; + color: @warningText; + } animation-duration: 1.4s; animation-iteration-count: infinite; @@ -232,7 +236,10 @@ button.umb-variant-switcher__toggle { .umb-variant-switcher__item.--error { .umb-variant-switcher__name { - color: @red; + color: @formErrorText; + .show-validation-type-warning & { + color: @formWarningText; + } &::after { content: '!'; position: relative; @@ -249,6 +256,10 @@ button.umb-variant-switcher__toggle { font-weight: bold; background-color: @errorBackground; color: @errorText; + .show-validation-type-warning & { + background-color: @warningBackground; + color: @warningText; + } animation-duration: 1.4s; animation-iteration-count: infinite; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 035bf02f910c..60020066c0e1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -10,6 +10,10 @@ .scoped-view{ display: none; } + + .abstract { + margin-bottom : 20px; + } } .umb-overlay__form { @@ -51,7 +55,7 @@ } .umb-overlay__title { - font-size: 16px; + font-size: 20px; color: @black; line-height: 16px; font-weight: bold; @@ -267,6 +271,9 @@ .umb-overlay .text-error { color: @formErrorText; } +.umb-overlay .text-warning { + color: @formWarningText; +} .umb-overlay .text-success { color: @formSuccessText; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index 5e9772fb2631..5fd743aaf074 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -86,6 +86,16 @@ } } } + + .show-validation.show-validation-type-warning &.-has-error { + color: @yellow-d2; + &:hover { + color: @yellow-d2 !important; + } + &::before { + background-color: @yellow-d2; + } + } } &__action:active, @@ -122,14 +132,6 @@ line-height: 16px; display: block; - &.-type-alert { - background-color: @red; - } - - &.-type-warning { - background-color: @yellow-d2; - } - &:empty { height: 12px; min-width: 12px; @@ -137,6 +139,11 @@ &.--error-badge { display: none; font-weight: 900; + background-color: @red; + + .show-validation-type-warning & { + background-color: @yellow-d2; + } } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less index febee80a9778..ffe87277e660 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less @@ -20,7 +20,7 @@ > span { position: absolute; - color: @white; + color: @ui-active-type; background: @ui-active; padding: 1px 3px; font-size: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less index 57ba73305a42..c281f7f5eafe 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less @@ -26,7 +26,10 @@ a.umb-list-item:focus { } .umb-list-item--error { - color: @red; + color: @formErrorText; +} +.umb-list-item--warning { + color: @formWarningText; } .umb-list-item:hover .umb-list-checkbox, diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 5f79d65de161..71be01e6fff8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -34,21 +34,6 @@ } -.umb-media-grid__item.-unselectable { - &::before { - content: ""; - position: absolute; - z-index: 1; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: @baseBorderRadius; - background-color: rgba(230, 230, 230, .8); - pointer-events: none; - } -} - .umb-media-grid__item.-selectable, .umb-media-grid__item.-folder {// If folders isnt selectable, they opens if clicked, therefor... cursor: pointer; @@ -59,21 +44,12 @@ } .umb-media-grid__item.-folder { - &.-selectable { .media-grid-item-edit:hover .umb-media-grid__item-name, .media-grid-item-edit:focus .umb-media-grid__item-name { text-decoration: underline; } } - - &.-unselectable { - &:hover, &:focus { - .umb-media-grid__item-name { - text-decoration: underline; - } - } - } } @@ -85,8 +61,7 @@ } .umb-media-grid__item.-selected, .umb-media-grid__item.-selectable:hover { - &::before { - content: ""; + .umb-media-grid__item-select { position: absolute; z-index:2; top: -2px; @@ -100,15 +75,21 @@ } } .umb-media-grid__item.-selectable:hover { - &::before { + .umb-media-grid__item-select { opacity: .33; } } .umb-media-grid__item.-selected:hover { - &::before { + .umb-media-grid__item-select { opacity: .75; } } +.umb-media-grid__item.-filtered:not(.-folder) { + cursor:not-allowed; + * { + pointer-events: none; + } +} .umb-media-grid__item-file-icon { transform: translate(-50%,-50%); @@ -189,14 +170,25 @@ } } -.umb-media-grid__item-name { - cursor: pointer; +.umb-media-grid__item-overlay { + cursor: pointer; + + &:hover .umb-media-grid__item-name{ + text-decoration: underline; + } +} + +.umb-media-grid__item-overlay:not(.-selected) { + &:hover + .umb-media-grid__item-select { + display: none; + } } .umb-media-grid__item-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } 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 bd787e2329ff..9dd40a4386c9 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 @@ -48,6 +48,10 @@ &.--error { border-color: @formErrorBorder !important; } + + .show-validation-type-warning &.--error { + border-color: @formWarningBorder !important; + } } .umb-nested-content__item.ui-sortable-placeholder { @@ -292,4 +296,4 @@ .umb-textarea, .umb-textstring { width:100%; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less index 6ae92ffa4e99..cc5c17ba70cb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-range-slider.less @@ -2,19 +2,35 @@ .umb-range-slider.noUi-target { background: linear-gradient(to bottom, @grayLighter 0%, @grayLighter 100%); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: none; border-radius: 20px; - height: 10px; - border: none; + height: 8px; + border: 1px solid @inputBorder; + &:focus, &:focus-within { + border-color: @inputBorderFocus; + } +} +.umb-range-slider .noUi-connects { + cursor: pointer; + height: 20px; + top: -6px; +} +.umb-range-slider .noUi-tooltip { + padding: 2px 6px; } - .umb-range-slider .noUi-handle { + outline: none; + cursor: grab; border-radius: 100px; border: none; box-shadow: none; width: 20px !important; height: 20px !important; - background-color: @blueMid; + right: -10px !important; // half the handle width + background-color: @blueExtraDark; +} +.umb-range-slider .noUi-horizontal .noUi-handle { + top: -7px; } .umb-range-slider .noUi-handle::before { @@ -25,10 +41,6 @@ display: none; } -.umb-range-slider .noUi-handle { - right: -10px !important; // half the handle width -} - .umb-range-slider .noUi-marker-large.noUi-marker-horizontal { height: 10px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less index 15b317aa459b..1b249f1c3a62 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less @@ -86,6 +86,13 @@ background-color: @red !important; border-color: @errorBorder; } +.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button, +.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:hover, +.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:focus { + color: @white !important; + background-color: @yellow-d2 !important; + border-color: @warningBorder; +} .show-validation .umb-tab--error .umb-tab-button:before { content: "\e25d"; diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 3782fca695c2..60561f9accfc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -506,10 +506,20 @@ input[type="checkbox"][readonly] { .formFieldState(@formErrorText, @formErrorText, @formErrorBackground); } +// ValidationError as a warning +.show-validation.show-validation-type-warning.ng-invalid .control-group.error, +.show-validation.show-validation-type-warning.ng-invalid .umb-editor-header__name-wrapper { + .formFieldState(@formWarningText, @formWarningText, @formWarningBackground); +} + //val-highlight directive styling .highlight-error { color: @formErrorText !important; border-color: @red-l1 !important; + .show-validation-type-warning & { + color: @formWarningText !important; + border-color: @yellow-d2 !important; + } } // FORM ACTIONS diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index 9739a90dae7a..b046ca69d923 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -405,7 +405,7 @@ } } -.checkeredBackground(@backgroundColor: @gray-9, @fillColor: @black, @fillOpacity: 0.25) { +.checkeredBackground(@backgroundColor: @white, @fillColor: @black, @fillOpacity: 0.1) { background-image: url('data:image/svg+xml;charset=utf-8,\ \ \ 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 f5e652aa3d85..328ba2229baf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -463,9 +463,16 @@ .umb-cropper{ position: relative; + width: 100%; +} + +.umb-cropper .crop-container { + position: relative; + width: 100%; + padding-bottom: 9 / 16 * 100%; } -.umb-cropper img, .umb-cropper-gravity img{ +.umb-cropper img { position: relative; max-width: 100%; height: auto; @@ -477,75 +484,72 @@ max-width: none; } - .umb-cropper .overlay, .umb-cropper-gravity .overlay { - top: 0; - left: 0; + .umb-cropper .overlay { + position: absolute; + top: 0 !important; + bottom: 0; + left: 0 !important; + right: 0; cursor: move; z-index: @zindexCropperOverlay; - position: absolute; -} + border: 1px solid @inputBorder; + outline: none; -.umb-cropper .viewport{ - overflow: hidden; - position: relative; - margin: auto; - max-width: 100%; - height: auto; - } + &:focus { + border-color: @inputBorderFocus; + } +} -.umb-cropper-gravity .viewport{ +.umb-cropper .viewport { + position: absolute; overflow: hidden; - position: relative; width: 100%; height: 100%; -} + .checkeredBackground(); + contain: strict; + > img { + position: absolute; + } + } -.umb-cropper .viewport:after { - content: ""; +.umb-cropper .viewport .__mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: @zindexCropperOverlay - 1; - opacity: .75; - box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + box-shadow: 0 0 0 2000px rgba(255, 255, 255, .8); } - -.umb-cropper-gravity .overlay{ - width: 14px; - height: 14px; - text-align: center; - border-radius: 20px; - background: @pinkLight; - border: 3px solid @white; - opacity: 0.8; -} - -.umb-cropper-gravity .overlay i { - font-size: 26px; - line-height: 26px; - opacity: 0.8 !important; +.umb-cropper .viewport .__mask-info { + position: absolute; + bottom: -20px; + height: 20px; + right: 0; + z-index: @zindexCropperOverlay - 1; + font-size: 12px; + opacity: 0.7; + padding: 0px 6px; } -.umb-cropper .crop-container { - text-align: center; +.umb-cropper .crop-controls-wrapper { + display: flex; + height: 50px; + align-items: center; + background-color: #fff; + .btn:last-of-type { + margin-right: 10px; + } } .umb-cropper .crop-slider-wrapper { - padding: 10px; - border-top: 1px solid @gray-10; - margin-top: 10px; + flex: auto; display: flex; align-items: center; justify-content: center; flex-wrap: wrap; - @media (min-width: 769px) { - padding: 10px 50px 10px 50px; - } - i { color: @gray-3; flex: 0 0 25px; @@ -558,11 +562,20 @@ } .crop-slider { - padding: 50px 15px 40px 15px; - width: 66.6%; + width: calc(100% - 100px); } } +.umb-cropper .crop-controls-wrapper__icon-left { + margin-right: 10px; + +} +.umb-cropper .crop-controls-wrapper__icon-right { + margin-left: 10px; + font-size: 22px; +} + +/* .umb-cropper-gravity .viewport, .umb-cropper-gravity, .umb-cropper-imageholder { display: inline-block; max-width: 100%; @@ -572,30 +585,51 @@ float: left; } + .umb-cropper-imageholder umb-image-gravity { + display:block; + } + */ + + .umb-crop-thumbnail-container { + img { + max-width: unset; + } + } + .cropList { display: inline-block; position: relative; vertical-align: top; + flex:0; } - .gravity-container { - border: 1px solid @gray-8; + .umb-cropper-gravity .gravity-container { + border: 1px solid @inputBorder; + box-sizing: border-box; line-height: 0; + width: 100%; + height: 100%; + overflow: hidden; + .checkeredBackground(); + contain: content; + + &:focus, &:focus-within { + border-color: @inputBorderFocus; + } .viewport { - max-width: 600px; - .checkeredBackground(); + position: relative; + width: 100%; + height: 100%; - img { - display: block; - margin-left: auto; - margin-right: auto; - } + display: flex; + justify-content: center; + align-items: center; img { display: block; - margin-left: auto; - margin-right: auto; + max-width: 100%; + max-height: 100%; } &:hover { @@ -604,6 +638,62 @@ } } + + .umb-cropper-gravity img { + position: relative; + max-width: 100%; + height: auto; + top: 0; + left: 0; + } + + .umb-cropper-gravity .overlayViewport { + position: absolute; + top:0; + bottom:0; + left:0; + right:0; + contain: strict; + + display: flex; + justify-content: center; + align-items: center; + } + .umb-cropper-gravity .overlay { + position: relative; + display: block; + max-width: 100%; + max-height: 100%; + cursor: crosshair; + } + .umb-cropper-gravity .overlay .focalPoint { + position: absolute; + top: 0; + left: 0; + cursor: move; + z-index: @zindexCropperOverlay; + + width: 14px; + height: 14px; + // this element should have no width or height as its preventing the jQuery draggable-plugin to go all the way to the sides: + margin-left: -10px; + margin-top: -10px; + margin-right: -10px; + margin-bottom: -10px; + + text-align: center; + border-radius: 20px; + background: @pinkLight; + border: 3px solid @white; + opacity: 0.8; + } + + .umb-cropper-gravity .overlay .focalPoint i { + font-size: 26px; + line-height: 26px; + opacity: 0.8 !important; + } + .imagecropper { display: flex; align-items: flex-start; @@ -611,24 +701,13 @@ @media (max-width: 768px) { flex-direction: column; - float: left; - max-width: 100%; - } - - .viewport img { - .checkeredBackground(); } + } .imagecropper .umb-cropper__container { position: relative; - margin-bottom: 10px; - max-width: 100%; - border: 1px solid @gray-10; - - @media (min-width: 769px) { - width: 600px; - } + width: 100%; } .imagecropper .umb-cropper__container .button-drawer { diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index cab0745a427e..9d114b093ee7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -291,8 +291,8 @@ @btnSuccessBackground: @ui-btn-positive;// updated 2019 @btnSuccessBackgroundHighlight: @ui-btn-positive-hover;// updated 2019 -@btnWarningBackground: @orange; -@btnWarningBackgroundHighlight: lighten(@orange, 10%); +@btnWarningBackground: @yellow-d2; +@btnWarningBackgroundHighlight: lighten(@yellow-d2, 10%); @btnDangerBackground: @red; @btnDangerBackgroundHighlight: @red-l1; @@ -480,7 +480,7 @@ @formWarningBorder: darken(spin(@warningBackground, -10), 3%); @formErrorText: @errorBackground; -@formErrorBackground: lighten(@errorBackground, 55%); +@formErrorBackground: @errorBackground; @formErrorBorder: @red; @formSuccessText: @successBackground; diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index a5df6904e5af..bc10dfa050a5 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -1,18 +1,18 @@ -/** +/** * @ngdoc controller - * @name Umbraco.MainController + * @name Umbraco.MainController * @function - * - * @description + * + * @description * The main application controller - * + * */ -function MainController($scope, $location, appState, treeService, notificationsService, - userService, historyService, updateChecker, navigationService, eventsService, +function MainController($scope, $location, appState, treeService, notificationsService, + userService, historyService, updateChecker, navigationService, eventsService, tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { - + //the null is important because we do an explicit bool check on this in the view - $scope.authenticated = null; + $scope.authenticated = null; $scope.touchDevice = appState.getGlobalState("touchDevice"); $scope.infiniteMode = false; $scope.overlay = {}; @@ -27,14 +27,14 @@ function MainController($scope, $location, appState, treeService, notificationsS assetsService.loadJs(tinyJsAsset, $scope); }); - // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { enableTabbingActive(); } } - + function enableTabbingActive() { $scope.tabbingActive = true; $scope.$digest(); @@ -186,7 +186,7 @@ function MainController($scope, $location, appState, treeService, notificationsS evts.push(eventsService.on("appState.overlay", function (name, args) { $scope.overlay = args; })); - + // events for tours evts.push(eventsService.on("appState.tour.start", function (name, args) { $scope.tour = args; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 90803a376578..5e6613c0f447 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -17,7 +17,6 @@ angular.module("umbraco") "alias": "empty", "name": data[0], "icon": "icon-add", - "active": true, "view": "" }, { @@ -28,10 +27,16 @@ angular.module("umbraco") "disabled": vm.model.clipboardItems.length === 0 }]; - vm.activeTab = vm.navigation[0]; + if (vm.model.openClipboard === true) { + vm.activeTab = vm.navigation[1]; + } else { + vm.activeTab = vm.navigation[0]; + } + + vm.activeTab.active = true; } ); - + vm.onNavigationChanged = function (tab) { vm.activeTab.active = false; vm.activeTab = tab; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js new file mode 100644 index 000000000000..6c8a0385365e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.controller.js @@ -0,0 +1,183 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.MediaEntryEditorController", + function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) { + + var unsubscribe = []; + var vm = this; + + vm.loading = true; + vm.model = $scope.model; + vm.mediaEntry = vm.model.mediaEntry; + vm.currentCrop = null; + + localizationService.localizeMany([ + vm.model.createFlow ? "general_cancel" : "general_close", + vm.model.createFlow ? "general_create" : "buttons_submitChanges" + ]).then(function (data) { + vm.closeLabel = data[0]; + vm.submitLabel = data[1]; + }); + + vm.title = ""; + + function init() { + + updateMedia(); + + unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) { + // if this media item uses the updated media type we want to reload the media file + if(args && args.media && args.media.key === vm.mediaEntry.mediaKey) { + updateMedia(); + } + })); + } + + function updateMedia() { + + vm.loading = true; + entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) { + vm.media = mediaEntity; + vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true); + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + + localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(function (data) { + vm.title = data; + }); + }, function () { + localizationService.localize("mediaPicker_deletedItem").then(function (localized) { + vm.media = { + name: localized, + icon: "icon-picture", + trashed: true + }; + vm.loading = false; + vm.hasDimensions = false; + vm.isCroppable = false; + }); + }); + } + + vm.onImageLoaded = onImageLoaded; + function onImageLoaded(isCroppable, hasDimensions) { + vm.isCroppable = isCroppable; + vm.hasDimensions = hasDimensions; + }; + + + vm.repickMedia = repickMedia; + function repickMedia() { + vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced); + } + + function onMediaReplaced() { + + // mark we have changes: + vm.imageCropperForm.$setDirty(); + + // un-select crop: + vm.currentCrop = null; + + // + updateMedia(); + } + + vm.openMedia = openMedia; + function openMedia() { + + var mediaEditor = { + id: vm.mediaEntry.mediaKey, + submit: function () { + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.mediaEditor(mediaEditor); + } + + + vm.focalPointChanged = function(left, top) { + //update the model focalpoint value + vm.mediaEntry.focalPoint = { + left: left, + top: top + }; + + //set form to dirty to track changes + setDirty(); + } + + + + vm.selectCrop = selectCrop; + function selectCrop(targetCrop) { + vm.currentCrop = targetCrop; + setDirty(); + // TODO: start watchin values of crop, first when changed set to dirty. + }; + + vm.deselectCrop = deselectCrop; + function deselectCrop() { + vm.currentCrop = null; + }; + + vm.resetCrop = resetCrop; + function resetCrop() { + if (vm.currentCrop) { + $scope.$evalAsync( () => { + vm.model.propertyEditor.resetCrop(vm.currentCrop); + vm.forceUpdateCrop = Math.random(); + }); + } + } + + function setDirty() { + vm.imageCropperForm.$setDirty(); + } + + + vm.submitAndClose = function () { + if (vm.model && vm.model.submit) { + vm.model.submit(vm.model); + } + } + + vm.close = function () { + if (vm.model && vm.model.close) { + if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) { + var labels = vm.model.createFlow === true ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"]; + localizationService.localizeMany(labels).then(function (localizations) { + const confirm = { + title: localizations[0], + view: "default", + content: localizations[1], + submitButtonLabelKey: "general_discard", + submitButtonStyle: "danger", + closeButtonLabelKey: "prompt_stay", + submit: function () { + overlayService.close(); + vm.model.close(vm.model); + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(confirm); + }); + } else { + vm.model.close(vm.model); + } + + } + } + + init(); + $scope.$on("$destroy", function () { + unsubscribe.forEach(x => x()); + }); + + } + ); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html new file mode 100644 index 000000000000..afa34518999b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.html @@ -0,0 +1,118 @@ +
+ + + + + + + + +
+ +
+ This item is in the Recycle Bin +
+ +
+
+ + + + +
+ +
+ +
+ + + + + +
+ +
+ + + + + + + +
+ + +
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less new file mode 100644 index 000000000000..1de962f7e11f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediaentryeditor/mediaentryeditor.less @@ -0,0 +1,122 @@ +.umb-media-entry-editor { + + .umb-cropper-imageholder { + position: relative; + width: 100%; + height: 100%; + } + .umb-cropper-gravity { + height: 100%; + } + .umb-cropper__container { + width: 100%; + height: 100%; + } + .umb-cropper { + height: 100%; + } + .umb-cropper .crop-container { + padding-bottom: 0; + height: calc(100% - 50px) + } + .umb-cropper .crop-controls-wrapper { + justify-content: center; + } + .umb-cropper .crop-slider-wrapper { + max-width: 500px; + } +} + +.umb-media-entry-editor__pane { + display: flex; + flex-flow: row-reverse; + height: 100%; + width: 100%; +} + +.umb-media-entry-editor__crops { + background-color: white; + overflow: auto; + + > button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + text-align: center; + padding: 4px 10px 0 10px; + border-bottom: 1px solid @gray-9; + box-sizing: border-box; + height: 120px; + width: 120px; + color: @ui-active-type; + + &:hover { + color: @ui-active-type-hover; + text-decoration: none; + } + + &:active { + .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); + } + + &::before { + content: ""; + position: absolute; + width: 0px; + max-height: 50px; + height: (100% - 16px); + top: auto; + bottom: auto; + background-color: @ui-light-active-border; + left: 0; + border-radius: 0 3px 3px 0; + opacity: 0; + transition: all .2s linear; + } + + &.--is-active { + color: @ui-light-active-type; + + &::before { + opacity: 1; + width: 4px; + } + } + &.--is-defined { + + } + + > .__icon { + font-size: 24px; + display: block; + text-align: center; + margin-bottom: 7px; + } + + > .__text { + font-size: 12px; + line-height: 1em; + margin-top: 4px; + } + } +} + +.umb-media-entry-editor__imagecropper { + flex: auto; + height: 100%; +} + +.umb-media-entry-editor__imageholder { + display: block; + position: relative; + height: calc(100% - 50px); +} +.umb-media-entry-editor__imageholder-actions { + background-color: white; + height: 50px; + display: flex; + justify-content: center; +} + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index fec2e632c5f2..029dedf214a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.MediaPickerController", - function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage, notificationsService) { + function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage, notificationsService, clipboardService) { var vm = this; @@ -19,6 +19,8 @@ angular.module("umbraco") vm.enterSubmitFolder = enterSubmitFolder; vm.focalPointChanged = focalPointChanged; vm.changePagination = changePagination; + vm.onNavigationChanged = onNavigationChanged; + vm.clickClearClipboard = clickClearClipboard; vm.clickHandler = clickHandler; vm.clickItemName = clickItemName; @@ -27,7 +29,10 @@ angular.module("umbraco") vm.selectLayout = selectLayout; vm.showMediaList = false; + vm.navigation = []; + var dialogOptions = $scope.model; + vm.clipboardItems = dialogOptions.clipboardItems; $scope.disableFolderSelect = (dialogOptions.disableFolderSelect && dialogOptions.disableFolderSelect !== "0") ? true : false; $scope.disableFocalPoint = (dialogOptions.disableFocalPoint && dialogOptions.disableFocalPoint !== "0") ? true : false; @@ -100,10 +105,32 @@ angular.module("umbraco") function setTitle() { if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMedia") + localizationService.localizeMany(["defaultdialogs_selectMedia", "mediaPicker_tabClipboard"]) .then(function (data) { - $scope.model.title = data; + $scope.model.title = data[0]; + + + vm.navigation = [{ + "alias": "empty", + "name": data[0], + "icon": "icon-umb-media", + "active": true, + "view": "" + }]; + + if(vm.clipboardItems) { + vm.navigation.push({ + "alias": "clipboard", + "name": data[1], + "icon": "icon-paste-in", + "view": "", + "disabled": vm.clipboardItems.length === 0 + }); + } + + vm.activeTab = vm.navigation[0]; }); + } } @@ -149,7 +176,7 @@ angular.module("umbraco") .then(function (node) { $scope.target = node; // Moving directly to existing node's folder - gotoFolder({ id: node.parentId }).then(function() { + gotoFolder({ id: node.parentId }).then(function () { selectMedia(node); $scope.target.url = mediaHelper.resolveFileFromEntity(node); $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); @@ -169,10 +196,10 @@ angular.module("umbraco") function upload(v) { var fileSelect = $(".umb-file-dropzone .file-select"); - if (fileSelect.length === 0){ + if (fileSelect.length === 0) { localizationService.localize('media_uploadNotAllowed').then(function (message) { notificationsService.warning(message); }); } - else{ + else { fileSelect.trigger("click"); } } @@ -395,6 +422,19 @@ angular.module("umbraco") }); }; + function onNavigationChanged(tab) { + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; + }; + + function clickClearClipboard() { + vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true; + vm.clipboardItems = []; + dialogOptions.clickClearClipboard(); + }; + var debounceSearchMedia = _.debounce(function () { $scope.$apply(function () { if (vm.searchOptions.filter) { @@ -504,13 +544,7 @@ angular.module("umbraco") var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; for (var i = 0; i < data.length; i++) { - if (data[i].metaData.MediaPath !== null) { - data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); - data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); - } - if (data[i].metaData.UpdateDate !== null){ - data[i].updateDate = data[i].metaData.UpdateDate; - } + setDefaultData(data[i]); data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; } @@ -523,6 +557,16 @@ angular.module("umbraco") }); } + function setDefaultData(item) { + if (item.metaData.MediaPath !== null) { + item.thumbnail = mediaHelper.resolveFileFromEntity(item, true); + item.image = mediaHelper.resolveFileFromEntity(item, false); + } + if (item.metaData.UpdateDate !== null) { + item.updateDate = item.metaData.UpdateDate; + } + } + function preSelectMedia() { for (var folderIndex = 0; folderIndex < $scope.images.length; folderIndex++) { var folderImage = $scope.images[folderIndex]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index df0c8e3cef72..d1f0699b1373 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -3,13 +3,15 @@ - +
@@ -19,21 +21,20 @@
@@ -49,20 +50,20 @@
-
+
+ layouts="vm.layout.layouts" + active-layout="vm.layout.activeLayout" + on-layout-select="vm.selectLayout(layout)">
+ layouts="vm.layout.layouts" + active-layout="vm.layout.activeLayout" + on-layout-select="vm.selectLayout(layout)">
@@ -86,31 +87,29 @@ + class="umb-breadcrumbs__add-ancestor" + ng-show="model.showFolderInput" + ng-model="model.newFolderName" + ng-keydown="enterSubmitFolder($event)" + ng-blur="vm.submitFolder()" + focus-when="{{model.showFolderInput}}" />
- + - - +
- @@ -145,11 +142,30 @@
- - - + + + +
+ + +
+ +
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html index 933551bbff8d..af692f8322e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-crop.html @@ -1,25 +1,36 @@
-
- -
-
+
+ +
+
{{width}}px x {{height}}px
+
+
+
-
- +
+
+ -
- - -
+
+ + +
- + +
+
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html index edd840a47f3d..10aa6a774ab6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/imaging/umb-image-gravity.html @@ -1,12 +1,17 @@
- +
- + -
+
+
+ +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less new file mode 100644 index 000000000000..f7e576433570 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card-grid.less @@ -0,0 +1,137 @@ +.umb-media-card-grid { + /* Grid Setup */ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + grid-auto-rows: minmax(100px, auto); + grid-gap: 10px; + + justify-items: center; + align-items: center; +} +.umb-media-card-grid__cell { + position: relative; + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; +} + +.umb-media-card-grid--inline-create-button { + position: absolute; + height: 100%; + z-index: 1; + opacity: 0; + outline: none; + left: 0; + width: 12px; + margin-left: -7px; + padding-left: 6px; + margin-right: -6px; + transition: opacity 240ms; + + &::before { + content: ''; + position: absolute; + background: @blueMid; + background: linear-gradient(0deg, rgba(@blueMid,0) 0%, rgba(@blueMid,1) 50%, rgba(@blueMid,0) 100%); + border-left: 1px solid white; + border-right: 1px solid white; + border-radius: 2px; + left: 0; + top: 0; + bottom: 0; + width: 2px; + animation: umb-media-card-grid--inline-create-button_before 400ms ease-in-out alternate infinite; + transform: scaleX(.99); + transition: transform 240ms ease-out; + + @keyframes umb-media-card-grid--inline-create-button_before { + 0% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + + > .__plus { + position: absolute; + display: flex; + justify-content: center; + align-items: center; + pointer-events: none; // lets stop avoiding the mouse values in JS move event. + box-sizing: border-box; + width: 28px; + height: 28px; + margin-left: -18px; + margin-top: -18px - 8px; + border-radius: 3em; + font-size: 14px; + border: 2px solid @blueMid; + color: @blueMid; + background-color: rgba(255, 255, 255, .96); + box-shadow: 0 0 0 2px rgba(255, 255, 255, .96); + transform: scale(0); + transition: transform 240ms ease-in; + + animation: umb-media-card-grid--inline-create-button__plus 400ms ease-in-out alternate infinite; + + @keyframes umb-media-card-grid--inline-create-button__plus { + 0% { color: rgba(@blueMid, 1); } + 100% { color: rgba(@blueMid, 0.8); } + } + + } + + &:focus { + > .__plus { + border-color: @ui-outline; + } + } + + &:hover, &:focus { + opacity: 1; + + &::before { + transform: scaleX(1); + } + > .__plus { + transform: scale(1); + transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); + + } + } +} + +.umb-media-card-grid__create-button { + position: relative; + width: 100%; + padding-bottom: 100%; + + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + font-weight: bold; + box-sizing: border-box; + border-radius: @baseBorderRadius; + + > div { + position: absolute; + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + } +} + +.umb-media-card-grid__create-button:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; +} + +.umb-media-card-grid__create-button.--disabled, +.umb-media-card-grid__create-button.--disabled:hover { + color: @gray-7; + border-color: @gray-7; + cursor: default; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html new file mode 100644 index 000000000000..01ce31415ee1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html @@ -0,0 +1,47 @@ + +
+ +
+ +

+ + +

+ +

+ + +

+ + + {{vm.media.name}} + + + + + + + + +
+ + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less new file mode 100644 index 000000000000..de3840b4d786 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.less @@ -0,0 +1,186 @@ +.umb-media-card, +umb-media-card { + position: relative; + display: inline-block; + width: 100%; + //background-color: white; + border-radius: @baseBorderRadius; + //box-shadow: 0 1px 2px rgba(0,0,0,.2); + overflow: hidden; + + transition: box-shadow 120ms; + + cursor: pointer; + + .umb-outline(); + + &:hover { + box-shadow: 0 1px 3px rgba(@ui-action-type-hover, .5); + } + + &.--isOpen { + &::after { + content: ""; + position: absolute; + border: 2px solid @ui-active-border; + border-radius: @baseBorderRadius; + top:0; + bottom: 0; + left: 0; + right: 0; + } + } + + &.--hasError { + border: 2px solid @errorBackground; + } + + &.--sortable-placeholder { + &::after { + content: ""; + position: absolute; + background-color:rgba(@ui-drop-area-color, .05); + border: 2px solid rgba(@ui-drop-area-color, .1); + border-radius: @baseBorderRadius; + box-shadow: 0 0 4px rgba(@ui-drop-area-color, 0.05); + top:0; + bottom: 0; + left: 0; + right: 0; + animation: umb-block-card--sortable-placeholder 400ms ease-in-out alternate infinite; + @keyframes umb-block-card--sortable-placeholder { + 0% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + box-shadow: none; + } + + .__status { + + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 2px; + + &.--error { + background-color: @errorBackground; + color: @errorText; + } + } + + .__showcase { + position: relative; + max-width: 100%; + min-height: 120px; + max-height: 240px; + text-align: center; + //padding-bottom: 10/16*100%; + //background-color: @gray-12; + + img { + object-fit: contain; + max-height: 240px; + } + + umb-file-icon { + width: 100%; + padding-bottom: 100%; + display: block; + .umb-file-icon { + position: absolute; + top: 0; + bottom: 0; + left: 10px; + right: 10px; + display: flex; + align-items: center; + justify-content: center; + } + } + } + + .__info { + position: absolute; + text-align: left; + bottom: 0; + width: 100%; + background-color: #fff; + padding-top: 6px; + padding-bottom: 7px;// 7 + 1 to compentiate for the -1 substraction in margin-bottom. + + opacity: 0; + transition: opacity 120ms; + + &.--error { + opacity: 1; + background-color: @errorBackground; + .__name, .__subname { + color: @errorText; + } + } + + .__name { + font-weight: bold; + font-size: 13px; + color: @ui-action-type; + margin-left: 16px; + margin-bottom: -1px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .__subname { + color: @gray-4; + font-size: 12px; + margin-left: 16px; + margin-top: 1px; + margin-bottom: -1px; + line-height: 1.5em; + } + } + + &:hover, &:focus, &:focus-within { + .__info { + opacity: 1; + } + .__info:not(.--error) { + .__name { + color: @ui-action-type-hover; + } + } + } + + .__actions { + position: absolute; + top: 10px; + right: 10px; + font-size: 0; + background-color: rgba(255, 255, 255, .96); + border-radius: 16px; + padding-left: 5px; + padding-right: 5px; + + opacity: 0; + transition: opacity 120ms; + .__action { + position: relative; + display: inline-block; + padding: 5px; + font-size: 18px; + + color: @ui-action-discreet-type; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + } + &:hover, &:focus, &:focus-within { + .__actions { + opacity: 1; + } + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js new file mode 100644 index 000000000000..24b20367aa79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umbMediaCard.component.js @@ -0,0 +1,97 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .component("umbMediaCard", { + templateUrl: "views/components/mediacard/umb-media-card.html", + controller: MediaCardController, + controllerAs: "vm", + transclude: true, + bindings: { + mediaKey: " { + if(newValue !== oldValue) { + vm.updateThumbnail(); + } + })); + + function checkErrorState() { + + vm.notAllowed = (vm.media &&vm.allowedTypes && vm.allowedTypes.length > 0 && vm.allowedTypes.indexOf(vm.media.metaData.ContentTypeAlias) === -1); + + if ( + vm.hasError === true || vm.notAllowed === true || (vm.media && vm.media.trashed === true) + ) { + $element.addClass("--hasError") + vm.mediaCardForm.$setValidity('error', false) + } else { + $element.removeClass("--hasError") + vm.mediaCardForm.$setValidity('error', true) + } + } + + vm.$onInit = function () { + + unsubscribe.push($scope.$watchGroup(["vm.media.trashed", "vm.hasError"], checkErrorState)); + + vm.updateThumbnail(); + + unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) { + // if this media item uses the updated media type we want to reload the media file + if(args && args.media && args.media.key === vm.mediaKey) { + vm.updateThumbnail(); + } + })); + } + + + vm.$onDestroy = function () { + unsubscribe.forEach(x => x()); + } + + vm.updateThumbnail = function () { + + if(vm.mediaKey && vm.mediaKey !== "") { + vm.loading = true; + + entityResource.getById(vm.mediaKey, "Media").then(function (mediaEntity) { + vm.media = mediaEntity; + checkErrorState(); + vm.thumbnail = mediaHelper.resolveFileFromEntity(mediaEntity, true); + + vm.loading = false; + }, function () { + localizationService.localize("mediaPicker_deletedItem").then(function (localized) { + vm.media = { + name: localized, + icon: "icon-picture", + trashed: true + }; + vm.loading = false; + $element.addClass("--hasError") + vm.mediaCardForm.$setValidity('error', false) + }); + }); + } + + } + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html new file mode 100644 index 000000000000..a49a99a81eaf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references-table.html @@ -0,0 +1,33 @@ +
+
+ {{vm.title}} +
+ +
+
+
+
+
Name
+
Alias
+
Open
+
+
+
+
+
+
{{::reference.name}}
+
{{::reference.alias}}
+ +
+
+
+ + +
+ + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html index 7c6dfd3db12b..f3da66a932fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/references/umb-tracked-references.html @@ -1,6 +1,6 @@  - + @@ -11,40 +11,27 @@
+
+ + +
+
Used in Documents
-
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
+ + + One or more of this item's descendants is being used in a content item. + +
@@ -60,39 +47,25 @@
-
+
+ + +
+
Used in Members
-
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
+ + + One or more of this item's descendants is being used in a a member + +
@@ -109,38 +82,24 @@
+ + +
+
Used in Media
-
-
-
-
-
Name
-
Alias
-
Open
-
-
-
-
-
-
{{::reference.name}}
-
{{::reference.alias}}
- -
-
-
- - -
- - -
+ + + One or more of this item's descendants is being used in a media item + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index f41390bce3a7..975405626772 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -6,39 +6,42 @@ ng-click="clickItem(item, $event, $index)" ng-repeat="item in items | filter:filterBy" ng-style="item.flexStyle" - ng-class="{'-selected': item.selected, '-file': !item.thumbnail, '-folder': item.isFolder, '-svg': item.extension == 'svg', '-selectable': item.selectable, '-unselectable': !item.selectable}"> -
- -
- -
{{item.name}}
-
+ ng-class="{'-selected': item.selected, '-file': !item.thumbnail, '-folder': item.isFolder, '-svg': item.extension == 'svg', '-selectable': item.selectable, '-unselectable': !item.selectable, '-filtered': item.filtered}"> - -
+ +
+ +
{{item.name}}
+
- - {{item.name}} + +
- - {{item.name}} + +
- - {{item.name}} + + {{item.name}} + + + {{item.name}} + + + {{item.name}} + + + + - - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html index 6ec8b08da66e..e2319f099d66 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -74,14 +74,14 @@

{{ miniListView.node.name }}

{{ child.name }}
- -
+ +
No items have been added Sorry, we can not find what you are looking for.
- -
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index 41e24a6cda74..fadc0ac3b1d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -6,9 +6,9 @@

Click to upload

- +
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index eac489ab0893..01526bb90780 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -143,7 +143,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", // Mini list view $scope.selectListViewNode = function (node) { node.selected = node.selected === true ? false : true; - nodeSelectHandler({}, { node: node }); + nodeSelectHandler({ node: node }); }; $scope.closeMiniListView = function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html index fa9ab8c43777..36e01991dfa6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.html @@ -16,7 +16,9 @@
-
+
+ + + + - * @@ -36,10 +39,13 @@ - - + - {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} + {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} + + + {{saveVariantSelectorForm.saveInvariant.errorMsg}} diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js index 936ab3b1049e..5030f3e98e2c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.controller.js @@ -4,12 +4,18 @@ function UnpublishController($scope, localizationService, contentEditingHelper) { var vm = this; + var autoSelectedVariants = []; + vm.id = $scope.content.id; + + vm.warningText = null; + vm.changeSelection = changeSelection; function onInit() { + $scope.model.hideSubmitButton = true; vm.variants = $scope.model.variants; vm.unpublishableVariants = vm.variants.filter(publishedVariantFilter) @@ -26,7 +32,7 @@ // node has variants if (vm.variants.length !== 1) { - + vm.unpublishableVariants = contentEditingHelper.getSortedVariantsAndSegments(vm.unpublishableVariants); var active = vm.variants.find(v => v.active); @@ -39,7 +45,7 @@ // autoselect other variants if needed changeSelection(active); } - + } function changeSelection(selectedVariant) { @@ -66,7 +72,7 @@ // that was automatically selected so it goes back to the state before the mandatory language was selected. // We also want to enable all checkboxes again if (!selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { - + vm.variants.forEach(variant => { // check if variant was auto selected, then deselect @@ -108,6 +114,17 @@ }); }); + vm.checkingReferencesComplete = () => { + $scope.model.hideSubmitButton = false; + }; + + vm.onReferencesWarning = () => { + $scope.model.submitButtonStyle = "danger"; + localizationService.localize("references_unpublishWarning").then((value) => { + vm.warningText = value; + }); + }; + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html index dc3862879a51..9d8c27115145 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/unpublish.html @@ -11,6 +11,8 @@

+ +
@@ -49,7 +51,14 @@
-
+ +
+ +
+ +
+ {{vm.warningText}} +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/delete.html b/src/Umbraco.Web.UI.Client/src/views/media/delete.html index 7231ccf2c4fb..2c817e8cb942 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/delete.html @@ -14,11 +14,17 @@ Are you sure you want to delete {{currentNode.name}}?

+ + +
+ {{warningText}} +
+
When items are deleted from the recycle bin, they will be gone forever.
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index 879257137752..c7b26ec9d04f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -2,11 +2,14 @@ * @ngdoc controller * @name Umbraco.Editors.ContentDeleteController * @function - * + * * @description * The controller for deleting content */ -function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService) { +function MediaDeleteController($scope, mediaResource, treeService, navigationService, editorState, $location, overlayService,localizationService) { + + $scope.checkingReferences = true; + $scope.warningText = null; $scope.performDelete = function() { @@ -26,7 +29,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer treeService.removeNode($scope.currentNode); if (rootNode) { - //ensure the recycle bin has child nodes now + //ensure the recycle bin has child nodes now var recycleBin = treeService.getDescendantNode(rootNode, -21); if (recycleBin) { recycleBin.hasChildren = true; @@ -36,7 +39,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer } } } - + //if the current edited item is the same one as we're deleting, we need to navigate elsewhere if (editorState.current && editorState.current.id == $scope.currentNode.id) { @@ -68,6 +71,17 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer $scope.close = function() { navigationService.hideDialog(); }; + + $scope.checkingReferencesComplete = () => { + $scope.checkingReferences = false; + }; + + $scope.onReferencesWarning = () => { + localizationService.localize("references_deleteWarning").then((value) => { + $scope.warningText = value; + }); + }; + } angular.module("umbraco").controller("Umbraco.Editors.Media.DeleteController", MediaDeleteController); 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 f41f22a1a9c9..88d112e2d638 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 @@ -2,29 +2,29 @@ * @ngdoc controller * @name Umbraco.Editors.Media.EditController * @function - * + * * @description * The controller for the media editor */ -function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, - entityResource, navigationService, notificationsService, localizationService, - serverValidationManager, contentEditingHelper, fileManager, formHelper, +function mediaEditController($scope, $routeParams, $location, $http, $q, appState, mediaResource, + entityResource, navigationService, notificationsService, localizationService, + serverValidationManager, contentEditingHelper, fileManager, formHelper, editorState, umbRequestHelper, eventsService) { - + var evts = []; var nodeId = null; var create = false; var infiniteMode = $scope.model && $scope.model.infiniteMode; - // when opening the editor through infinite editing get the + // when opening the editor through infinite editing get the // node id from the model instead of the route param if(infiniteMode && $scope.model.id) { nodeId = $scope.model.id; } else { nodeId = $routeParams.id; } - - // when opening the editor through infinite editing get the + + // when opening the editor through infinite editing get the // create option from the model instead of the route param if(infiniteMode) { create = $scope.model.create; @@ -72,22 +72,22 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } function init() { - + var content = $scope.content; - + // 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.) content.apps.forEach(app => { if (app === $scope.app) { isAppPresent = true; } }); - + // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { content.apps.forEach(app => { @@ -98,9 +98,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } }); } - + } - + // if we still dont have a app, lets show the first one: if (isAppPresent === false) { content.apps[0].active = true; @@ -108,16 +108,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat } editorState.set($scope.content); - + bindEvents(); } - + function bindEvents() { //bindEvents can be called more than once and we don't want to have multiple bound events for (var e in evts) { eventsService.unsubscribe(evts[e]); } - + evts.push(eventsService.on("editors.mediaType.saved", function(name, args) { // if this media item uses the updated media type we need to reload the media item if(args && args.mediaType && args.mediaType.key === $scope.content.contentType.key) { @@ -131,7 +131,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat })); } $scope.page.submitButtonLabelKey = "buttons_save"; - + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ function syncTreeNode(content, path, initialLoad) { @@ -149,7 +149,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat //it's a child item, just sync the ui node to the parent navigationService.syncTree({ tree: "media", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node // from the server so that we can load in the actions menu. umbRequestHelper.resourcePromise( $http.get(content.treeNodeUrl), @@ -176,7 +176,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $scope.save = function () { if (formHelper.submitForm({ scope: $scope })) { - + $scope.page.saveButtonState = "busy"; mediaResource.save($scope.content, create, fileManager.getFiles()) @@ -200,12 +200,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat editorState.set($scope.content); syncTreeNode($scope.content, data.path); - + $scope.page.saveButtonState = "success"; init(); } + eventsService.emit("editors.media.saved", {media: data}); + + return data; + }, function(err) { formHelper.resetForm({ scope: $scope, hasErrors: true }); @@ -213,16 +217,16 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat err: err, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); - + editorState.set($scope.content); $scope.page.saveButtonState = "error"; }); } else { - showValidationNotification(); + showValidationNotification(); } - + }; function loadMedia() { @@ -231,7 +235,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat .then(function (data) { $scope.content = data; - + if (data.isChildOfListView && data.trashed === false) { $scope.page.listViewPath = ($routeParams.page) ? "/media/media/edit/" + data.parentId + "?page=" + $routeParams.page @@ -247,9 +251,9 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat serverValidationManager.notifyAndClearAllSubscriptions(); if(!infiniteMode) { - syncTreeNode($scope.content, data.path, true); + syncTreeNode($scope.content, data.path, true); } - + 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") @@ -279,7 +283,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $scope.appChanged = function (app) { $scope.app = app; - + // setup infinite mode if(infiniteMode) { $scope.page.submitButtonLabelKey = "buttons_saveAndClose"; @@ -296,7 +300,7 @@ function mediaEditController($scope, $routeParams, $location, $http, $q, appStat $location.path($scope.page.listViewPath.split("?")[0]); } }; - + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html index d9d8cad9821b..6e67c947936f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/numberrange.html @@ -4,6 +4,7 @@ type="number" ng-model="model.value.min" placeholder="0" + min="0" ng-max="model.value.max" fix-number /> @@ -11,7 +12,7 @@ type="number" ng-model="model.value.max" placeholder="∞" - ng-min="model.value.min" + ng-min="model.value.min || 0" fix-number /> diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js index dcc9add395d8..d02e626bfa2f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js @@ -99,6 +99,11 @@ function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeRe eventsService.unsubscribe(evts[e]); } }); + + if ($scope.model.config.itemType) { + currentItemType = $scope.model.config.itemType; + init(); + } } angular.module('umbraco').controller("Umbraco.PrevalueEditors.TreeSourceTypePickerController", TreeSourceTypePickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index 12862d8f4160..9b408beccef1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -48,8 +48,14 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { > button { color: @formErrorText; + .show-validation-type-warning & { + color: @formWarningText; + } span.caret { border-top-color: @formErrorText; + .show-validation-type-warning & { + border-top-color: @formWarningText; + } } } } @@ -72,6 +78,9 @@ padding: 2px; line-height: 10px; background-color: @formErrorText; + .show-validation-type-warning & { + background-color: @formWarningText; + } font-weight: 900; animation-duration: 1.4s; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index 613a47b92619..837fd3f56471 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -42,6 +42,9 @@ ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > .umb-block-list__block--content > div > & { color: @formErrorText; + .show-validation-type-warning & { + color: @formWarningText; + } } ng-form.ng-invalid-val-server-match-content > .umb-block-list__block:not(.--active) > .umb-block-list__block--content > div > & { > span { @@ -61,6 +64,9 @@ padding: 2px; line-height: 10px; background-color: @formErrorText; + .show-validation-type-warning & { + background-color: @formWarningText; + } font-weight: 900; animation-duration: 1.4s; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index 38959da6ba8c..a6c1eb219955 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -10,7 +10,7 @@
- + + class="btn-reset umb-block-list__clipboard-button umb-outline" + ng-class="{'--jump': vm.jumpClipboardButton}" + ng-disabled="vm.clipboardItems.length === 0" + ng-click="vm.requestShowClipboard(vm.layout.length, $event)" + localize="title" + title="@blockEditor_tabClipboard"> + + + Clipboard + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index e290c6e85684..47b1d00ca2d1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -10,7 +10,7 @@ .umb-block-list__wrapper { position: relative; - max-width: 1024px; + .umb-property-editor--limit-width(); > .ui-sortable > .ui-sortable-helper > .umb-block-list__block > .umb-block-list__block--content > * { box-shadow: 0px 5px 10px 0 rgba(0,0,0,.2); } @@ -23,10 +23,6 @@ > .umb-block-list__block--actions { opacity: 0; transition: opacity 120ms; - - .--error { - color: @formErrorBorder !important; - } } &:hover, @@ -100,6 +96,12 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo &:hover { color: @ui-action-discreet-type-hover; } + &.--error { + color: @errorBackground; + .show-validation-type-warning & { + color: @warningBackground; + } + } > .__error-badge { position: absolute; top: -2px; @@ -113,7 +115,10 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo font-weight: bold; padding: 2px; line-height: 8px; - background-color: @red; + background-color: @errorBackground; + .show-validation-type-warning & { + background-color: @warningBackground; + } display: none; font-weight: 900; } @@ -236,30 +241,98 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo } } } -.umb-block-list__create-button { - position: relative; + +.umb-block-list__actions { display: flex; - width: 100%; - align-items: center; - justify-content: center; + border: 1px dashed @ui-action-discreet-border; - color: @ui-action-discreet-type; - font-weight: bold; - margin: 2px 0; - padding: 5px 15px; - box-sizing: border-box; border-radius: @baseBorderRadius; -} + box-sizing: border-box; -.umb-block-list__create-button:hover { - color: @ui-action-discreet-type-hover; - border-color: @ui-action-discreet-border-hover; - text-decoration: none; -} + &:hover { + border-color: transparent; + > button { + + border-color: @ui-action-discreet-border; + + &.umb-block-list__clipboard-button { + opacity: 1; + } + } + + } + + > button { + position: relative; + display: flex; + //width: 100%; + align-items: center; + justify-content: center; + + color: @ui-action-discreet-type; + font-weight: bold; + margin: -1px; + padding: 5px 15px; + + border: 1px dashed transparent; + border-radius: @baseBorderRadius; + box-sizing: border-box; + + &:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; + z-index: 1; + } + + &[disabled], + &[disabled]:hover { + color: @gray-7; + border-color: @gray-7; + cursor: default; + } + + &.umb-block-list__create-button { + flex-grow: 1; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &.umb-block-list__clipboard-button { + margin-left: 0; + padding: 5px 12px; + font-size: 18px;// Align with block action buttons. + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + opacity: 0; + &:hover, &:focus { + opacity: 1; + } + + &.--jump { + + @keyframes umb-block-list__jump-clipboard-button { + 0% { opacity: 0; } + 10% { opacity: 1; } + 90% { opacity: 1; } + 100% { opacity: 0; } + } + animation: umb-block-list__jump-clipboard-button 2s; -.umb-block-list__create-button.--disabled, -.umb-block-list__create-button.--disabled:hover { - color: @gray-7; - border-color: @gray-7; - cursor: default; + i{ + @keyframes umb-block-list__jump-clipboard-button-i { + 10% { transform: scale(1); } + 10% { transform: scale(1.33); } + 20% { transform: scale(.82); } + 30% { transform: scale(1.24); } + 40% { transform: scale(.94); } + 50% { transform: scale(1); } + } + animation: umb-block-list__jump-clipboard-button-i 2s; + } + } + + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 613e6a8c6aa7..7334fbeadf5e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -28,7 +28,7 @@ } }); - function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper) { + function BlockListController($scope, $timeout, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper, eventsService) { var unsubscribe = []; var modelObject; @@ -53,6 +53,14 @@ }; vm.supportCopy = clipboardService.isSupported(); + vm.clipboardItems = []; + unsubscribe.push(eventsService.on("clipboardService.storageUpdate", updateClipboard)); + unsubscribe.push($scope.$on("editors.content.splitViewChanged", (event, eventData) => { + var compositeId = vm.umbVariantContent.editor.compositeId; + if(eventData.editors.some(x => x.compositeId === compositeId)) { + updateAllBlockObjects(); + } + })); vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model. vm.availableBlockTypes = []; // Available block entries of this property editor. @@ -64,6 +72,7 @@ }); vm.$onInit = function() { + if (vm.umbProperty && !vm.umbVariantContent) {// if we dont have vm.umbProperty, it means we are in the DocumentTypeEditor. // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope // inheritance is (i.e.infinite editing) @@ -173,6 +182,8 @@ // is invalid for some reason or the data structure has changed. invalidLayoutItems.push(entry); } + } else { + updateBlockObject(entry.$block); } }); @@ -187,12 +198,24 @@ vm.availableContentTypesAliases = modelObject.getAvailableAliasesForBlockContent(); vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker(); + updateClipboard(true); + vm.loading = false; $scope.$evalAsync(); } + function updateAllBlockObjects() { + // Update the blockObjects in our layout. + vm.layout.forEach(entry => { + // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. + if (entry.$block) { + updateBlockObject(entry.$block); + } + }); + } + function getDefaultViewForBlock(block) { var defaultViewFolderPath = "views/propertyeditors/blocklist/blocklistentryeditors/"; @@ -233,9 +256,6 @@ if (block === null) return null; - ensureCultureData(block.content); - ensureCultureData(block.settings); - block.view = (block.config.view ? block.config.view : getDefaultViewForBlock(block)); block.showValidation = block.config.view ? true : false; @@ -250,20 +270,51 @@ block.setParentForm = function (parentForm) { this._parentForm = parentForm; }; - block.activate = activateBlock.bind(null, block); - block.edit = function () { + + /** decorator methods, to enable switching out methods without loosing references that would have been made in Block Views codes */ + block.activate = function() { + this._activate(); + }; + block.edit = function() { + this._edit(); + }; + block.editSettings = function() { + this._editSettings(); + }; + block.requestDelete = function() { + this._requestDelete(); + }; + block.delete = function() { + this._delete(); + }; + block.copy = function() { + this._copy(); + }; + updateBlockObject(block); + + return block; + } + + /** As the block object now contains references to this instance of a property editor, we need to ensure that the Block Object contains latest references. + * This is a bit hacky but the only way to maintain this reference currently. + * Notice this is most relevant for invariant properties on variant documents, specially for the scenario where the scope of the reference we stored is destroyed, therefor we need to ensure we always have references to a current running property editor*/ + function updateBlockObject(block) { + + ensureCultureData(block.content); + ensureCultureData(block.settings); + + block._activate = activateBlock.bind(null, block); + block._edit = function () { var blockIndex = vm.layout.indexOf(this.layout); editBlock(this, false, blockIndex, this._parentForm); }; - block.editSettings = function () { + block._editSettings = function () { var blockIndex = vm.layout.indexOf(this.layout); editBlock(this, true, blockIndex, this._parentForm); }; - block.requestDelete = requestDeleteBlock.bind(null, block); - block.delete = deleteBlock.bind(null, block); - block.copy = copyBlock.bind(null, block); - - return block; + block._requestDelete = requestDeleteBlock.bind(null, block); + block._delete = deleteBlock.bind(null, block); + block._copy = copyBlock.bind(null, block); } function addNewBlock(index, contentElementTypeKey) { @@ -406,9 +457,34 @@ editorService.open(blockEditorModel); } - vm.showCreateDialog = showCreateDialog; + vm.requestShowCreate = requestShowCreate; + function requestShowCreate(createIndex, mouseEvent) { + + if (vm.blockTypePicker) { + return; + } - function showCreateDialog(createIndex, $event) { + if (vm.availableBlockTypes.length === 1) { + var wasAdded = false; + var blockType = vm.availableBlockTypes[0]; + + wasAdded = addNewBlock(createIndex, blockType.blockConfigModel.contentElementTypeKey); + + if(wasAdded && !(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + userFlowWhenBlockWasCreated(createIndex); + } + } else { + showCreateDialog(createIndex); + } + + } + vm.requestShowClipboard = requestShowClipboard; + function requestShowClipboard(createIndex, mouseEvent) { + showCreateDialog(createIndex, true); + } + + vm.showCreateDialog = showCreateDialog; + function showCreateDialog(createIndex, openClipboard) { if (vm.blockTypePicker) { return; @@ -424,6 +500,7 @@ $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) availableItems: vm.availableBlockTypes, title: vm.labels.grid_addElement, + openClipboard: openClipboard, orderBy: "$index", view: "views/common/infiniteeditors/blockpicker/blockpicker.html", size: (amountOfAvailableTypes > 8 ? "medium" : "small"), @@ -444,19 +521,15 @@ } }, submit: function(blockPickerModel, mouseEvent) { - var added = false; + var wasAdded = false; if (blockPickerModel && blockPickerModel.selectedItem) { - added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey); + wasAdded = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentElementTypeKey); } if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); - if (added && vm.layout.length > createIndex) { - if (inlineEditing === true) { - activateBlock(vm.layout[createIndex].$block); - } else if (inlineEditing === false && vm.layout[createIndex].$block.hideContentInOverlay !== true) { - editBlock(vm.layout[createIndex].$block, false, createIndex, blockPickerModel.$parentForm, {createFlow: true}); - } + if (wasAdded) { + userFlowWhenBlockWasCreated(createIndex); } } }, @@ -475,7 +548,28 @@ clipboardService.clearEntriesOfType(clipboardService.TYPES.BLOCK, vm.availableContentTypesAliases); }; - blockPickerModel.clipboardItems = []; + blockPickerModel.clipboardItems = vm.clipboardItems; + + // open block picker overlay + editorService.open(blockPickerModel); + + }; + function userFlowWhenBlockWasCreated(createIndex) { + if (vm.layout.length > createIndex) { + var blockObject = vm.layout[createIndex].$block; + if (inlineEditing === true) { + blockObject.activate(); + } else if (inlineEditing === false && blockObject.hideContentInOverlay !== true) { + blockObject.edit(); + } + } + } + + function updateClipboard(firstTime) { + + var oldAmount = vm.clipboardItems.length; + + vm.clipboardItems = []; var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); entriesForPaste.forEach(function (entry) { @@ -511,19 +605,33 @@ if(Array.isArray(entry.data) === false) { pasteEntry.blockConfigModel = modelObject.getBlockConfiguration(entry.data.data.contentTypeKey); } - blockPickerModel.clipboardItems.push(pasteEntry); + vm.clipboardItems.push(pasteEntry); }); - blockPickerModel.clipboardItems.sort( (a, b) => { + vm.clipboardItems.sort( (a, b) => { return b.date - a.date }); - // open block picker overlay - editorService.open(blockPickerModel); + if(firstTime !== true && vm.clipboardItems.length > oldAmount) { + jumpClipboard(); + } + } - }; + var jumpClipboardTimeout; + function jumpClipboard() { + + if(jumpClipboardTimeout) { + return; + } + + vm.jumpClipboardButton = true; + jumpClipboardTimeout = $timeout(() => { + vm.jumpClipboardButton = false; + jumpClipboardTimeout = null; + }, 2000); + } - var requestCopyAllBlocks = function () { + function requestCopyAllBlocks() { var aliases = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js index dab8d2c6f8e0..018c2b72c123 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.controller.js @@ -1,4 +1,4 @@ -function booleanEditorController($scope, angularHelper) { +function booleanEditorController($scope) { // Setup the default config // This allow to overwrite the configuration when property editor is re-used @@ -36,6 +36,12 @@ function booleanEditorController($scope, angularHelper) { } } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + setupViewModel(); if ($scope.model && !$scope.model.value) { @@ -51,7 +57,7 @@ function booleanEditorController($scope, angularHelper) { // Update the value when the toggle is clicked $scope.toggle = function(){ - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); if ($scope.renderModel.value){ $scope.model.value = $scope.model.config.falsevalue; setupViewModel(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html index 75f3f5452ae0..aa47e0c667dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/boolean/boolean.html @@ -1,11 +1,17 @@
- - + + + + + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js index 886d051f4d0d..8166255a26ee 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.controller.js @@ -63,7 +63,7 @@ function ColorPickerController($scope, $timeout) { // this is required to re-validate $timeout(function () { var newColor = color ? color.value : null; - $scope.propertyForm.selectedColor.$setViewValue(newColor); + vm.modelValueForm.selectedColor.$setViewValue(newColor); }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html index b7942b7f687d..cb5c80a6f14e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.html @@ -1,17 +1,19 @@ 
-
- You haven't defined any colors -
+ +
+ You haven't defined any colors +
- - + + - + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js index 967137b9302a..cb5905e2c903 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js @@ -104,7 +104,7 @@ return x.value === item.value && x.label === item.label; }); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } function add(evt) { @@ -130,7 +130,7 @@ $scope.newLabel = ""; $scope.hasError = false; $scope.focusOnNew = true; - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); return; } @@ -156,6 +156,12 @@ $scope.newLabel = defaultLabel; } + function setDirty() { + if (vm.modelValueForm) { + vm.modelValueForm.selectedColor.$setDirty(); + } + } + $scope.sortableOptions = { axis: 'y', containment: 'parent', @@ -164,7 +170,7 @@ items: '> div.control-group', tolerance: 'pointer', update: function (e, ui) { - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index 0f012810ba96..e818fe9a23a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -15,7 +15,7 @@ * @param {any} editorService * @param {any} userService */ -function contentPickerController($scope, $q, $routeParams, $location, entityResource, editorState, iconHelper, angularHelper, navigationService, localizationService, editorService, userService, overlayService) { +function contentPickerController($scope, $q, $routeParams, $location, entityResource, editorState, iconHelper, navigationService, localizationService, editorService, userService, overlayService) { var vm = { labels: { @@ -112,7 +112,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso scroll: true, zIndex: 6000, update: function (e, ui) { - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } }; @@ -180,7 +180,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso $scope.clear(); $scope.add(data); } - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); }, treeAlias: $scope.model.config.startNode.type, section: $scope.model.config.startNode.type, @@ -257,9 +257,9 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso _.each(model.selection, function (item, i) { $scope.add(item); }); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); editorService.close(); } @@ -288,7 +288,7 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso var currIds = $scope.model.value ? $scope.model.value.split(',') : []; if (currIds.length > 0) { currIds.splice(index, 1); - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); $scope.model.value = currIds.join(); } @@ -375,6 +375,12 @@ function contentPickerController($scope, $q, $routeParams, $location, entityReso } }); + function setDirty() { + if ($scope.contentPickerForm && $scope.contentPickerForm.modelValue) { + $scope.contentPickerForm.modelValue.$setDirty(); + } + } + /** Syncs the renderModel based on the actual model.value and returns a promise */ function syncRenderModel(doValidation) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index 1a17ea269849..8ebbaae91f83 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -19,14 +19,12 @@
- @@ -64,16 +62,18 @@
+ + -
+
You need to add at least {{model.config.minNumber}} items
-
+
You can only have {{model.config.maxNumber}} items selected
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 30b6fc4c8fbe..af1dea167a52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -161,7 +161,14 @@ function dateTimePickerController($scope, angularHelper, dateHelper, validationM else { $scope.model.value = null; } - angularHelper.getCurrentForm($scope).$setDirty(); + + setDirty(); + } + + function setDirty() { + if ($scope.datePickerForm) { + $scope.datePickerForm.datepicker.$setDirty(); + } } /** Sets the value of the date picker control adn associated viewModel objects based on the model value */ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js index c485f4bbc6e9..4f1016e68028 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.controller.js @@ -11,11 +11,14 @@ * */ function fileUploadController($scope, fileManager) { - + $scope.fileChanged = onFileChanged; //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + + $scope.fileExtensionsString = $scope.model.config.fileExtensions ? $scope.model.config.fileExtensions.map(x => "."+x.value).join(",") : ""; + /** * Called when the file selection value changes * @param {any} value @@ -38,12 +41,12 @@ files: [] }); } - + }; angular.module("umbraco") .controller('Umbraco.PropertyEditors.FileUploadController', fileUploadController) - .run(function (mediaHelper, umbRequestHelper, assetsService) { + .run(function (mediaHelper) { if (mediaHelper && mediaHelper.registerFileResolver) { //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html index 522278e99ec4..36509e894796 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/fileupload/fileupload.html @@ -4,6 +4,7 @@ property-alias="{{model.alias}}" value="model.value" required="model.validation.mandatory" - on-files-selected="fileChanged(value)"> + on-files-selected="fileChanged(value)" + accept-file-ext="fileExtensionsString">
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js index cf201976adca..fdf70693b2ef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js @@ -4,10 +4,37 @@ angular.module("umbraco") var vm = this; + vm.toggleAllowed = toggleAllowed; + vm.configureSection = configureSection; + vm.deleteSection = deleteSection; + vm.selectRow = selectRow; + vm.percentage = percentage; + vm.scaleUp = scaleUp; + vm.scaleDown = scaleDown; + vm.close = close; + vm.submit = submit; + vm.labels = {}; function init() { + $scope.currentLayout = $scope.model.currentLayout; + $scope.columns = $scope.model.columns; + $scope.rows = $scope.model.rows; + $scope.currentSection = null; + + // Setup copy of rows on sections + if ($scope.currentLayout && $scope.currentLayout.sections) { + $scope.currentLayout.sections.forEach(section => { + section.rows = Utilities.copy($scope.rows); + + // Check if rows are selected + section.rows.forEach(row => { + row.selected = section.allowed && section.allowed.includes(row.name); + }); + }); + } + var labelKeys = [ "grid_addGridLayout", "grid_allowAllRowConfigurations" @@ -28,46 +55,43 @@ angular.module("umbraco") } } - $scope.currentLayout = $scope.model.currentLayout; - $scope.columns = $scope.model.columns; - $scope.rows = $scope.model.rows; - $scope.currentSection = undefined; - - $scope.scaleUp = function(section, max, overflow){ + function scaleUp(section, max, overflow){ var add = 1; - if(overflow !== true){ - add = (max > 1) ? 1 : max; + if (overflow !== true){ + add = (max > 1) ? 1 : max; } //var add = (max > 1) ? 1 : max; section.grid = section.grid+add; - }; + } - $scope.scaleDown = function(section){ + function scaleDown(section){ var remove = (section.grid > 1) ? 1 : 0; section.grid = section.grid-remove; - }; + } - $scope.percentage = function(spans){ + function percentage(spans){ return ((spans / $scope.columns) * 100).toFixed(8); - }; + } /**************** Section *****************/ - $scope.configureSection = function(section, template){ - if(section === undefined){ + function configureSection(section, template) { + if (section === null || section === undefined) { var space = ($scope.availableLayoutSpace > 4) ? 4 : $scope.availableLayoutSpace; section = { - grid: space + grid: space, + rows: Utilities.copy($scope.rows) }; template.sections.push(section); - } - - $scope.currentSection = section; - $scope.currentSection.allowAll = section.allowAll || !section.allowed || !section.allowed.length; - }; + } + + section.allowAll = section.allowAll || !section.allowed || !section.allowed.length; - $scope.toggleAllowed = function (section) { + $scope.currentSection = section; + } + + function toggleAllowed(section) { section.allowAll = !section.allowAll; if (section.allowed) { @@ -76,21 +100,22 @@ angular.module("umbraco") else { section.allowed = []; } - }; + } - $scope.deleteSection = function(section, template) { + function deleteSection(section, template) { if ($scope.currentSection === section) { - $scope.currentSection = undefined; + $scope.currentSection = null; } var index = template.sections.indexOf(section) template.sections.splice(index, 1); - }; + } + + function selectRow(section, row) { - $scope.selectRow = function (section, row) { section.allowed = section.allowed || []; var index = section.allowed.indexOf(row.name); - if (row.allowed === true) { + if (row.selected === true) { if (index === -1) { section.allowed.push(row.name); } @@ -98,22 +123,32 @@ angular.module("umbraco") else { section.allowed.splice(index, 1); } - }; + } - $scope.close = function() { + function close() { if ($scope.model.close) { + cleanUpRows(); $scope.model.close(); } - }; + } - $scope.submit = function () { + function submit() { if ($scope.model.submit) { + cleanUpRows(); $scope.model.submit($scope.currentLayout); } - }; + } + + function cleanUpRows () { + $scope.currentLayout.sections.forEach(section => { + if (section.rows) { + delete section.rows; + } + }); + } $scope.$watch("currentLayout", function(layout){ - if(layout){ + if (layout) { var total = 0; _.forEach(layout.sections, function(section){ total = (total + section.grid); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html index 6ff18e83ad9c..5e05f56b4819 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html @@ -34,38 +34,37 @@
-
+
- {{currentSection.grid}} -
@@ -74,7 +73,7 @@ @@ -85,7 +84,7 @@
    -
  • +
  • - + on-change="vm.selectRow(currentSection, row)"> - @@ -153,13 +152,13 @@ button-style="link" label-key="general_close" shortcut="esc" - action="close()"> + action="vm.close()"> + action="vm.submit()"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js index 83a9fd53941c..b36352a66bfb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js @@ -2,10 +2,26 @@ function RowConfigController($scope, localizationService) { var vm = this; + vm.configureCell = configureCell; + vm.closeArea = closeArea; + vm.deleteArea = deleteArea; + vm.selectEditor = selectEditor; + vm.toggleAllowed = toggleAllowed; + vm.percentage = percentage; + vm.scaleUp = scaleUp; + vm.scaleDown = scaleDown; + vm.close = close; + vm.submit = submit; + vm.labels = {}; function init() { - + + $scope.currentRow = $scope.model.currentRow; + $scope.columns = $scope.model.columns; + $scope.editors = $scope.model.editors; + $scope.nameChanged = false; + var labelKeys = [ "grid_addRowConfiguration", "grid_allowAllEditors" @@ -25,12 +41,8 @@ function RowConfigController($scope, localizationService) { $scope.model.title = value; } } - - $scope.currentRow = $scope.model.currentRow; - $scope.columns = $scope.model.columns; - $scope.editors = $scope.model.editors; - $scope.scaleUp = function(section, max, overflow) { + function scaleUp(section, max, overflow) { var add = 1; if (overflow !== true) { add = (max > 1) ? 1 : max; @@ -39,19 +51,19 @@ function RowConfigController($scope, localizationService) { section.grid = section.grid + add; }; - $scope.scaleDown = function(section) { + function scaleDown(section) { var remove = (section.grid > 1) ? 1 : 0; section.grid = section.grid - remove; - }; + } - $scope.percentage = function(spans) { + function percentage(spans) { return ((spans / $scope.columns) * 100).toFixed(8); - }; + } /**************** area *****************/ - $scope.configureCell = function(cell, row) { + function configureCell(cell, row) { if ($scope.currentCell && $scope.currentCell === cell) { delete $scope.currentCell; } @@ -75,12 +87,13 @@ function RowConfigController($scope, localizationService) { $scope.editors.forEach(function (e) { e.allowed = cell.allowed.indexOf(e.alias) !== -1 }); + cell.allowAll = cell.allowAll || !cell.allowed || !cell.allowed.length; + $scope.currentCell = cell; - $scope.currentCell.allowAll = cell.allowAll || !cell.allowed || !cell.allowed.length; } - }; + } - $scope.toggleAllowed = function (cell) { + function toggleAllowed(cell) { cell.allowAll = !cell.allowAll; if (cell.allowed) { @@ -89,21 +102,22 @@ function RowConfigController($scope, localizationService) { else { cell.allowed = []; } - }; + } - $scope.deleteArea = function (cell, row) { + function deleteArea(cell, row) { if ($scope.currentCell === cell) { $scope.currentCell = null; } var index = row.areas.indexOf(cell) row.areas.splice(index, 1); - }; + } - $scope.closeArea = function() { + // This doesn't seem to be used? + function closeArea() { $scope.currentCell = null; - }; + } - $scope.selectEditor = function (cell, editor) { + function selectEditor(cell, editor) { cell.allowed = cell.allowed || []; var index = cell.allowed.indexOf(editor.alias); @@ -115,22 +129,20 @@ function RowConfigController($scope, localizationService) { else { cell.allowed.splice(index, 1); } - }; + } - $scope.close = function () { + function close () { if ($scope.model.close) { $scope.model.close(); } - }; + } - $scope.submit = function () { + function submit() { if ($scope.model.submit) { $scope.model.submit($scope.currentRow); } - }; + } - $scope.nameChanged = false; - var originalName = $scope.currentRow.name; $scope.$watch("currentRow", function(row) { if (row) { @@ -141,6 +153,7 @@ function RowConfigController($scope, localizationService) { $scope.availableRowSpace = $scope.columns - total; + var originalName = $scope.currentRow.name; if (originalName) { if (originalName != row.name) { $scope.nameChanged = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index 9d105e26295b..5cf0676526c4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -41,10 +41,10 @@
    {{currentCell.grid}} -
    @@ -84,7 +84,7 @@ - @@ -93,7 +93,7 @@ + on-change="vm.selectEditor(currentCell, editor)"> {{editor.name}} ({{editor.alias}}) @@ -132,13 +132,13 @@ button-style="link" label-key="general_close" shortcut="esc" - action="close()"> + action="vm.close()"> + action="vm.submit()"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js index 70c74d0391cb..e9d9950bdd61 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js @@ -1,6 +1,6 @@ angular.module('umbraco') .controller("Umbraco.PropertyEditors.ImageCropperController", - function ($scope, fileManager, $timeout) { + function ($scope, fileManager, $timeout, mediaHelper) { var config = Utilities.copy($scope.model.config); @@ -18,6 +18,8 @@ angular.module('umbraco') //declare a special method which will be called whenever the value has changed from the server $scope.model.onValueChanged = onValueChanged; + var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; + $scope.acceptFileExt = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); /** * Called when the umgImageGravity component updates the focal point value * @param {any} left @@ -31,7 +33,7 @@ angular.module('umbraco') }; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } /** @@ -67,7 +69,13 @@ angular.module('umbraco') function onFileSelected(value, files) { setModelValueWithSrc(value); //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); + } + + function setDirty() { + if ($scope.imageCropperForm) { + $scope.imageCropperForm.modelValue.$setDirty(); + } } function imageLoaded(isCroppable, hasDimensions) { @@ -84,7 +92,7 @@ angular.module('umbraco') if (files && files[0]) { $scope.imageSrc = files[0].fileSrc; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } } @@ -138,13 +146,13 @@ angular.module('umbraco') $scope.currentPoint = null; //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); } else { // we have a crop open already - close the crop (this will discard any changes made) close(); - // the crop editor needs a digest cycle to close down properly, otherwise its state + // the crop editor needs a digest cycle to close down properly, otherwise its state // is reused for the new crop... and that's really bad $timeout(function () { crop(targetCrop); @@ -168,7 +176,7 @@ angular.module('umbraco') $scope.close(); //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); }; function reset() { @@ -201,7 +209,7 @@ angular.module('umbraco') } //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); + setDirty(); }; function isCustomCrop(crop) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 84ddf7ee3b92..9dc1a3b91ad9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -2,6 +2,9 @@ ng-controller="Umbraco.PropertyEditors.ImageCropperController"> + + + + hide-selection="true" + accept-file-ext="acceptFileExt">
    -
    +
    @@ -22,7 +26,6 @@ width="{{currentCrop.width}}" crop="currentCrop.coordinates" center="model.value.focalPoint" - max-size="450" src="imageSrc">
    @@ -46,7 +49,7 @@
    - +
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js index bb87f0463d9b..a5ccf7a9aa05 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js @@ -1,5 +1,5 @@ //inject umbracos assetsServce and dialog service -function MarkdownEditorController($scope, $element, assetsService, editorService, angularHelper, $timeout) { +function MarkdownEditorController($scope, $element, assetsService, editorService, $timeout) { //tell the assets service to load the markdown.editor libs from the markdown editors //plugin folder @@ -41,6 +41,12 @@ function MarkdownEditorController($scope, $element, assetsService, editorService editorService.linkPicker(linkPicker); } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + assetsService .load([ "lib/markdown/markdown.converter.js", @@ -78,7 +84,7 @@ function MarkdownEditorController($scope, $element, assetsService, editorService if ($scope.model.value !== $("textarea", $element).val()) { if ($scope.markdownEditorInitComplete) { //only set dirty after init load to avoid "unsaved" dialogue when we don't want it - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); } else { $scope.markdownEditorInitComplete = true; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html index 0834c4705b4b..6f2592d20a3f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html @@ -1,8 +1,11 @@
      -
      - + +
      -
      + + +
      +
      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 f3a57224e21f..c6320a7cf2b8 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 @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", - function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, angularHelper, overlayService) { + function ($scope, entityResource, mediaHelper, $timeout, userService, localizationService, editorService, overlayService, clipboardService) { var vm = this; @@ -10,9 +10,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl vm.add = add; vm.remove = remove; + vm.copyItem = copyItem; vm.editItem = editItem; vm.showAdd = showAdd; + vm.mediaItems = []; + let selectedIds = []; + //check the pre-values for multi-picker var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; var onlyImages = $scope.model.config.onlyImages && $scope.model.config.onlyImages !== '0' ? true : false; @@ -22,9 +26,6 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.allowAddMedia = false; function setupViewModel() { - $scope.mediaItems = []; - $scope.ids = []; - $scope.isMultiPicker = multiPicker; if ($scope.model.value) { @@ -53,7 +54,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() // compares and be completely sure it works. var found = medias.find(m => m.udi.toString() === id.toString() || m.id.toString() === id.toString()); - + var mediaItem = found || { name: vm.labels.deletedItem, @@ -67,45 +68,50 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl return mediaItem; }); - medias.forEach(media => { - if (!media.extension && media.id && media.metaData) { - media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); - } + medias.forEach(media => appendMedia(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); - } + sync(); + }); + } + } - $scope.mediaItems.push(media); + function appendMedia(media) { + if (!media.extension && media.id && media.metaData) { + media.extension = mediaHelper.getFileExtension(media.metaData.MediaPath); + } - if ($scope.model.config.idType === "udi") { - $scope.ids.push(media.udi); - } else { - $scope.ids.push(media.id); - } - }); + // 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); + } - sync(); - }); + vm.mediaItems.push(media); + + if ($scope.model.config.idType === "udi") { + selectedIds.push(media.udi); + } else { + selectedIds.push(media.id); } } function sync() { - $scope.model.value = $scope.ids.join(); - removeAllEntriesAction.isDisabled = $scope.ids.length === 0; + $scope.model.value = selectedIds.join(); + removeAllEntriesAction.isDisabled = selectedIds.length === 0; + copyAllEntriesAction.isDisabled = removeAllEntriesAction.isDisabled; } function setDirty() { - angularHelper.getCurrentForm($scope).$setDirty(); + if (vm.modelValueForm) { + vm.modelValueForm.modelValue.$setDirty(); + } } function reloadUpdatedMediaItems(updatedMediaNodes) { - // because the images can be edited through the media picker we need to + // because the images can be edited through the media picker we need to // 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 + // We have to get the entities from the server because the media // can be edited without being selected - $scope.mediaItems.forEach(media => { + vm.mediaItems.forEach(media => { if (updatedMediaNodes.indexOf(media.udi) !== -1) { media.loading = true; entityResource.getById(media.udi, "Media") @@ -127,7 +133,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl ]; localizationService.localizeMany(labelKeys) - .then(function(data) { + .then(function (data) { vm.labels.deletedItem = data[0]; vm.labels.trashed = data[1]; @@ -141,7 +147,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl else { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; - } + } } // only allow users to add and edit media if they have access to the media section @@ -155,12 +161,56 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } function remove(index) { - $scope.mediaItems.splice(index, 1); - $scope.ids.splice(index, 1); + vm.mediaItems.splice(index, 1); + selectedIds.splice(index, 1); sync(); setDirty(); } + function copyAllEntries() { + if($scope.mediaItems.length > 0) { + + // gather aliases + var aliases = $scope.mediaItems.map(mediaEntity => mediaEntity.metaData.ContentTypeAlias); + + // remove duplicate aliases + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var data = $scope.mediaItems.map(mediaEntity => { return {"mediaKey": mediaEntity.key }}); + + localizationService.localize("clipboard_labelForArrayOfItems", [$scope.model.label]).then(function(localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, data, localizedLabel, "icon-thumbnail-list", $scope.model.id); + }); + } + } + + function copyItem(mediaItem) { + + var mediaEntry = {}; + mediaEntry.mediaKey = mediaItem.key; + + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaItem.metaData.ContentTypeAlias, mediaEntry, mediaItem.name, mediaItem.icon, mediaItem.udi); + } + + function pasteFromClipboard(pasteEntry, pasteType) { + + if (pasteEntry === undefined) { + return; + } + + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + + entityResource.getById(pasteEntry.mediaKey, "Media").then(function (mediaEntity) { + + if(disableFolderSelect === true && mediaEntity.metaData.ContentTypeAlias === "Folder") { + return; + } + + appendMedia(mediaEntity); + sync(); + }); + } + function editItem(item) { var mediaEditor = { id: item.id, @@ -172,9 +222,9 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if (model && model.mediaNode) { entityResource.getById(model.mediaNode.id, "Media") .then(function (mediaEntity) { - // if an image is selecting more than once + // if an image is selecting more than once // we need to update all the media items - $scope.mediaItems.forEach(media => { + vm.mediaItems.forEach(media => { if (media.id === model.mediaNode.id) { angular.extend(media, mediaEntity); media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); @@ -198,6 +248,22 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl multiPicker: multiPicker, onlyImages: onlyImages, disableFolderSelect: disableFolderSelect, + clickPasteItem: function(item, mouseEvent) { + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (pasteFromClipboard(entry, item.type)) { + indexIncrementor++; + } + }); + } else { + pasteFromClipboard(item.data, item.type); + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + editorService.close(); + } + setDirty(); + }, submit: function (model) { editorService.close(); @@ -208,13 +274,13 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } - $scope.mediaItems.push(media); + vm.mediaItems.push(media); if ($scope.model.config.idType === "udi") { - $scope.ids.push(media.udi); + selectedIds.push(media.udi); } else { - $scope.ids.push(media.id); + selectedIds.push(media.id); } }); @@ -229,6 +295,21 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl } } + + var allowedTypes = null; + if(onlyImages) { + allowedTypes = ["Image"]; // Media Type Image Alias. + } + + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, allowedTypes); + }; + + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, allowedTypes); + mediaPicker.clipboardItems.sort( (a, b) => { + return b.date - a.date + }); + editorService.mediaPicker(mediaPicker); } @@ -250,8 +331,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl overlayService.close(); }, submit: function () { - $scope.mediaItems.length = 0;// AngularJS way to empty the array. - $scope.ids.length = 0;// AngularJS way to empty the array. + vm.mediaItems.length = 0;// AngularJS way to empty the array. + selectedIds.length = 0;// AngularJS way to empty the array. sync(); setDirty(); overlayService.close(); @@ -260,6 +341,14 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl }); } + var copyAllEntriesAction = { + labelKey: 'clipboard_labelForCopyAllEntries', + labelTokens: ['Media'], + icon: "documents", + method: copyAllEntries, + isDisabled: true + } + var removeAllEntriesAction = { labelKey: 'clipboard_labelForRemoveAllEntries', labelTokens: [], @@ -267,9 +356,10 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl method: removeAllEntries, isDisabled: true }; - + if (multiPicker === true) { var propertyActions = [ + copyAllEntriesAction, removeAllEntriesAction ]; @@ -287,12 +377,12 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl cancel: ".unsortable", update: function () { setDirty(); - $timeout(function() { + $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 = $scope.mediaItems.map(media => $scope.model.config.idType === "udi" ? media.udi : media.id); - + selectedIds = vm.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 c09d7c7613f7..22a683fa4967 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 @@ -1,11 +1,11 @@
      -

      -

      +

      +

      -
        -
      • +
          +
        • @@ -46,13 +46,15 @@

    • -
    - + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html new file mode 100644 index 000000000000..5e67aafe3e0b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/mediapicker3.html @@ -0,0 +1 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js new file mode 100644 index 000000000000..b26c6f85496b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.controller.js @@ -0,0 +1,120 @@ +angular.module("umbraco").controller("Umbraco.PropertyEditors.MediaPicker3.CropConfigurationController", + function ($scope) { + + var unsubscribe = []; + + if (!$scope.model.value) { + $scope.model.value = []; + } + + $scope.setFocus = false; + + $scope.remove = function (crop, evt) { + evt.preventDefault(); + const i = $scope.model.value.indexOf(crop); + if (i > -1) { + $scope.model.value.splice(i, 1); + } + }; + + $scope.edit = function (crop, evt) { + evt.preventDefault(); + crop.editMode = true; + }; + + $scope.addNewCrop = function (evt) { + evt.preventDefault(); + + var crop = {}; + crop.editMode = true; + + $scope.model.value.push(crop); + $scope.validate(crop); + }; + + $scope.setChanges = function (crop) { + $scope.validate(crop); + if( + crop.hasWidthError !== true && + crop.hasHeightError !== true && + crop.hasAliasError !== true + ) { + crop.editMode = false; + window.dispatchEvent(new Event('resize.umbImageGravity')); + } + }; + + $scope.isEmpty = function (crop) { + return !crop.label && !crop.alias && !crop.width && !crop.height; + }; + + $scope.useForAlias = function (crop) { + if (crop.alias == null || crop.alias === "") { + crop.alias = (crop.label || "").toCamelCase(); + } + }; + + $scope.validate = function (crop) { + $scope.validateWidth(crop); + $scope.validateHeight(crop); + $scope.validateAlias(crop); + }; + + $scope.validateWidth = function (crop) { + crop.hasWidthError = !(Utilities.isNumber(crop.width) && crop.width > 0); + }; + + $scope.validateHeight = function (crop) { + crop.hasHeightError = !(Utilities.isNumber(crop.height) && crop.height > 0); + }; + + $scope.validateAlias = function (crop, $event) { + var exists = $scope.model.value.find( x => crop !== x && crop.alias === x.alias); + if (exists !== undefined || crop.alias === "") { + // alias is not valid + crop.hasAliasError = true; + } else { + // everything was good: + crop.hasAliasError = false; + } + }; + + $scope.confirmChanges = function (crop, event) { + if (event.keyCode == 13) { + $scope.setChanges(crop, event); + event.preventDefault(); + } + }; + + $scope.focusNextField = function (event) { + if (event.keyCode == 13) { + + var el = event.target; + + var inputs = Array.from(document.querySelectorAll("input:not(disabled)")); + var inputIndex = inputs.indexOf(el); + if (inputIndex > -1) { + var nextIndex = inputs.indexOf(el) +1; + + if(inputs.length > nextIndex) { + inputs[nextIndex].focus(); + event.preventDefault(); + } + } + } + }; + + $scope.sortableOptions = { + axis: 'y', + containment: 'parent', + cursor: 'move', + tolerance: 'pointer' + }; + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html new file mode 100644 index 000000000000..de7e7b176747 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.html @@ -0,0 +1,96 @@ +
    + +
    +
    +
    +
    + Label +
    +
    + Alias +
    +
    + Width +
    +
    + Height +
    +
    + Actions +
    +
    +
    +
    + +
    + +
    {{crop.label}}
    +
    {{crop.alias}}
    +
    {{crop.width}}px
    +
    {{crop.height}}px
    +
    + + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    + + +
    + +
    +
    +
    + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less new file mode 100644 index 000000000000..5f5a2d468979 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/prevalue/mediapicker3.crops.less @@ -0,0 +1,40 @@ +.umb-mediapicker3-crops { + + input.ng-invalid.ng-touched { + border-color:@formErrorBorder; + color:@formErrorBorder + } + + .umb-table button { + position: relative; + color: @ui-action-discreet-type; + margin-right: 10px; + font-size: 14px; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + +} + +.umb-mediapicker3-crops__add { + + margin-top:10px; + + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + font-weight: bold; + padding: 5px 15px; + box-sizing: border-box; + width: 100%; +} + +.umb-mediapicker3-crops__add:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html new file mode 100644 index 000000000000..aa9f50b7df17 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html @@ -0,0 +1,71 @@ +
    + + + +
    + +
    + +
    + + + + +
    + + +
    +
    + +
    +
    + + + +
    + + + + +
    +
    + Minimum %0% entries, needs %1% more. +
    + > +
    +
    +
    + Maximum %0% entries, %1% too many. +
    + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less new file mode 100644 index 000000000000..d02c0b055c90 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.less @@ -0,0 +1,13 @@ +.umb-mediapicker3 { + + .umb-media-card-grid { + padding: 20px; + border: 1px solid @inputBorder; + box-sizing: border-box; + .umb-property-editor--limit-width(); + + &.--singleMode { + max-width: 202px; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js new file mode 100644 index 000000000000..96f3126288b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.component.js @@ -0,0 +1,429 @@ +(function () { + "use strict"; + + + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbMediaPicker3PropertyEditor + * @function + * + * @description + * The component for the Media Picker property editor. + */ + angular + .module("umbraco") + .component("umbMediaPicker3PropertyEditor", { + templateUrl: "views/propertyeditors/MediaPicker3/umb-media-picker3-property-editor.html", + controller: MediaPicker3Controller, + controllerAs: "vm", + bindings: { + model: "=" + }, + require: { + propertyForm: "^form", + umbProperty: "?^umbProperty", + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors', + umbElementEditorContent: '?^^umbElementEditorContent' + } + }); + + function MediaPicker3Controller($scope, editorService, clipboardService, localizationService, overlayService, userService, entityResource) { + + var unsubscribe = []; + + // Property actions: + var copyAllMediasAction = null; + var removeAllMediasAction = null; + + var vm = this; + + vm.loading = true; + + vm.activeMediaEntry = null; + vm.supportCopy = clipboardService.isSupported(); + + vm.addMediaAt = addMediaAt; + vm.editMedia = editMedia; + vm.removeMedia = removeMedia; + vm.copyMedia = copyMedia; + + vm.labels = {}; + + localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { + vm.labels.grid_addElement = data[0]; + vm.labels.content_createEmpty = data[1]; + }); + + vm.$onInit = function() { + + vm.validationLimit = vm.model.config.validationLimit || {}; + // If single-mode we only allow 1 item as the maximum: + if(vm.model.config.multiple === false) { + vm.validationLimit.max = 1; + } + vm.model.config.crops = vm.model.config.crops || []; + vm.singleMode = vm.validationLimit.max === 1; + vm.allowedTypes = vm.model.config.filter ? vm.model.config.filter.split(",") : null; + + copyAllMediasAction = { + labelKey: "clipboard_labelForCopyAllEntries", + labelTokens: [vm.model.label], + icon: "documents", + method: requestCopyAllMedias, + isDisabled: true + }; + + removeAllMediasAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: requestRemoveAllMedia, + isDisabled: true + }; + + var propertyActions = []; + if(vm.supportCopy) { + propertyActions.push(copyAllMediasAction); + } + propertyActions.push(removeAllMediasAction); + + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); + } + + if(vm.model.value === null || !Array.isArray(vm.model.value)) { + vm.model.value = []; + } + + vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry)); + + userService.getCurrentUser().then(function (userData) { + + if (!vm.model.config.startNodeId) { + if (vm.model.config.ignoreUserStartNodes === true) { + vm.model.config.startNodeId = -1; + vm.model.config.startNodeIsVirtual = true; + } else { + vm.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; + vm.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; + } + } + + // only allow users to add and edit media if they have access to the media section + var hasAccessToMedia = userData.allowedSections.indexOf("media") !== -1; + vm.allowEdit = hasAccessToMedia; + vm.allowAdd = hasAccessToMedia; + + vm.loading = false; + }); + + }; + + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + } + + function addMediaAt(createIndex, $event) { + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: vm.singleMode !== true, + clickPasteItem: function(item, mouseEvent) { + + if (Array.isArray(item.data)) { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (requestPasteFromClipboard(createIndex + indexIncrementor, entry, item.type)) { + indexIncrementor++; + } + }); + } else { + requestPasteFromClipboard(createIndex, item.data, item.type); + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + mediaPicker.close(); + } + }, + submit: function (model) { + editorService.close(); + + var indexIncrementor = 0; + model.selection.forEach((entry) => { + var mediaEntry = {}; + mediaEntry.key = String.CreateGuid(); + mediaEntry.mediaKey = entry.key; + updateMediaEntryData(mediaEntry); + vm.model.value.splice(createIndex + indexIncrementor, 0, mediaEntry); + indexIncrementor++; + }); + + setDirty(); + }, + close: function () { + editorService.close(); + } + } + + if (vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + + mediaPicker.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType(clipboardService.TYPES.Media, vm.allowedTypes || null); + }; + + mediaPicker.clipboardItems = clipboardService.retriveEntriesOfType(clipboardService.TYPES.MEDIA, vm.allowedTypes || null); + mediaPicker.clipboardItems.sort( (a, b) => { + return b.date - a.date + }); + + editorService.mediaPicker(mediaPicker); + } + + // To be used by infinite editor. (defined here cause we need configuration from property editor) + function changeMediaFor(mediaEntry, onSuccess) { + + var mediaPicker = { + startNodeId: vm.model.config.startNodeId, + startNodeIsVirtual: vm.model.config.startNodeIsVirtual, + dataTypeKey: vm.model.dataTypeKey, + multiPicker: false, + submit: function (model) { + editorService.close(); + + model.selection.forEach((entry) => {// only one. + mediaEntry.mediaKey = entry.key; + }); + + // reset focal and crops: + mediaEntry.crops = null; + mediaEntry.focalPoint = null; + updateMediaEntryData(mediaEntry); + + if (onSuccess) { + onSuccess(); + } + }, + close: function () { + editorService.close(); + } + }; + + if (vm.model.config.filter) { + mediaPicker.filter = vm.model.config.filter; + } + + editorService.mediaPicker(mediaPicker); + } + + function resetCrop(cropEntry) { + Object.assign(cropEntry, vm.model.config.crops.find( c => c.alias === cropEntry.alias)); + cropEntry.coordinates = null; + setDirty(); + } + + function updateMediaEntryData(mediaEntry) { + + mediaEntry.crops = mediaEntry.crops || []; + mediaEntry.focalPoint = mediaEntry.focalPoint || { + left: 0.5, + top: 0.5 + }; + + // Copy config and only transfer coordinates. + var newCrops = Utilities.copy(vm.model.config.crops); + newCrops.forEach(crop => { + var oldCrop = mediaEntry.crops.filter(x => x.alias === crop.alias).shift(); + if (oldCrop && oldCrop.height === crop.height && oldCrop.width === crop.width) { + crop.coordinates = oldCrop.coordinates; + } + }); + mediaEntry.crops = newCrops; + } + + function removeMedia(media) { + var index = vm.model.value.indexOf(media); + if (index !== -1) { + vm.model.value.splice(index, 1); + } + } + + function deleteAllMedias() { + vm.model.value = []; + } + + function setActiveMedia(mediaEntryOrNull) { + vm.activeMediaEntry = mediaEntryOrNull; + } + + function editMedia(mediaEntry, options, $event) { + + if($event) + $event.stopPropagation(); + + options = options || {}; + + setActiveMedia(mediaEntry); + + var documentInfo = getDocumentNameAndIcon(); + + // make a clone to avoid editing model directly. + var mediaEntryClone = Utilities.copy(mediaEntry); + + var mediaEditorModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) + createFlow: options.createFlow === true, + documentName: documentInfo.name, + mediaEntry: mediaEntryClone, + propertyEditor: { + changeMediaFor: changeMediaFor, + resetCrop: resetCrop + }, + enableFocalPointSetter: vm.model.config.enableLocalFocalPoint || false, + view: "views/common/infiniteeditors/mediaEntryEditor/mediaEntryEditor.html", + size: "large", + submit: function(model) { + vm.model.value[vm.model.value.indexOf(mediaEntry)] = mediaEntryClone; + setActiveMedia(null) + editorService.close(); + }, + close: function(model) { + if(model.createFlow === true) { + // This means that the user cancelled the creation and we should remove the media item. + // TODO: remove new media item. + } + setActiveMedia(null) + editorService.close(); + } + }; + + // open property settings editor + editorService.open(mediaEditorModel); + } + + var getDocumentNameAndIcon = function () { + // get node name + var contentNodeName = "?"; + var contentNodeIcon = null; + if (vm.umbVariantContent) { + contentNodeName = vm.umbVariantContent.editor.content.name; + if (vm.umbVariantContentEditors) { + contentNodeIcon = vm.umbVariantContentEditors.content.icon.split(" ")[0]; + } else if (vm.umbElementEditorContent) { + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; + } + } else if (vm.umbElementEditorContent) { + contentNodeName = vm.umbElementEditorContent.model.documentType.name; + contentNodeIcon = vm.umbElementEditorContent.model.documentType.icon.split(" ")[0]; + } + + return { + name: contentNodeName, + icon: contentNodeIcon + } + }; + + var requestCopyAllMedias = function () { + var mediaKeys = vm.model.value.map(x => x.mediaKey) + entityResource.getByIds(mediaKeys, "Media").then(function (entities) { + + // gather aliases + var aliases = entities.map(mediaEntity => mediaEntity.metaData.ContentTypeAlias); + + // remove duplicate aliases + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var documentInfo = getDocumentNameAndIcon(); + + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, documentInfo.name]).then(function (localizedLabel) { + clipboardService.copyArray(clipboardService.TYPES.MEDIA, aliases, vm.model.value, localizedLabel, documentInfo.icon || "icon-thumbnail-list", vm.model.id); + }); + }); + }; + + function copyMedia(mediaEntry) { + entityResource.getById(mediaEntry.mediaKey, "Media").then(function (mediaEntity) { + clipboardService.copy(clipboardService.TYPES.MEDIA, mediaEntity.metaData.ContentTypeAlias, mediaEntry, mediaEntity.name, mediaEntity.icon, mediaEntry.key); + }); + } + + function requestPasteFromClipboard(createIndex, pasteEntry, pasteType) { + + if (pasteEntry === undefined) { + return false; + } + + pasteEntry = clipboardService.parseContentForPaste(pasteEntry, pasteType); + + pasteEntry.key = String.CreateGuid(); + updateMediaEntryData(pasteEntry); + vm.model.value.splice(createIndex, 0, pasteEntry); + + return true; + } + + function requestRemoveAllMedia() { + localizationService.localizeMany(["mediaPicker_confirmRemoveAllMediaEntryMessage", "general_remove"]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function () { + overlayService.close(); + }, + submit: function () { + deleteAllMedias(); + overlayService.close(); + } + }); + }); + } + + vm.sortableOptions = { + cursor: "grabbing", + handle: "umb-media-card", + cancel: "input,textarea,select,option", + classes: ".umb-media-card--dragging", + distance: 5, + tolerance: "pointer", + scroll: true, + update: function (ev, ui) { + setDirty(); + } + }; + + function onAmountOfMediaChanged() { + + // enable/disable property actions + if (copyAllMediasAction) { + copyAllMediasAction.isDisabled = vm.model.value.length === 0; + } + if (removeAllMediasAction) { + removeAllMediasAction.isDisabled = vm.model.value.length === 0; + } + + // validate limits: + if (vm.propertyForm && vm.validationLimit) { + + var isMinRequirementGood = vm.validationLimit.min === null || vm.model.value.length >= vm.validationLimit.min; + vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood); + + var isMaxRequirementGood = vm.validationLimit.max === null || vm.model.value.length <= vm.validationLimit.max; + vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood); + } + } + + unsubscribe.push($scope.$watch(() => vm.model.value.length, onAmountOfMediaChanged)); + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js new file mode 100644 index 000000000000..b561784d9f06 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umbMediaPicker3PropertyEditor.createButton.controller.js @@ -0,0 +1,18 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .controller("Umbraco.PropertyEditors.MediaPicker3PropertyEditor.CreateButtonController", + function Controller($scope) { + + var vm = this; + vm.plusPosY = 0; + + vm.onMouseMove = function($event) { + vm.plusPosY = $event.offsetY; + } + + }); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js index 73def3cc653b..5362cb1f1006 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.controller.js @@ -18,6 +18,12 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ }); } + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + $scope.openMemberGroupPicker = function() { var memberGroupPicker = { multiPicker: true, @@ -32,6 +38,7 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ if (newGroupIds && newGroupIds.length) { memberGroupResource.getByIds(newGroupIds).then(function (groups) { $scope.renderModel = _.union($scope.renderModel, groups); + setDirty(); editorService.close(); }); } @@ -47,10 +54,13 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ editorService.memberGroupPicker(memberGroupPicker); }; - $scope.remove =function(index){ + // TODO: I don't believe this is used + $scope.remove = function(index){ $scope.renderModel.splice(index, 1); + setDirty(); }; + // TODO: I don't believe this is used $scope.add = function (item) { var currIds = _.map($scope.renderModel, function (i) { return i.id; @@ -58,11 +68,14 @@ function memberGroupPicker($scope, editorService, memberGroupResource){ if (currIds.indexOf(item) < 0) { $scope.renderModel.push({ name: item, id: item, icon: 'icon-users' }); + setDirty(); } }; + // TODO: I don't believe this is used $scope.clear = function() { $scope.renderModel = []; + setDirty(); }; function renderModelIds() { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html index b1cafafb0d84..5a0788149e38 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergrouppicker/membergrouppicker.html @@ -1,21 +1,22 @@
    - +
    - + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js index 6662a86571db..2213841ece86 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.controller.js @@ -10,6 +10,12 @@ return selected; }; + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + $scope.pickGroup = function() { editorService.memberGroupPicker({ multiPicker: true, @@ -24,6 +30,7 @@ $scope.model.value[group.name] = true; }); }); + setDirty(); editorService.close(); }, close: function () { @@ -34,6 +41,7 @@ $scope.removeGroup = function (group) { $scope.model.value[group] = false; + setDirty(); } } angular.module('umbraco').controller("Umbraco.PropertyEditors.MemberGroupController", memberGroupController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html index 2715e11b1562..abfa628e90cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/membergroups/membergroups.html @@ -12,4 +12,9 @@ ng-click="pickGroup()"> Add + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js index c3137360e25e..315eb18ee4da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.controller.js @@ -1,6 +1,6 @@ //this controller simply tells the dialogs service to open a memberPicker window //with a specified callback, this callback will receive an object with a selection on it -function memberPickerController($scope, entityResource, iconHelper, angularHelper, editorService){ +function memberPickerController($scope, entityResource, iconHelper, editorService){ function trim(str, chr) { var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^' + chr + '+|' + chr + '+$', 'g'); @@ -27,11 +27,16 @@ function memberPickerController($scope, entityResource, iconHelper, angularHelpe } else { $scope.clear(); $scope.add(data); - } - angularHelper.getCurrentForm($scope).$setDirty(); + } } }; + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + //since most of the pre-value config's are used in the dialog options (i.e. maxNumber, minNumber, etc...) we'll merge the // pre-value config on to the dialog options if ($scope.model.config) { @@ -60,6 +65,7 @@ function memberPickerController($scope, entityResource, iconHelper, angularHelpe $scope.remove = function (index) { $scope.renderModel.splice(index, 1); + setDirty(); }; $scope.add = function (item) { @@ -76,7 +82,8 @@ function memberPickerController($scope, entityResource, iconHelper, angularHelpe if (currIds.indexOf(itemId) < 0) { item.icon = iconHelper.convertFromLegacyIcon(item.icon); - $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon}); + $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon }); + setDirty(); } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html index 99f7fffba83e..4efa7283da77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/memberpicker/memberpicker.html @@ -1,22 +1,23 @@
    - +
    - + + +
    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 172f9b224905..cfad66456daa 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 @@ -1,4 +1,4 @@ -function multiUrlPickerController($scope, angularHelper, localizationService, entityResource, iconHelper, editorService) { +function multiUrlPickerController($scope, localizationService, entityResource, iconHelper, editorService) { var vm = { labels: { @@ -16,8 +16,6 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en $scope.model.value = []; } - var currentForm = angularHelper.getCurrentForm($scope); - $scope.sortableOptions = { axis: "y", containment: "parent", @@ -27,7 +25,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en scroll: true, zIndex: 6000, update: function () { - currentForm.$setDirty(); + setDirty(); } }; @@ -66,7 +64,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en $scope.remove = function ($index) { $scope.renderModel.splice($index, 1); - currentForm.$setDirty(); + setDirty(); }; $scope.openLinkPicker = function (link, $index) { @@ -122,7 +120,7 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en link.published = true; } - currentForm.$setDirty(); + setDirty(); } editorService.close(); }, @@ -133,6 +131,12 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en editorService.linkPicker(linkPicker); }; + function setDirty() { + if ($scope.multiUrlPickerForm) { + $scope.multiUrlPickerForm.modelValue.$setDirty(); + } + } + function init() { localizationService.localizeMany(["general_recycleBin"]) .then(function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html index 503f4aac9b18..12877bff888a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html @@ -18,9 +18,9 @@
+ + 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 e1238313cca5..68d375360ecf 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 @@ -8,7 +8,7 @@ angular.module('umbraco').run(['clipboardService', function (clipboardService) { - function clearNestedContentPropertiesForStorage(prop, propClearingMethod) { + function resolveNestedContentPropertiesForPaste(prop, propClearingMethod) { // if prop.editor is "Umbraco.NestedContent" if ((typeof prop === 'object' && prop.editor === "Umbraco.NestedContent")) { @@ -17,8 +17,8 @@ for (var i = 0; i < value.length; i++) { var obj = value[i]; - // remove the key - delete obj.key; + // generate a new key. + obj.key = String.CreateGuid(); // Loop through all inner properties: for (var k in obj) { @@ -28,10 +28,10 @@ } } - clipboardService.registerClearPropertyResolver(clearNestedContentPropertiesForStorage, clipboardService.TYPES.ELEMENT_TYPE) + clipboardService.registerPastePropertyResolver(resolveNestedContentPropertiesForPaste, clipboardService.TYPES.ELEMENT_TYPE) - function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { + function resolveInnerNestedContentPropertiesForPaste(prop, propClearingMethod) { // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property data. if ((Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined)) { @@ -39,8 +39,8 @@ for (var i = 0; i < prop.length; i++) { var obj = prop[i]; - // remove the key - delete obj.key; + // generate a new key. + obj.key = String.CreateGuid(); // Loop through all inner properties: for (var k in obj) { @@ -50,7 +50,7 @@ } } - clipboardService.registerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage, clipboardService.TYPES.RAW) + clipboardService.registerPastePropertyResolver(resolveInnerNestedContentPropertiesForPaste, clipboardService.TYPES.RAW) }]); angular @@ -169,11 +169,14 @@ method: removeAllEntries, isDisabled: true }; + // helper to force the current form into the dirty state function setDirty() { - if ($scope.$parent.$parent.propertyForm) { - $scope.$parent.$parent.propertyForm.$setDirty(); + + if (vm.umbProperty) { + vm.umbProperty.setDirty(); } + }; function addNode(alias) { 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 f5f0f7f2a2c4..b0c3b88bc6c0 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 @@ -1,4 +1,4 @@ -function sliderController($scope, angularHelper) { +function sliderController($scope) { let sliderRef = null; @@ -14,7 +14,13 @@ function setModelValue(values) { $scope.model.value = values ? values.toString() : null; - angularHelper.getCurrentForm($scope).$setDirty(); + setDirty(); + } + + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } } $scope.setup = function(slider) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html index 27c59194c1c0..567280b30674 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html @@ -1,12 +1,14 @@ 
-
- - -
+ +
+ + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js index 217a9c8421f6..2b0a5326f66e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.controller.js @@ -10,6 +10,12 @@ function userPickerController($scope, iconHelper, editorService, overlayService, var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; + function setDirty() { + if ($scope.modelValueForm) { + $scope.modelValueForm.modelValue.$setDirty(); + } + } + $scope.openUserPicker = function () { var currentSelection = []; @@ -42,7 +48,7 @@ function userPickerController($scope, iconHelper, editorService, overlayService, submit: function () { $scope.renderModel.splice(index, 1); $scope.userName = ''; - + setDirty(); overlayService.close(); }, close: function () { @@ -68,11 +74,13 @@ function userPickerController($scope, iconHelper, editorService, overlayService, if (currIds.indexOf(itemId) < 0) { item.icon = item.icon ? iconHelper.convertFromLegacyIcon(item.icon) : "icon-user"; $scope.renderModel.push({ name: item.name, id: item.id, udi: item.udi, icon: item.icon, avatars: item.avatars }); + setDirty(); } }; $scope.clear = function() { $scope.renderModel = []; + setDirty(); }; var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.html index 8345317eee2f..331f5e0a94c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/userpicker/userpicker.html @@ -1,21 +1,22 @@
-
- +
+ -
+
- + - + + +
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 58baadcb3500..f3652a1273cf 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -88,7 +88,7 @@ - + @@ -348,9 +348,12 @@ False True - 8130 + 8140 / - http://localhost:8130 + http://localhost:8140 + 8131 + / + http://localhost:8131 False False diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 9e564635faf8..8e33f102c7a6 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -343,6 +343,7 @@ Kopiering af medietypen fejlede Flytning af medietypen fejlede + Auto vælg Kopiering af medlemstypen fejlede @@ -1100,6 +1101,17 @@ Mange hilsner fra Umbraco robotten Du har valgt et medie som er slettet eller lagt i papirkurven Du har valgt medier som er slettede eller lagt i papirkurven Slettet + Åben i mediebiblioteket + Skift medie + Nulstil medie beskæring + Rediger %0% på %1% + Annuller indsættelse? + + Du har foretaget ændringer til bruge af dette media. Er du sikker på at du vil annullere? + Fjern? + Fjern brugen af alle medier? + Udklipsholder + Ikke tilladt indtast eksternt link @@ -1845,6 +1857,7 @@ Mange hilsner fra Umbraco robotten Kopier %0% %0% fra %1% + Samling af %0% Fjern alle elementer Ryd udklipsholder @@ -1891,6 +1904,8 @@ Mange hilsner fra Umbraco robotten Error! The ElementType of this block does not exist anymore + Tilføj indhold + Tilføj %0% Feltet %0% bruger editor %1% som ikke er supporteret for blokke. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index dac067c92ef8..609c5892cd60 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -362,6 +362,7 @@ Failed to copy media type Failed to move media type + Auto pick Failed to copy member type @@ -1353,6 +1354,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You have picked a media item currently deleted or in the recycle bin You have picked media items currently deleted or in the recycle bin Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -2334,6 +2346,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont One or more of this item's descendants is being used in a media item One or more of this item's descendants is being used in a content item One or more of this item's descendants is being used in a member + The current item or one or more of this item's descendants is being used. Deleting this item can lead to broken links on your website. + This item or it's descendants is being used. Unpublishing can lead to broken links on your website. Please take the appropriate actions. Delete Saved Search @@ -2381,6 +2395,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Copy %0% %0% from %1% + Collection of %0% Remove all items Clear clipboard @@ -2547,6 +2562,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Error! The ElementType of this block does not exist anymore + Add content + Add %0% Property '%0%' uses editor '%1%' which is not supported in blocks. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 976383febb07..2e1b60d0c2b6 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -369,6 +369,7 @@ Failed to copy media type Failed to move media type + Auto pick Failed to copy member type @@ -1363,6 +1364,17 @@ To manage your website, simply open the Umbraco backoffice and start adding cont You have picked a media item currently deleted or in the recycle bin You have picked media items currently deleted or in the recycle bin Trashed + Open in Media Library + Change Media Item + Reset media crop + Edit %0% on %1% + Discard creation? + + You have made changes to this content. Are you sure you want to discard them? + Remove? + Remove all medias? + Clipboard + Not allowed enter external link @@ -2353,6 +2365,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont One or more of this item's descendants is being used in a media item One or more of this item's descendants is being used in a content item One or more of this item's descendants is being used in a member + The current item or or it's descendants is being used. Deleting can lead to broken links on your website. + This item or it's descendants is being used. Unpublishing can lead to broken links on your website. Please take the appropriate actions. Delete Saved Search @@ -2400,6 +2414,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Copy %0% %0% from %1% + Collection of %0% Remove all items Clear clipboard @@ -2566,6 +2581,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Error! The ElementType of this block does not exist anymore + Add content + Add %0% Property '%0%' uses editor '%1%' which is not supported in blocks. diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index 6abad820c944..8d9388949d63 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -46,6 +46,7 @@ public ContentCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publ public override void Refresh(JsonPayload[] payloads) { AppCaches.RuntimeCache.ClearOfType(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.ContentRecycleBinCacheKey); var idsRemoved = new HashSet(); var isolatedCache = AppCaches.IsolatedCaches.GetOrCreate(); @@ -53,9 +54,9 @@ public override void Refresh(JsonPayload[] payloads) foreach (var payload in payloads.Where(x => x.Id != default)) { //By INT Id - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Id)); //By GUID Key - isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + isolatedCache.Clear(RepositoryCacheKeys.GetKey(payload.Key)); _idkMap.ClearCache(payload.Id); diff --git a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs index c9caf5aaa76b..37d36fca45c5 100644 --- a/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DataTypeCacheRefresher.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.PropertyEditors.ValueConverters; using Umbraco.Core.Services; using Umbraco.Web.PublishedCache; @@ -56,6 +57,11 @@ public override void Refresh(JsonPayload[] payloads) foreach (var payload in payloads) { _idkMap.ClearCache(payload.Id); + + if (dataTypeCache.Success) + { + dataTypeCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + } } // TODO: not sure I like these? diff --git a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs index 3b2cf3e23d13..c3cd53168e27 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheBinder_Handlers.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -121,6 +122,8 @@ public void BindEvents(bool supportUnbinding = false) // bind to media events - handles all media changes Bind(() => MediaService.TreeChanged += MediaService_TreeChanged, () => MediaService.TreeChanged -= MediaService_TreeChanged); + Bind(() => MediaService.EmptiedRecycleBin += MediaService_EmptiedRecycleBin, + () => MediaService.EmptiedRecycleBin -= MediaService_EmptiedRecycleBin); // bind to content events Bind(() => ContentService.Saved += ContentService_Saved, // needed for permissions @@ -129,6 +132,8 @@ public void BindEvents(bool supportUnbinding = false) () => ContentService.Copied -= ContentService_Copied); Bind(() => ContentService.TreeChanged += ContentService_TreeChanged,// handles all content changes () => ContentService.TreeChanged -= ContentService_TreeChanged); + Bind(() => ContentService.EmptiedRecycleBin += ContentService_EmptiedRecycleBin, + () => ContentService.EmptiedRecycleBin -= ContentService_EmptiedRecycleBin); // TreeChanged should also deal with this //Bind(() => ContentService.SavedBlueprint += ContentService_SavedBlueprint, @@ -200,7 +205,12 @@ private void ContentService_TreeChanged(IContentService sender, TreeChange e) { } private void ContentService_Moved(IContentService sender, MoveEventArgs e) { } private void ContentService_Trashed(IContentService sender, MoveEventArgs e) { } - private void ContentService_EmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) { } + + private void ContentService_EmptiedRecycleBin(IContentService sender, RecycleBinEventArgs e) + { + var payloads = new[] { new ContentCacheRefresher.JsonPayload(Constants.System.RecycleBinContent, null, TreeChangeTypes.RefreshNode) }; + _distributedCache.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } private void ContentService_Published(IContentService sender, PublishEventArgs e) { } private void ContentService_Unpublished(IContentService sender, PublishEventArgs e) { } @@ -413,7 +423,12 @@ private void MediaService_Saved(IMediaService sender, SaveEventArgs e) { private void MediaService_Deleted(IMediaService sender, DeleteEventArgs e) { } private void MediaService_Moved(IMediaService sender, MoveEventArgs e) { } private void MediaService_Trashed(IMediaService sender, MoveEventArgs e) { } - private void MediaService_EmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) { } + + private void MediaService_EmptiedRecycleBin(IMediaService sender, RecycleBinEventArgs e) + { + var payloads = new[] { new MediaCacheRefresher.JsonPayload(Constants.System.RecycleBinMedia, null, TreeChangeTypes.RefreshNode) }; + _distributedCache.RefreshByPayload(MediaCacheRefresher.UniqueId, payloads); + } #endregion diff --git a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs index 0cecba7b7bec..e5f35e09c844 100644 --- a/src/Umbraco.Web/Cache/MacroCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MacroCacheRefresher.cs @@ -51,7 +51,7 @@ public override void Refresh(string json) var macroRepoCache = AppCaches.IsolatedCaches.Get(); if (macroRepoCache) { - macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + macroRepoCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); } } diff --git a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs index 1f54b62c5b09..a2c1110b8885 100644 --- a/src/Umbraco.Web/Cache/MediaCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MediaCacheRefresher.cs @@ -48,6 +48,7 @@ public override void Refresh(JsonPayload[] payloads) if (anythingChanged) { Current.AppCaches.ClearPartialViewCache(); + AppCaches.RuntimeCache.ClearByKey(CacheKeys.MediaRecycleBinCacheKey); var mediaCache = AppCaches.IsolatedCaches.Get(); @@ -61,8 +62,8 @@ public override void Refresh(JsonPayload[] payloads) // repository cache // it *was* done for each pathId but really that does not make sense // only need to do it for the current media - mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); - mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key)); + mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Id)); + mediaCache.Result.Clear(RepositoryCacheKeys.GetKey(payload.Key)); // remove those that are in the branch if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.RefreshBranch | TreeChangeTypes.Remove)) diff --git a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs index 48ae40ce3b6b..9483700a1932 100644 --- a/src/Umbraco.Web/Cache/MemberCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/MemberCacheRefresher.cs @@ -21,7 +21,7 @@ public MemberCacheRefresher(AppCaches appCaches, IdkMap idkMap) _legacyMemberRefresher = new LegacyMemberCacheRefresher(this, appCaches); } - public class JsonPayload + public class JsonPayload { [JsonConstructor] public JsonPayload(int id, string username) @@ -87,11 +87,11 @@ private void ClearCache(params JsonPayload[] payloads) _idkMap.ClearCache(p.Id); if (memberCache) { - memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id)); - memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username)); - } + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Id)); + memberCache.Result.Clear(RepositoryCacheKeys.GetKey(p.Username)); + } } - + } #endregion diff --git a/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs index c9c8b47bbf70..8899438a6a89 100644 --- a/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs @@ -35,7 +35,7 @@ public override void RefreshAll() public override void Refresh(int id) { var cache = AppCaches.IsolatedCaches.Get(); - if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); base.Refresh(id); } @@ -48,7 +48,7 @@ public override void Refresh(Guid id) public override void Remove(int id) { var cache = AppCaches.IsolatedCaches.Get(); - if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + if (cache) cache.Result.Clear(RepositoryCacheKeys.GetKey(id)); base.Remove(id); } diff --git a/src/Umbraco.Web/Cache/UserCacheRefresher.cs b/src/Umbraco.Web/Cache/UserCacheRefresher.cs index ce2cbbf754f7..ed71431fab32 100644 --- a/src/Umbraco.Web/Cache/UserCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserCacheRefresher.cs @@ -43,13 +43,13 @@ public override void Remove(int id) var userCache = AppCaches.IsolatedCaches.Get(); if (userCache) { - userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); userCache.Result.ClearByKey(CacheKeys.UserContentStartNodePathsPrefix + id); userCache.Result.ClearByKey(CacheKeys.UserMediaStartNodePathsPrefix + id); userCache.Result.ClearByKey(CacheKeys.UserAllContentStartNodesPrefix + id); userCache.Result.ClearByKey(CacheKeys.UserAllMediaStartNodesPrefix + id); } - + base.Remove(id); } diff --git a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs index cfdf8f3669af..7ef5f088d8c2 100644 --- a/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UserGroupCacheRefresher.cs @@ -58,7 +58,7 @@ public override void Remove(int id) var userGroupCache = AppCaches.IsolatedCaches.Get(); if (userGroupCache) { - userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); + userGroupCache.Result.Clear(RepositoryCacheKeys.GetKey(id)); userGroupCache.Result.ClearByKey(UserGroupRepository.GetByAliasCacheKeyPrefix); } diff --git a/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs b/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs index 1c05da890627..37697d848c65 100644 --- a/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs +++ b/src/Umbraco.Web/Dashboards/DashboardCollectionBuilder.cs @@ -10,22 +10,8 @@ namespace Umbraco.Web.Dashboards { public class DashboardCollectionBuilder : WeightedCollectionBuilderBase { - private Dictionary _customWeights = new Dictionary(); - protected override DashboardCollectionBuilder This => this; - /// - /// Changes the default weight of a dashboard - /// - /// The type of dashboard - /// The new dashboard weight - /// - public DashboardCollectionBuilder SetWeight(int weight) where T : IDashboard - { - _customWeights[typeof(T)] = weight; - return this; - } - protected override IEnumerable CreateItems(IFactory factory) { // get the manifest parser just-in-time - injecting it in the ctor would mean that @@ -47,20 +33,13 @@ private IEnumerable Merge(IEnumerable dashboardsFromCode private int GetWeight(IDashboard dashboard) { - var typeOfDashboard = dashboard.GetType(); - if(_customWeights.ContainsKey(typeOfDashboard)) - { - return _customWeights[typeOfDashboard]; - } - switch (dashboard) { case ManifestDashboard manifestDashboardDefinition: return manifestDashboardDefinition.Weight; default: - var weightAttribute = dashboard.GetType().GetCustomAttribute(false); - return weightAttribute?.Weight ?? DefaultWeight; + return GetWeight(dashboard.GetType()); } } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 55a253d847d6..e4462f88fcb4 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -704,7 +704,32 @@ public async Task PostAddFile() if (result.FormData["contentTypeAlias"] == Constants.Conventions.MediaTypes.AutoSelect) { - if (Current.Configs.Settings().Content.ImageFileTypes.Contains(ext)) + var mediaTypes = Services.MediaTypeService.GetAll(); + // Look up MediaTypes + foreach (var mediaTypeItem in mediaTypes) + { + var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile"); + if (fileProperty != null) { + var dataTypeKey = fileProperty.DataTypeKey; + var dataType = Services.DataTypeService.GetDataType(dataTypeKey); + + if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig) { + var fileExtensions = fileExtensionsConfig.FileExtensions; + if (fileExtensions != null) + { + if (fileExtensions.Where(x => x.Value == ext).Count() != 0) + { + mediaType = mediaTypeItem.Alias; + break; + } + } + } + } + + } + + // If media type is still File then let's check if it's an image. + if (mediaType == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext)) { mediaType = Constants.Conventions.MediaTypes.Image; } diff --git a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs index f39b267e18b0..766cb1e99f91 100644 --- a/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateCoreExtensions.cs @@ -28,6 +28,11 @@ public static string GetCropUrl(this IPublishedContent mediaItem, string cropAli return mediaItem.GetCropUrl(imageUrlGenerator, cropAlias: cropAlias, useCropDimensions: true); } + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, IImageUrlGenerator imageUrlGenerator, ImageCropperValue imageCropperValue) + { + return mediaItem.Url().GetCropUrl(imageUrlGenerator, imageCropperValue, 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. /// @@ -375,5 +380,11 @@ public static string GetCropUrl( return imageUrlGenerator.GetImageUrl(options); } + + public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, string alias, IImageUrlGenerator imageUrlGenerator, string cacheBusterValue) + { + return mediaWithCrops.LocalCrops.Src + mediaWithCrops.LocalCrops.GetCropUrl(alias, imageUrlGenerator, cacheBusterValue: cacheBusterValue); + + } } } diff --git a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs index dad2f9e3f335..51845946f133 100644 --- a/src/Umbraco.Web/ImageCropperTemplateExtensions.cs +++ b/src/Umbraco.Web/ImageCropperTemplateExtensions.cs @@ -1,7 +1,5 @@ using System; -using Newtonsoft.Json.Linq; using System.Globalization; -using System.Text; using Newtonsoft.Json; using Umbraco.Core; using Umbraco.Core.Composing; @@ -32,6 +30,8 @@ public static class ImageCropperTemplateExtensions /// public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator); + public static string GetCropUrl(this IPublishedContent mediaItem, string cropAlias, ImageCropperValue imageCropperValue) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, cropAlias, Current.ImageUrlGenerator, imageCropperValue); + /// /// Gets the ImageProcessor URL by the crop alias using the specified property containing the image cropper Json data on the IPublishedContent item. /// @@ -118,6 +118,13 @@ public static string GetCropUrl( ImageCropRatioMode? ratioMode = null, bool upScale = true) => ImageCropperTemplateCoreExtensions.GetCropUrl(mediaItem, Current.ImageUrlGenerator, width, height, propertyAlias, cropAlias, quality, imageCropMode, imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode, upScale); + public static string GetLocalCropUrl(this MediaWithCrops mediaWithCrops, + string alias, + string cacheBusterValue = null) + => ImageCropperTemplateCoreExtensions.GetLocalCropUrl(mediaWithCrops, alias, Current.ImageUrlGenerator, cacheBusterValue); + + + /// /// Gets the ImageProcessor URL from the image path. /// diff --git a/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs new file mode 100644 index 000000000000..859b3b35ebe6 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileExtensionConfigItem.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace Umbraco.Web.PropertyEditors +{ + public class FileExtensionConfigItem : IFileExtensionConfigItem + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs new file mode 100644 index 000000000000..55f947797a26 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfiguration.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration for the file upload address value editor. + /// + public class FileUploadConfiguration : IFileExtensionsConfig + { + [ConfigurationField("fileExtensions", "Accepted file extensions", "multivalues")] + public List FileExtensions { get; set; } = new List(); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs new file mode 100644 index 000000000000..abbd19a79315 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/FileUploadConfigurationEditor.cs @@ -0,0 +1,12 @@ +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration editor for the file upload value editor. + /// + public class FileUploadConfigurationEditor : ConfigurationEditor + { + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 052af18aa10a..a105d490be26 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -32,6 +32,10 @@ public FileUploadPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem _uploadAutoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, contentSection); } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new FileUploadConfigurationEditor(); + /// /// Creates the corresponding property value editor. /// diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs new file mode 100644 index 000000000000..c4934540c793 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfig.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Marker interface for any editor configuration that supports defining file extensions + /// + public interface IFileExtensionsConfig + { + List FileExtensions { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs new file mode 100644 index 000000000000..682e8815659a --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/IFileExtensionConfigItem.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; + +namespace Umbraco.Web.PropertyEditors +{ + public interface IFileExtensionConfigItem + { + int Id { get; set; } + + string Value { get; set; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs new file mode 100644 index 000000000000..4c3c6564a5da --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3Configuration.cs @@ -0,0 +1,60 @@ +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration for the media picker value editor. + /// + public class MediaPicker3Configuration : IIgnoreUserStartNodesConfig + { + [ConfigurationField("filter", "Accepted types", "treesourcetypepicker", + Description = "Limit to specific types")] + public string Filter { get; set; } + + [ConfigurationField("multiple", "Pick multiple items", "boolean", Description = "Outputs a IEnumerable")] + public bool Multiple { get; set; } + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of medias")] + public NumberRange ValidationLimit { get; set; } = new NumberRange(); + + public class NumberRange + { + [JsonProperty("min")] + public int? Min { get; set; } + + [JsonProperty("max")] + public int? Max { get; set; } + } + + [ConfigurationField("startNodeId", "Start node", "mediapicker")] + public Udi StartNodeId { get; set; } + + [ConfigurationField(Core.Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes, + "Ignore User Start Nodes", "boolean", + Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")] + public bool IgnoreUserStartNodes { get; set; } + + [ConfigurationField("enableLocalFocalPoint", "Enable Focal Point", "boolean")] + public bool EnableLocalFocalPoint { get; set; } + + [ConfigurationField("crops", "Image Crops", "views/propertyeditors/MediaPicker3/prevalue/mediapicker3.crops.html", Description = "Local crops, stored on document")] + public CropConfiguration[] Crops { get; set; } + + public class CropConfiguration + { + [JsonProperty("alias")] + public string Alias { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("width")] + public int Width { get; set; } + + [JsonProperty("height")] + public int Height { get; set; } + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs new file mode 100644 index 000000000000..37063aa1536f --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3ConfigurationEditor.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents the configuration editor for the media picker value editor. + /// + public class MediaPicker3ConfigurationEditor : ConfigurationEditor + { + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3ConfigurationEditor() + { + // configure fields + // this is not part of ContentPickerConfiguration, + // but is required to configure the UI editor (when editing the configuration) + + Field(nameof(MediaPicker3Configuration.StartNodeId)) + .Config = new Dictionary { { "idType", "udi" } }; + + Field(nameof(MediaPicker3Configuration.Filter)) + .Config = new Dictionary { { "itemType", "media" } }; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs new file mode 100644 index 000000000000..526b4830c8cd --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents a media picker property editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.MediaPicker3, + EditorType.PropertyValue, + "Media Picker v3", + "mediapicker3", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Media, + Icon = Constants.Icons.MediaImage)] + public class MediaPicker3PropertyEditor : DataEditor + { + /// + /// Initializes a new instance of the class. + /// + public MediaPicker3PropertyEditor(ILogger logger) + : base(logger) + { + } + + /// + protected override IConfigurationEditor CreateConfigurationEditor() => new MediaPicker3ConfigurationEditor(); + + protected override IDataValueEditor CreateValueEditor() => new MediaPicker3PropertyValueEditor(Attribute); + + internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference + { + /// + /// Note: no FromEditor() and ToEditor() methods + /// We do not want to transform the way the data is stored in the DB and would like to keep a raw JSON string + /// + public MediaPicker3PropertyValueEditor(DataEditorAttribute attribute) : base(attribute) + { + } + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + if (rawJson.IsNullOrWhiteSpace()) + yield break; + + var mediaWithCropsDtos = JsonConvert.DeserializeObject(rawJson); + + foreach (var mediaWithCropsDto in mediaWithCropsDtos) + { + yield return new UmbracoEntityReference(GuidUdi.Create(Constants.UdiEntityType.Media, mediaWithCropsDto.MediaKey)); + } + } + + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs new file mode 100644 index 000000000000..f9b2ad75e169 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerWithCropsValueConverter.cs @@ -0,0 +1,119 @@ +using Newtonsoft.Json; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + [DefaultPropertyValueConverter] + public class MediaPickerWithCropsValueConverter : PropertyValueConverterBase + { + + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + + public MediaPickerWithCropsValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + } + + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; + + /// + /// Enusre this property value convertor is for the New Media Picker with Crops aka MediaPicker 3 + /// + public override bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.Equals(Core.Constants.PropertyEditors.Aliases.MediaPicker3); + + /// + /// Check if the raw JSON value is not an empty array + /// + public override bool? IsValue(object value, PropertyValueLevel level) => value?.ToString() != "[]"; + + /// + /// What C# model type does the raw JSON return for Models & Views + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + // Check do we want to return IPublishedContent collection still or a NEW model ? + var isMultiple = IsMultipleDataType(propertyType.DataType); + return isMultiple + ? typeof(IEnumerable) + : typeof(MediaWithCrops); + } + + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source?.ToString(); + + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + var mediaItems = new List(); + var isMultiple = IsMultipleDataType(propertyType.DataType); + if (inter == null) + { + return isMultiple ? mediaItems: null; + } + + var dtos = JsonConvert.DeserializeObject>(inter.ToString()); + + foreach(var media in dtos) + { + var item = _publishedSnapshotAccessor.PublishedSnapshot.Media.GetById(media.MediaKey); + if (item != null) + { + mediaItems.Add(new MediaWithCrops + { + MediaItem = item, + LocalCrops = new ImageCropperValue + { + Crops = media.Crops, + FocalPoint = media.FocalPoint, + Src = item.Url() + } + }); + } + } + + return isMultiple ? mediaItems : FirstOrDefault(mediaItems); + } + + /// + /// Is the media picker configured to pick multiple media items + /// + /// + /// + private bool IsMultipleDataType(PublishedDataType dataType) + { + var config = dataType.ConfigurationAs(); + return config.Multiple; + } + + private object FirstOrDefault(IList mediaItems) + { + return mediaItems.Count == 0 ? null : mediaItems[0]; + } + + + /// + /// Model/DTO that represents the JSON that the MediaPicker3 stores + /// + [DataContract] + internal class MediaWithCropsDto + { + [DataMember(Name = "key")] + public Guid Key { get; set; } + + [DataMember(Name = "mediaKey")] + public Guid MediaKey { get; set; } + + [DataMember(Name = "crops")] + public IEnumerable Crops { get; set; } + + [DataMember(Name = "focalPoint")] + public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } + } + } +} diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index c851894149e7..b43717d418f7 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -174,7 +174,7 @@ public static T Value(this IPublishedContent content, string alias, string cu // else... if we have a property, at least let the converter return its own // vision of 'no value' (could be an empty enumerable) - otherwise, default - return property == null ? default : property.Value(culture, segment, fallback, defaultValue); + return property == null ? default : property.Value(culture, segment); } #endregion diff --git a/src/Umbraco.Web/PublishedPropertyExtension.cs b/src/Umbraco.Web/PublishedPropertyExtension.cs index 6e8647db475a..b431f2482822 100644 --- a/src/Umbraco.Web/PublishedPropertyExtension.cs +++ b/src/Umbraco.Web/PublishedPropertyExtension.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models.PublishedContent; @@ -36,16 +37,9 @@ public static T Value(this IPublishedProperty property, string culture = null // we have a value // try to cast or convert it var value = property.GetValue(culture, segment); - if (value is T valueAsT) - { - return valueAsT; - } - + if (value is T valueAsT) return valueAsT; var valueConverted = value.TryConvertTo(); - if (valueConverted) - { - return valueConverted.Result; - } + if (valueConverted) return valueConverted.Result; // cannot cast nor convert the value, nothing we can return but 'default' // note: we don't want to fallback in that case - would make little sense @@ -54,28 +48,14 @@ public static T Value(this IPublishedProperty property, string culture = null // we don't have a value, try fallback if (PublishedValueFallback.TryGetValue(property, culture, segment, fallback, defaultValue, out var fallbackValue)) - { return fallbackValue; - } // we don't have a value - neither direct nor fallback // give a chance to the converter to return something (eg empty enumerable) var noValue = property.GetValue(culture, segment); - if (noValue == null) - { - return default; - } - - if (noValue is T noValueAsT) - { - return noValueAsT; - } - + if (noValue is T noValueAsT) return noValueAsT; var noValueConverted = noValue.TryConvertTo(); - if (noValueConverted) - { - return noValueConverted.Result; - } + if (noValueConverted) return noValueConverted.Result; // cannot cast noValue nor convert it, nothing we can return but 'default' return default; diff --git a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs index 0045cf33dc2d..a6e4f4b450c1 100644 --- a/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs +++ b/src/Umbraco.Web/Routing/PublishedContentNotFoundHandler.cs @@ -42,7 +42,7 @@ internal void WriteOutput(HttpContext context) response.Write(""); if (string.IsNullOrWhiteSpace(_message) == false) response.Write("

" + _message + "

"); - response.Write("

This page can be replaced with a custom 404. Check the documentation for \"custom 404\".

"); + response.Write("

This page can be replaced with a custom 404. Check the documentation for Custom 404 Error Pages.

"); response.Write("

This page is intentionally left ugly ;-)

"); response.Write(""); } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index ea78ef7aa03d..6ad133c359d3 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -335,7 +335,6 @@ protected sealed override TreeNodeCollection GetTreeNodes(string id, FormDataCol "icon-trash", RecycleBinSmells, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/recyclebin")); - } return nodes; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d5c3e1638eea..b7545c053c05 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -63,7 +63,7 @@ - + 2.7.0.100 @@ -255,9 +255,17 @@ + + + + + + + + @@ -267,6 +275,7 @@ + @@ -1307,4 +1316,4 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web/UrlHelperRenderExtensions.cs b/src/Umbraco.Web/UrlHelperRenderExtensions.cs index 0f5b0557f4a1..592c88945bae 100644 --- a/src/Umbraco.Web/UrlHelperRenderExtensions.cs +++ b/src/Umbraco.Web/UrlHelperRenderExtensions.cs @@ -262,6 +262,32 @@ public static IHtmlString GetCropUrl(this UrlHelper urlHelper, return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); } + public static IHtmlString GetCropUrl(this UrlHelper urlHelper, + ImageCropperValue imageCropperValue, + string cropAlias, + int? width = null, + int? height = null, + int? quality = null, + ImageCropMode? imageCropMode = null, + ImageCropAnchor? imageCropAnchor = null, + bool preferFocalPoint = false, + bool useCropDimensions = true, + string cacheBusterValue = null, + string furtherOptions = null, + ImageCropRatioMode? ratioMode = null, + bool upScale = true, + bool htmlEncode = true) + { + if (imageCropperValue == null) return EmptyHtmlString; + + var imageUrl = imageCropperValue.Src; + var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode, + imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode, + upScale); + return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url); + } + + #endregion ///