diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 0101ac9d16..5537a46ef8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -22,7 +22,7 @@ This project and everyone participating in it, is governed by the [our Code of C [Reviews](#reviews) * [Styleguides](#styleguides) - * [The PR team](#the-pr-team) + * [The Core Contributors](#the-core-contributors-team) * [Questions?](#questions) [Working with the code](#working-with-the-code) @@ -72,7 +72,7 @@ Great question! The short version goes like this: ### Pull requests The most successful pull requests usually look a like this: - * Fill in the required template, linking your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. + * Fill in the required template (shown when starting a PR on GitHub), and link your pull request to an issue on the [issue tracker,](https://github.com/umbraco/Umbraco-CMS/issues) if applicable. * Include screenshots and animated GIFs in your pull request whenever possible. * Unit tests, while optional, are awesome. Thank you! * New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated. @@ -98,20 +98,21 @@ To be honest, we don't like rules very much. We trust you have the best of inten That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc. -### The PR team +### The Core Contributors team -The pull request team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: +The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time: - [Anders Bjerner](https://github.com/abjerner) -- [Dave Woestenborghs](https://github.com/dawoe) - [Emma Burstow](https://github.com/emmaburstow) - [Poornima Nayar](https://github.com/poornimanayar) +- [Kenn Jacobsen](https://twitter.com/KennJacobsen_DK) + These wonderful people aim to provide you with a first reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. Hq will have final sign-off and will check the work again before it is merged. ### Questions? -You can get in touch with [the PR team](#the-pr-team) in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: +You can get in touch with [the core contributors team](#the-core-contributors-team) in multiple ways; we love open conversations and we are a friendly bunch. No question you have is stupid. Any question you have usually helps out multiple people with the same question. Ask away: - If there's an existing issue on the issue tracker then that's a good place to leave questions and discuss how to start or move forward. - Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.com/forum/contributing-to-umbraco-cms/) forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction. @@ -122,9 +123,10 @@ You can get in touch with [the PR team](#the-pr-team) in multiple ways; we love In order to build the Umbraco source code locally, first make sure you have the following installed. - * Visual Studio 2017 v15.9.7+ - * Node v10+ - * npm v6.4.1+ + * [Visual Studio 2017 v15.9.7+](https://visualstudio.microsoft.com/vs/) + * [Node.js v10+](https://nodejs.org/en/download/) + * npm v6.4.1+ (installed with Node.js) + * [Git command line](https://git-scm.com/download/) The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. @@ -193,4 +195,4 @@ In this command we're syncing with the `v8/contrib` branch, but you can of cours ### And finally -We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) and use your experience to contribute to making the docs we have, even better. We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the PR team and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. +We welcome all kinds of contributions to this repository. If you don't feel you'd like to make code changes here, you can visit our [documentation repository](https://github.com/umbraco/UmbracoDocs) and use your experience to contribute to making the docs we have, even better. We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Contributors and HQ. So, if you see something on the issue tracker or pull requests you feel you can add to, please don't be shy. diff --git a/.gitignore b/.gitignore index 870d24c648..927c2ef570 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,10 @@ build/temp/ # eof /src/Umbraco.Web.UI.Client/TESTS-*.xml /src/ApiDocs/api/* + +# Acceptance tests +cypress.env.json +/src/Umbraco.Tests.AcceptanceTest/cypress/support/chainable.ts +/src/Umbraco.Tests.AcceptanceTest/package-lock.json +/src/Umbraco.Tests.AcceptanceTest/cypress/videos/ +/src/Umbraco.Tests.AcceptanceTest/cypress/screenshots/ diff --git a/build/NuSpecs/tools/Views.Web.config.install.xdt b/build/NuSpecs/tools/Views.Web.config.install.xdt index 828bb8612f..7dd2640d09 100644 --- a/build/NuSpecs/tools/Views.Web.config.install.xdt +++ b/build/NuSpecs/tools/Views.Web.config.install.xdt @@ -11,7 +11,7 @@ - + diff --git a/build/build.ps1 b/build/build.ps1 index 6e124d1508..3ba347a6dc 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -381,7 +381,7 @@ { Write-Host "Restore NuGet" Write-Host "Logging to $($this.BuildTemp)\nuget.restore.log" - $params = "-Source", $nugetsourceUmbraco + $params = "-Source", $nugetsourceUmbraco &$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\src\Umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" @params if (-not $?) { throw "Failed to restore NuGet packages." } }) @@ -535,6 +535,7 @@ # run if (-not $get) { +cd if ($command.Length -eq 0) { $command = @( "Build" ) diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs new file mode 100644 index 0000000000..984bc495b0 --- /dev/null +++ b/src/Umbraco.Core/Constants-SqlTemplates.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Core +{ + public static partial class Constants + { + public static class SqlTemplates + { + public static class VersionableRepository + { + public const string GetVersionIds = "Umbraco.Core.VersionableRepository.GetVersionIds"; + public const string GetVersion = "Umbraco.Core.VersionableRepository.GetVersion"; + public const string GetVersions = "Umbraco.Core.VersionableRepository.GetVersions"; + public const string EnsureUniqueNodeName = "Umbraco.Core.VersionableRepository.EnsureUniqueNodeName"; + public const string GetSortOrder = "Umbraco.Core.VersionableRepository.GetSortOrder"; + public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode"; + public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId"; + } + + } + } +} diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 3edad0c963..7bce23e98e 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -60,6 +60,19 @@ namespace Umbraco.Core #endregion + internal static bool IsMoving(this IContentBase entity) + { + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // When this occurs, only Path + Level + UpdateDate are being changed. In this case we can bypass a lot of the below + // operations which will make this whole operation go much faster. When moving we don't need to create + // new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsPropertyDirty(nameof(entity.Path)) + && entity.IsPropertyDirty(nameof(entity.Level)) + && entity.IsPropertyDirty(nameof(entity.UpdateDate)); + + return isMoving; + } + /// /// Removes characters that are not valid XML characters from all entity properties /// of type string. See: http://stackoverflow.com/a/961504/5018 diff --git a/src/Umbraco.Core/FactoryExtensions.cs b/src/Umbraco.Core/FactoryExtensions.cs index 8514525417..8ae2f76af3 100644 --- a/src/Umbraco.Core/FactoryExtensions.cs +++ b/src/Umbraco.Core/FactoryExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Umbraco.Core.Composing; @@ -77,15 +78,28 @@ namespace Umbraco.Core var ctorParameters = ctor.GetParameters(); var ctorArgs = new object[ctorParameters.Length]; + var availableArgs = new List(args); var i = 0; foreach (var parameter in ctorParameters) { // no! IsInstanceOfType is not ok here // ReSharper disable once UseMethodIsInstanceOfType - var arg = args?.FirstOrDefault(a => parameter.ParameterType.IsAssignableFrom(a.GetType())); - ctorArgs[i++] = arg ?? factory.GetInstance(parameter.ParameterType); + var idx = availableArgs.FindIndex(a => parameter.ParameterType.IsAssignableFrom(a.GetType())); + if(idx >= 0) + { + // Found a suitable supplied argument + ctorArgs[i++] = availableArgs[idx]; + + // A supplied argument can be used at most once + availableArgs.RemoveAt(idx); + } + else + { + // None of the provided arguments is suitable: get an instance from the factory + ctorArgs[i++] = factory.GetInstance(parameter.ParameterType); + } } return ctor.Invoke(ctorArgs); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs new file mode 100644 index 0000000000..9f0f147083 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReport.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReport + { + public ContentDataIntegrityReport(IReadOnlyDictionary detectedIssues) + { + DetectedIssues = detectedIssues; + } + + public bool Ok => DetectedIssues.Count == 0 || DetectedIssues.Count == DetectedIssues.Values.Count(x => x.Fixed); + + public IReadOnlyDictionary DetectedIssues { get; } + + public IReadOnlyDictionary FixedIssues + => DetectedIssues.Where(x => x.Value.Fixed).ToDictionary(x => x.Key, x => x.Value); + + public enum IssueType + { + /// + /// The item's level and path are inconsistent with it's parent's path and level + /// + InvalidPathAndLevelByParentId, + + /// + /// The item's path doesn't contain all required parts + /// + InvalidPathEmpty, + + /// + /// The item's path parts are inconsistent with it's level value + /// + InvalidPathLevelMismatch, + + /// + /// The item's path does not end with it's own ID + /// + InvalidPathById, + + /// + /// The item's path does not have it's parent Id as the 2nd last entry + /// + InvalidPathByParentId, + } + } +} diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs new file mode 100644 index 0000000000..517b9e80dc --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportEntry.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReportEntry + { + public ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType issueType) + { + IssueType = issueType; + } + + public ContentDataIntegrityReport.IssueType IssueType { get; } + public bool Fixed { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs new file mode 100644 index 0000000000..c4689467c1 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentDataIntegrityReportOptions.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models +{ + public class ContentDataIntegrityReportOptions + { + /// + /// Set to true to try to automatically resolve data integrity issues + /// + public bool FixIssues { get; set; } + + // TODO: We could define all sorts of options for the data integrity check like what to check for, what to fix, etc... + // things like Tag data consistency, etc... + } +} diff --git a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs index 225e29a8a1..f27feba8cf 100644 --- a/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs +++ b/src/Umbraco.Core/Models/Editors/ContentPropertyFile.cs @@ -16,6 +16,11 @@ /// public string Culture { get; set; } + /// + /// When dealing with content variants, this is the segment for the variant + /// + public string Segment { get; set; } + /// /// An array of metadata that is parsed out from the file info posted to the server which is set on the client. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs index 0810f2207b..39b9f038be 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ILivePublishedModelFactory.cs @@ -1,5 +1,21 @@ namespace Umbraco.Core.Models.PublishedContent { + /// + /// Provides a live published model creation service. + /// + public interface ILivePublishedModelFactory2 : ILivePublishedModelFactory + { + /// + /// Tells the factory that it should build a new generation of models + /// + void Reset(); + + /// + /// If the live model factory + /// + bool Enabled { get; } + } + /// /// Provides a live published model creation service. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index b11e991118..458b63ade3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -105,13 +105,13 @@ namespace Umbraco.Core.Models.PublishedContent { "Email", Constants.DataTypes.Textbox }, { "Username", Constants.DataTypes.Textbox }, { "PasswordQuestion", Constants.DataTypes.Textbox }, - { "Comments", Constants.DataTypes.Textbox }, + { "Comments", Constants.DataTypes.Textarea }, { "IsApproved", Constants.DataTypes.Boolean }, { "IsLockedOut", Constants.DataTypes.Boolean }, - { "LastLockoutDate", Constants.DataTypes.DateTime }, - { "CreateDate", Constants.DataTypes.DateTime }, - { "LastLoginDate", Constants.DataTypes.DateTime }, - { "LastPasswordChangeDate", Constants.DataTypes.DateTime }, + { "LastLockoutDate", Constants.DataTypes.LabelDateTime }, + { "CreateDate", Constants.DataTypes.LabelDateTime }, + { "LastLoginDate", Constants.DataTypes.LabelDateTime }, + { "LastPasswordChangeDate", Constants.DataTypes.LabelDateTime } }; #region Content type diff --git a/src/Umbraco.Core/Models/RelationTypeExtensions.cs b/src/Umbraco.Core/Models/RelationTypeExtensions.cs new file mode 100644 index 0000000000..05f438de7a --- /dev/null +++ b/src/Umbraco.Core/Models/RelationTypeExtensions.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Core.Models +{ + internal static class RelationTypeExtensions + { + internal static bool IsSystemRelationType(this IRelationType relationType) => + relationType.Alias == Constants.Conventions.RelationTypes.RelatedDocumentAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelatedMediaAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateDocumentOnCopyAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias + || relationType.Alias == Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + } +} diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index 33dabe1b24..92d397520e 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -134,15 +134,16 @@ namespace Umbraco.Core.Persistence.Factories // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { - var isInvariantValue = propertyValue.Culture == null; - var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null; + var isInvariantValue = propertyValue.Culture == null && propertyValue.Segment == null; + var isCultureValue = propertyValue.Culture != null; + var isSegmentValue = propertyValue.Segment != null; // deal with published value - if (propertyValue.PublishedValue != null && publishedVersionId > 0) + if ((propertyValue.PublishedValue != null || isSegmentValue) && publishedVersionId > 0) propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); // deal with edit value - if (propertyValue.EditedValue != null) + if (propertyValue.EditedValue != null || isSegmentValue) propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs index acfa51f895..152dcbe6d3 100644 --- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs +++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs @@ -18,6 +18,48 @@ namespace Umbraco.Core.Persistence /// public static partial class NPocoDatabaseExtensions { + /// + /// Iterates over the result of a paged data set with a db reader + /// + /// + /// + /// + /// The number of rows to load per page + /// + /// + /// + /// + /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to + /// iterate over each row with a reader using Query vs Fetch. + /// + internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) + { + var sqlString = sql.SQL; + var sqlArgs = sql.Arguments; + + int? itemCount = null; + long pageIndex = 0; + do + { + // Get the paged queries + database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var sqlCount, out var sqlPage); + + // get the item count once + if (itemCount == null) + { + itemCount = database.ExecuteScalar(sqlCount, sqlArgs); + } + pageIndex++; + + // iterate over rows without allocating all items to memory (Query vs Fetch) + foreach (var row in database.Query(sqlPage, sqlArgs)) + { + yield return row; + } + + } while ((pageIndex * pageSize) < itemCount); + } + // NOTE // // proper way to do it with TSQL and SQLCE diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs index 217719e144..ad9e2d27c1 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; @@ -77,5 +78,7 @@ namespace Umbraco.Core.Persistence.Repositories /// Here, can be null but cannot. IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, IQuery filter, Ordering ordering); + + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 13b687eb4e..845006891d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -83,7 +83,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets all version ids, current first public virtual IEnumerable GetVersionIds(int nodeId, int maxRows) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersionIds", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersionIds, tsql => tsql.Select(x => x.Id) .From() .Where(x => x.NodeId == SqlTemplate.Arg("nodeId")) @@ -99,7 +99,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the version we want to delete - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersion", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersion, tsql => tsql.Select().From().Where(x => x.Id == SqlTemplate.Arg("versionId")) ); var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); @@ -121,7 +121,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // TODO: test object node type? // get the versions we want to delete, excluding the current one - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetVersions", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetVersions, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) ); var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); @@ -403,7 +403,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // content type alias is invariant - if(ordering.OrderBy.InvariantEquals("contentTypeAlias")) + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) { var joins = Sql() .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype"); @@ -477,6 +477,123 @@ namespace Umbraco.Core.Persistence.Repositories.Implement IQuery filter, Ordering ordering); + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + { + var report = new Dictionary(); + + var sql = SqlContext.Sql() + .Select() + .From() + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); + + var nodesToRebuild = new Dictionary>(); + var validNodes = new Dictionary(); + var rootIds = new[] {Constants.System.Root, Constants.System.RecycleBinContent, Constants.System.RecycleBinMedia}; + var currentParentIds = new HashSet(rootIds); + var prevParentIds = currentParentIds; + var lastLevel = -1; + + // use a forward cursor (query) + foreach (var node in Database.Query(sql)) + { + if (node.Level != lastLevel) + { + // changing levels + prevParentIds = currentParentIds; + currentParentIds = null; + lastLevel = node.Level; + } + + if (currentParentIds == null) + { + // we're reset + currentParentIds = new HashSet(); + } + + currentParentIds.Add(node.NodeId); + + // paths parts without the roots + var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray(); + + if (!prevParentIds.Contains(node.ParentId)) + { + // invalid, this will be because the level is wrong (which prob means path is wrong too) + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathAndLevelByParentId)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (pathParts.Length == 0) + { + // invalid path + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathEmpty)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (pathParts.Length != node.Level) + { + // invalid, either path or level is wrong + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathLevelMismatch)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (pathParts[pathParts.Length - 1] != node.NodeId.ToString()) + { + // invalid path + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathById)); + AppendNodeToFix(nodesToRebuild, node); + } + else if (!rootIds.Contains(node.ParentId) && pathParts[pathParts.Length - 2] != node.ParentId.ToString()) + { + // invalid path + report.Add(node.NodeId, new ContentDataIntegrityReportEntry(ContentDataIntegrityReport.IssueType.InvalidPathByParentId)); + AppendNodeToFix(nodesToRebuild, node); + } + else + { + // it's valid! + + // don't track unless we are configured to fix + if (options.FixIssues) + validNodes.Add(node.NodeId, node); + } + } + + var updated = new List(); + + if (options.FixIssues) + { + // iterate all valid nodes to see if these are parents for invalid nodes + foreach (var (nodeId, node) in validNodes) + { + if (!nodesToRebuild.TryGetValue(nodeId, out var invalidNodes)) continue; + + // now we can try to rebuild the invalid paths. + + foreach (var invalidNode in invalidNodes) + { + invalidNode.Level = (short)(node.Level + 1); + invalidNode.Path = node.Path + "," + invalidNode.NodeId; + updated.Add(invalidNode); + } + } + + foreach (var node in updated) + { + Database.Update(node); + if (report.TryGetValue(node.NodeId, out var entry)) + entry.Fixed = true; + } + } + + return new ContentDataIntegrityReport(report); + } + + private static void AppendNodeToFix(IDictionary> nodesToRebuild, NodeDto node) + { + if (nodesToRebuild.TryGetValue(node.ParentId, out var childIds)) + childIds.Add(node); + else + nodesToRebuild[node.ParentId] = new List { node }; + } + // here, filter can be null and ordering cannot protected IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, @@ -770,7 +887,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual string EnsureUniqueNodeName(int parentId, string nodeName, int id = 0) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.EnsureUniqueNodeName", tsql => tsql + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql .Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name")) .From() .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))); @@ -783,7 +900,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetNewChildSortOrder(int parentId, int first) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetSortOrder", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From().Where(x => x.ParentId == SqlTemplate.Arg("parentId") && x.NodeObjectType == NodeObjectTypeId) ); @@ -792,7 +909,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual NodeDto GetParentNodeDto(int parentId) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetParentNode", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("parentId")) ); @@ -801,7 +918,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected virtual int GetReservedId(Guid uniqueId) { - var template = SqlContext.Templates.Get("Umbraco.Core.VersionableRepository.GetReservedId", tsql => + var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql.Select(x => x.NodeId).From().Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation) ); var id = Database.ExecuteScalar(template.Sql(new { uniqueId = uniqueId })); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index ccfa8209fb..db1e2b350d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -321,7 +321,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .InnerJoin() .On((c, d) => c.Id == d.Id) .Where(x => x.NodeId == SqlTemplate.Arg("nodeId") && !x.Current && x.VersionDate < SqlTemplate.Arg("versionDate")) - .Where( x => !x.Published) + .Where(x => !x.Published) ); var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); foreach (var versionDto in versionDtos) @@ -519,8 +519,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IContent entity) { - var entityBase = entity as EntityBase; - var isEntityDirty = entityBase != null && entityBase.IsDirty(); + var isEntityDirty = entity.IsDirty(); // check if we need to make any database changes at all if ((entity.PublishedState == PublishedState.Published || entity.PublishedState == PublishedState.Unpublished) @@ -535,29 +534,41 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // update entity.UpdatingEntity(); + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); + // TODO: I'm sure we can also detect a "Copy" (of a descendant) operation and probably perform similar checks below. + // There is probably more stuff that would be required for copying but I'm sure not all of this logic would be, we could more than likely boost + // copy performance by 95% just like we did for Move + + var publishing = entity.PublishedState == PublishedState.Publishing; - // check if we need to create a new version - if (publishing && entity.PublishedVersionId > 0) + if (!isMoving) { - // published version is not published anymore - Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); - } + // check if we need to create a new version + if (publishing && entity.PublishedVersionId > 0) + { + // published version is not published anymore + Database.Execute(Sql().Update(u => u.Set(x => x.Published, false)).Where(x => x.Id == entity.PublishedVersionId)); + } - // sanitize names - SanitizeNames(entity, publishing); + // sanitize names + SanitizeNames(entity, publishing); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) - { - var parent = GetParentNodeDto(entity.ParentId); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty("ParentId")) + { + var parent = GetParentNodeDto(entity.ParentId); + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } } // create the dto @@ -568,146 +579,152 @@ namespace Umbraco.Core.Persistence.Repositories.Implement nodeDto.ValidatePathWithException(); Database.Update(nodeDto); - // update the content dto - Database.Update(dto.ContentDto); - - // update the content & document version dtos - var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; - var documentVersionDto = dto.DocumentVersionDto; - if (publishing) + if (!isMoving) { - documentVersionDto.Published = true; // now published - contentVersionDto.Current = false; // no more current - } - Database.Update(contentVersionDto); - Database.Update(documentVersionDto); + // update the content dto + Database.Update(dto.ContentDto); - // and, if publishing, insert new content & document version dtos - if (publishing) - { - entity.PublishedVersionId = entity.VersionId; - - contentVersionDto.Id = 0; // want a new id - contentVersionDto.Current = true; // current version - contentVersionDto.Text = entity.Name; - Database.Insert(contentVersionDto); - entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - - documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); - } - - // replace the property data (rather than updating) - // only need to delete for the version that existed, the new version (if any) has no property data yet - var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; - var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deletePropertyDataSql); - - // insert property data - var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0, - entity.Properties, LanguageRepository, out var edited, out var editedCultures); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); - - // if !publishing, we may have a new name != current publish name, - // also impacts 'edited' - if (!publishing && entity.PublishName != entity.Name) - edited = true; - - if (entity.ContentType.VariesByCulture()) - { - // bump dates to align cultures to version + // update the content & document version dtos + var contentVersionDto = dto.DocumentVersionDto.ContentVersionDto; + var documentVersionDto = dto.DocumentVersionDto; if (publishing) - entity.AdjustDates(contentVersionDto.VersionDate); + { + documentVersionDto.Published = true; // now published + contentVersionDto.Current = false; // no more current + } + Database.Update(contentVersionDto); + Database.Update(documentVersionDto); - // names also impact 'edited' - // ReSharper disable once UseDeconstruction - foreach (var cultureInfo in entity.CultureInfos) - if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) - { - edited = true; - (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + // and, if publishing, insert new content & document version dtos + if (publishing) + { + entity.PublishedVersionId = entity.VersionId; - // TODO: change tracking - // at the moment, we don't do any dirty tracking on property values, so we don't know whether the - // culture has just been edited or not, so we don't update its update date - that date only changes - // when the name is set, and it all works because the controller does it - but, if someone uses a - // service to change a property value and save (without setting name), the update date does not change. - } + contentVersionDto.Id = 0; // want a new id + contentVersionDto.Current = true; // current version + contentVersionDto.Text = entity.Name; + Database.Insert(contentVersionDto); + entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id - // replace the content version variations (rather than updating) + documentVersionDto.Published = false; // non-published version + Database.Insert(documentVersionDto); + } + + // replace the property data (rather than updating) // only need to delete for the version that existed, the new version (if any) has no property data yet - var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); - Database.Execute(deleteContentVariations); + var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId; + var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deletePropertyDataSql); - // replace the document version variations (rather than updating) - var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); - Database.Execute(deleteDocumentVariations); + // insert property data + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0, + entity.Properties, LanguageRepository, out var edited, out var editedCultures); + foreach (var propertyDataDto in propertyDataDtos) + Database.Insert(propertyDataDto); - // TODO: NPoco InsertBulk issue? - // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) - // but by using SQL Server and updating a variants name will cause: Unable to cast object of type - // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. - // (same in PersistNewItem above) + // if !publishing, we may have a new name != current publish name, + // also impacts 'edited' + if (!publishing && entity.PublishName != entity.Name) + edited = true; - // insert content variations - Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + if (entity.ContentType.VariesByCulture()) + { + // bump dates to align cultures to version + if (publishing) + entity.AdjustDates(contentVersionDto.VersionDate); - // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); + // names also impact 'edited' + // ReSharper disable once UseDeconstruction + foreach (var cultureInfo in entity.CultureInfos) + if (cultureInfo.Name != entity.GetPublishName(cultureInfo.Culture)) + { + edited = true; + (editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(cultureInfo.Culture); + + // TODO: change tracking + // at the moment, we don't do any dirty tracking on property values, so we don't know whether the + // culture has just been edited or not, so we don't update its update date - that date only changes + // when the name is set, and it all works because the controller does it - but, if someone uses a + // service to change a property value and save (without setting name), the update date does not change. + } + + // replace the content version variations (rather than updating) + // only need to delete for the version that existed, the new version (if any) has no property data yet + var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete); + Database.Execute(deleteContentVariations); + + // replace the document version variations (rather than updating) + var deleteDocumentVariations = Sql().Delete().Where(x => x.NodeId == entity.Id); + Database.Execute(deleteDocumentVariations); + + // TODO: NPoco InsertBulk issue? + // we should use the native NPoco InsertBulk here but it causes problems (not sure exactly all scenarios) + // but by using SQL Server and updating a variants name will cause: Unable to cast object of type + // 'Umbraco.Core.Persistence.FaultHandling.RetryDbConnection' to type 'System.Data.SqlClient.SqlConnection'. + // (same in PersistNewItem above) + + // insert content variations + Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); + + // insert document variations + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); + } + + // refresh content + entity.SetCultureEdited(editedCultures); + + // update the document dto + // at that point, when un/publishing, the entity still has its old Published value + // so we need to explicitly update the dto to persist the correct value + if (entity.PublishedState == PublishedState.Publishing) + dto.Published = true; + else if (entity.PublishedState == PublishedState.Unpublishing) + dto.Published = false; + entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited + Database.Update(dto); + + //update the schedule + if (entity.IsPropertyDirty("ContentSchedule")) + PersistContentSchedule(entity, true); + + // if entity is publishing, update tags, else leave tags there + // means that implicitly unpublished, or trashed, entities *still* have tags in db + if (entity.PublishedState == PublishedState.Publishing) + SetEntityTags(entity, _tagRepository); } - // refresh content - entity.SetCultureEdited(editedCultures); - - // update the document dto - // at that point, when un/publishing, the entity still has its old Published value - // so we need to explicitly update the dto to persist the correct value - if (entity.PublishedState == PublishedState.Publishing) - dto.Published = true; - else if (entity.PublishedState == PublishedState.Unpublishing) - dto.Published = false; - entity.Edited = dto.Edited = !dto.Published || edited; // if not published, always edited - Database.Update(dto); - - //update the schedule - if (entity.IsPropertyDirty("ContentSchedule")) - PersistContentSchedule(entity, true); - - // if entity is publishing, update tags, else leave tags there - // means that implicitly unpublished, or trashed, entities *still* have tags in db - if (entity.PublishedState == PublishedState.Publishing) - SetEntityTags(entity, _tagRepository); - // trigger here, before we reset Published etc OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); - // flip the entity's published property - // this also flips its published state - if (entity.PublishedState == PublishedState.Publishing) + if (!isMoving) { - entity.Published = true; - entity.PublishTemplateId = entity.TemplateId; - entity.PublisherId = entity.WriterId; - entity.PublishName = entity.Name; - entity.PublishDate = entity.UpdateDate; + // flip the entity's published property + // this also flips its published state + if (entity.PublishedState == PublishedState.Publishing) + { + entity.Published = true; + entity.PublishTemplateId = entity.TemplateId; + entity.PublisherId = entity.WriterId; + entity.PublishName = entity.Name; + entity.PublishDate = entity.UpdateDate; - SetEntityTags(entity, _tagRepository); + SetEntityTags(entity, _tagRepository); + } + else if (entity.PublishedState == PublishedState.Unpublishing) + { + entity.Published = false; + entity.PublishTemplateId = null; + entity.PublisherId = null; + entity.PublishName = null; + entity.PublishDate = null; + + ClearEntityTags(entity, _tagRepository); + } + + PersistRelations(entity); + + // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? } - else if (entity.PublishedState == PublishedState.Unpublishing) - { - entity.Published = false; - entity.PublishTemplateId = null; - entity.PublisherId = null; - entity.PublishName = null; - entity.PublishDate = null; - - ClearEntityTags(entity, _tagRepository); - } - - PersistRelations(entity); - - // TODO: note re. tags: explicitly unpublished entities have cleared tags, but masked or trashed entities *still* have tags in the db - so what? entity.ResetDirtyProperties(); @@ -1183,7 +1200,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (temp.Template2Id.HasValue && templates.ContainsKey(temp.Template2Id.Value)) temp.Content.PublishTemplateId = temp.Template2Id; } - + // set properties if (loadProperties) @@ -1216,7 +1233,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement SetVariations(temp.Content, contentVariations, documentVariations); } } - + foreach (var c in content) @@ -1430,7 +1447,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement yield return dto; } - + } private class ContentVariation diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs index 081efcfdf6..242f21c749 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaRepository.cs @@ -219,7 +219,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistNewItem(IMedia entity) { - var media = (Models.Media) entity; entity.AddingEntity(); // ensure unique name on the same level @@ -274,15 +273,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentVersionDto.NodeId = nodeDto.NodeId; contentVersionDto.Current = true; Database.Insert(contentVersionDto); - media.VersionId = contentVersionDto.Id; + entity.VersionId = contentVersionDto.Id; // persist the media version dto var mediaVersionDto = dto.MediaVersionDto; - mediaVersionDto.Id = media.VersionId; + mediaVersionDto.Id = entity.VersionId; Database.Insert(mediaVersionDto); // persist the property data - var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); foreach (var propertyDataDto in propertyDataDtos) Database.Insert(propertyDataDto); @@ -298,26 +297,32 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override void PersistUpdatedItem(IMedia entity) { - var media = (Models.Media) entity; - // update - media.UpdatingEntity(); + entity.UpdatingEntity(); - // ensure unique name on the same level - entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); + // Check if this entity is being moved as a descendant as part of a bulk moving operations. + // In this case we can bypass a lot of the below operations which will make this whole operation go much faster. + // When moving we don't need to create new versions, etc... because we cannot roll this operation back anyways. + var isMoving = entity.IsMoving(); - // ensure that strings don't contain characters that are invalid in xml - // TODO: do we really want to keep doing this here? - entity.SanitizeEntityPropertiesForXmlStorage(); - - // if parent has changed, get path, level and sort order - if (entity.IsPropertyDirty("ParentId")) + if (!isMoving) { - var parent = GetParentNodeDto(entity.ParentId); + // ensure unique name on the same level + entity.Name = EnsureUniqueNodeName(entity.ParentId, entity.Name, entity.Id); - entity.Path = string.Concat(parent.Path, ",", entity.Id); - entity.Level = parent.Level + 1; - entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + // ensure that strings don't contain characters that are invalid in xml + // TODO: do we really want to keep doing this here? + entity.SanitizeEntityPropertiesForXmlStorage(); + + // if parent has changed, get path, level and sort order + if (entity.IsPropertyDirty(nameof(entity.ParentId))) + { + var parent = GetParentNodeDto(entity.ParentId); + + entity.Path = string.Concat(parent.Path, ",", entity.Id); + entity.Level = parent.Level + 1; + entity.SortOrder = GetNewChildSortOrder(entity.ParentId, 0); + } } // create the dto @@ -328,26 +333,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement nodeDto.ValidatePathWithException(); Database.Update(nodeDto); - // update the content dto - Database.Update(dto.ContentDto); + if (!isMoving) + { + // update the content dto + Database.Update(dto.ContentDto); - // update the content & media version dtos - var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; - var mediaVersionDto = dto.MediaVersionDto; - contentVersionDto.Current = true; - Database.Update(contentVersionDto); - Database.Update(mediaVersionDto); + // update the content & media version dtos + var contentVersionDto = dto.MediaVersionDto.ContentVersionDto; + var mediaVersionDto = dto.MediaVersionDto; + contentVersionDto.Current = true; + Database.Update(contentVersionDto); + Database.Update(mediaVersionDto); - // replace the property data - var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == media.VersionId); - Database.Execute(deletePropertyDataSql); - var propertyDataDtos = PropertyFactory.BuildDtos(media.ContentType.Variations, media.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); - foreach (var propertyDataDto in propertyDataDtos) - Database.Insert(propertyDataDto); + // replace the property data + var deletePropertyDataSql = SqlContext.Sql().Delete().Where(x => x.VersionId == entity.VersionId); + Database.Execute(deletePropertyDataSql); + var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, 0, entity.Properties, LanguageRepository, out _, out _); + foreach (var propertyDataDto in propertyDataDtos) + Database.Insert(propertyDataDto); - SetEntityTags(entity, _tagRepository); + SetEntityTags(entity, _tagRepository); - PersistRelations(entity); + PersistRelations(entity); + } OnUowRefreshedEntity(new ScopedEntityEventArgs(AmbientScope, entity)); diff --git a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs index ac8c9f1be7..697e3fd11f 100644 --- a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs +++ b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs @@ -17,6 +17,20 @@ namespace Umbraco.Core /// public static bool IsLiveFactory(this IPublishedModelFactory factory) => factory is ILivePublishedModelFactory; + /// + /// Returns true if the current is an implementation of and is enabled + /// + /// + /// + public static bool IsLiveFactoryEnabled(this IPublishedModelFactory factory) + { + if (factory is ILivePublishedModelFactory2 liveFactory2) + return liveFactory2.Enabled; + + // if it's not ILivePublishedModelFactory2 we can't determine if it's enabled or not so return true + return factory is ILivePublishedModelFactory; + } + [Obsolete("This method is no longer used or necessary and will be removed from future")] [EditorBrowsable(EditorBrowsableState.Never)] public static void WithSafeLiveFactory(this IPublishedModelFactory factory, Action action) @@ -50,15 +64,17 @@ namespace Umbraco.Core { lock (liveFactory.SyncRoot) { - // TODO: Fix this in 8.3! - We need to change the ILivePublishedModelFactory interface to have a Reset method and then when we have an embedded MB - // version we will publicize the ResetModels (and change the name to Reset). - // For now, this will suffice and we'll use reflection, there should be no other implementation of ILivePublishedModelFactory. - // Calling ResetModels resets the MB flag so that the next time EnsureModels is called (which is called when nucache lazily calls CreateModel) it will - // trigger the recompiling of pure live models. - var resetMethod = liveFactory.GetType().GetMethod("ResetModels", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); - if (resetMethod != null) - resetMethod.Invoke(liveFactory, null); - + if (liveFactory is ILivePublishedModelFactory2 liveFactory2) + { + liveFactory2.Reset(); + } + else + { + // This is purely here for backwards compat and to avoid breaking changes but this code will probably never get executed + var resetMethod = liveFactory.GetType().GetMethod("ResetModels", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + if (resetMethod != null) + resetMethod.Invoke(liveFactory, null); + } action(); } } diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 4433a8e307..5f5d0d607f 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -3,8 +3,10 @@ using System.Data; using System.Data.SqlClient; using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using System.Web; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -16,7 +18,7 @@ namespace Umbraco.Core.Runtime internal class SqlMainDomLock : IMainDomLock { private string _lockId; - private const string MainDomKey = "Umbraco.Core.Runtime.SqlMainDom"; + private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; private IUmbracoDatabase _db; @@ -32,6 +34,7 @@ namespace Umbraco.Core.Runtime // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; + _dbFactory = new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, _logger, @@ -40,6 +43,12 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + if (!_dbFactory.Configured) + { + // if we aren't configured, then we're in an install state, in which case we have no choice but to assume we can acquire + return true; + } + if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); @@ -119,6 +128,16 @@ namespace Umbraco.Core.Runtime } + /// + /// Returns the keyvalue table key for the current server/app + /// + /// + /// The key is the the normal MainDomId which takes into account the AppDomainAppId and the physical file path of the app and this is + /// combined with the current machine name. The machine name is required because the default semaphore lock is machine wide so it implicitly + /// takes into account machine name whereas this needs to be explicitly per machine. + /// + private string MainDomKey { get; } = MainDomKeyPrefix + "-" + (NetworkHelper.MachineName + MainDom.GetMainDomId()).GenerateHash(); + private void ListeningLoop() { while (true) @@ -126,6 +145,12 @@ namespace Umbraco.Core.Runtime // poll every 1 second Thread.Sleep(1000); + if (!_dbFactory.Configured) + { + // if we aren't configured, we just keep looping since we can't query the db + continue; + } + lock (_locker) { // If cancellation has been requested we will just exit. Depending on timing of the shutdown, @@ -358,41 +383,44 @@ namespace Umbraco.Core.Runtime _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); - var db = GetDatabase(); - try + if (_dbFactory.Configured) { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // When we are disposed, it means we have released the MainDom lock - // and called all MainDom release callbacks, in this case - // if another maindom is actually coming online we need - // to signal to the MainDom coming online that we have shutdown. - // To do that, we update the existing main dom DB record with a suffixed "_updated" string. - // Otherwise, if we are just shutting down, we want to just delete the row. - if (_mainDomChanging) + var db = GetDatabase(); + try { - _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (_mainDomChanging) + { + _logger.Debug("Releasing MainDom, updating row, new application is booting."); + db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } } - else + catch (Exception ex) { - _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; + } + finally + { + db?.CompleteTransaction(); + ResetDatabase(); } - } - catch (Exception ex) - { - ResetDatabase(); - _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; - } - finally - { - db?.CompleteTransaction(); - ResetDatabase(); } } } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6f9ca58821..58279fb4da 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -526,6 +526,6 @@ namespace Umbraco.Core.Services OperationResult Rollback(int id, int versionId, string culture = "*", int userId = Constants.Security.SuperUserId); #endregion - + } } diff --git a/src/Umbraco.Core/Services/IContentServiceBase.cs b/src/Umbraco.Core/Services/IContentServiceBase.cs index 439c55d0d0..c40f49347f 100644 --- a/src/Umbraco.Core/Services/IContentServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentServiceBase.cs @@ -1,9 +1,16 @@ -namespace Umbraco.Core.Services +using Umbraco.Core.Models; + +namespace Umbraco.Core.Services { /// /// Placeholder for sharing logic between the content, media (and member) services /// TODO: Start sharing the logic! /// public interface IContentServiceBase : IService - { } + { + /// + /// Checks/fixes the data integrity of node paths/levels stored in the database + /// + ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options); + } } diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 1558b0170b..068864a558 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -601,23 +601,27 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - return GetPagedDescendantsLocked(contentPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(contentPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); } - return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); } } - private IEnumerable GetPagedDescendantsLocked(string contentPath, long pageIndex, int pageSize, out long totalChildren, + private IQuery GetPagedDescendantQuery(string contentPath) + { + var query = Query(); + if (!contentPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); + return query; + } + + private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, IQuery filter, Ordering ordering) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - var query = Query(); - if (!contentPath.IsNullOrWhiteSpace()) - query.Where(x => x.Path.SqlStartsWith($"{contentPath},", TextColumnType.NVarchar)); - return _documentRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } @@ -1866,7 +1870,7 @@ namespace Umbraco.Core.Services.Implement public OperationResult MoveToRecycleBin(IContent content, int userId) { var evtMsgs = EventMessagesFactory.Get(); - var moves = new List>(); + var moves = new List<(IContent, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -1925,7 +1929,7 @@ namespace Umbraco.Core.Services.Implement return; } - var moves = new List>(); + var moves = new List<(IContent, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -1978,7 +1982,7 @@ namespace Umbraco.Core.Services.Implement // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything private void PerformMoveLocked(IContent content, int parentId, IContent parent, int userId, - ICollection> moves, + ICollection<(IContent, string)> moves, bool? trash) { content.WriterId = userId; @@ -1990,7 +1994,7 @@ namespace Umbraco.Core.Services.Implement var paths = new Dictionary(); - moves.Add(Tuple.Create(content, content.Path)); // capture original path + moves.Add((content, content.Path)); // capture original path //need to store the original path to lookup descendants based on it below var originalPath = content.Path; @@ -2007,20 +2011,24 @@ namespace Umbraco.Core.Services.Implement paths[content.Id] = (parent == null ? (parentId == Constants.System.RecycleBinContent ? "-1,-20" : Constants.System.RootString) : parent.Path) + "," + content.Id; const int pageSize = 500; - var total = long.MaxValue; - while (total > 0) + var query = GetPagedDescendantQuery(originalPath); + long total; + do { - var descendants = GetPagedDescendantsLocked(originalPath, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced + var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + moves.Add((descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveContentLocked(descendant, userId, trash); } - } + + } while (total > pageSize); } @@ -2375,6 +2383,25 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Succeed(evtMsgs); } + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Constants.Locks.ContentTree); + + var report = _documentRepository.CheckDataIntegrity(options); + + if (report.FixedIssues.Count > 0) + { + //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref + var root = new Content("root", -1, new ContentType(-1)) {Id = -1, Key = Guid.Empty}; + scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); + } + + return report; + } + } + #endregion #region Internal Methods @@ -2812,7 +2839,7 @@ namespace Umbraco.Core.Services.Implement // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. var changes = new List>(); - var moves = new List>(); + var moves = new List<(IContent, string)>(); var contentTypeIdsA = contentTypeIds.ToArray(); // using an immediate uow here because we keep making changes with diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index 528d0a0bf9..ecd4cccc8d 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -530,23 +530,27 @@ namespace Umbraco.Core.Services.Implement totalChildren = 0; return Enumerable.Empty(); } - return GetPagedDescendantsLocked(mediaPath[0].Path, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(mediaPath[0].Path), pageIndex, pageSize, out totalChildren, filter, ordering); } - return GetPagedDescendantsLocked(null, pageIndex, pageSize, out totalChildren, filter, ordering); + return GetPagedLocked(GetPagedDescendantQuery(null), pageIndex, pageSize, out totalChildren, filter, ordering); } } - private IEnumerable GetPagedDescendantsLocked(string mediaPath, long pageIndex, int pageSize, out long totalChildren, + private IQuery GetPagedDescendantQuery(string mediaPath) + { + var query = Query(); + if (!mediaPath.IsNullOrWhiteSpace()) + query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); + return query; + } + + private IEnumerable GetPagedLocked(IQuery query, long pageIndex, int pageSize, out long totalChildren, IQuery filter, Ordering ordering) { if (pageIndex < 0) throw new ArgumentOutOfRangeException(nameof(pageIndex)); if (pageSize <= 0) throw new ArgumentOutOfRangeException(nameof(pageSize)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); - var query = Query(); - if (!mediaPath.IsNullOrWhiteSpace()) - query.Where(x => x.Path.SqlStartsWith(mediaPath + ",", TextColumnType.NVarchar)); - return _mediaRepository.GetPage(query, pageIndex, pageSize, out totalChildren, filter, ordering); } @@ -888,7 +892,7 @@ namespace Umbraco.Core.Services.Implement public Attempt MoveToRecycleBin(IMedia media, int userId = Constants.Security.SuperUserId) { var evtMsgs = EventMessagesFactory.Get(); - var moves = new List>(); + var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -940,7 +944,7 @@ namespace Umbraco.Core.Services.Implement return OperationResult.Attempt.Succeed(evtMsgs); } - var moves = new List>(); + var moves = new List<(IMedia, string)>(); using (var scope = ScopeProvider.CreateScope()) { @@ -979,7 +983,7 @@ namespace Umbraco.Core.Services.Implement // MUST be called from within WriteLock // trash indicates whether we are trashing, un-trashing, or not changing anything - private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection> moves, bool? trash) + private void PerformMoveLocked(IMedia media, int parentId, IMedia parent, int userId, ICollection<(IMedia, string)> moves, bool? trash) { media.ParentId = parentId; @@ -989,7 +993,7 @@ namespace Umbraco.Core.Services.Implement var paths = new Dictionary(); - moves.Add(Tuple.Create(media, media.Path)); // capture original path + moves.Add((media, media.Path)); // capture original path //need to store the original path to lookup descendants based on it below var originalPath = media.Path; @@ -1006,21 +1010,25 @@ namespace Umbraco.Core.Services.Implement paths[media.Id] = (parent == null ? (parentId == Constants.System.RecycleBinMedia ? "-1,-21" : Constants.System.RootString) : parent.Path) + "," + media.Id; const int pageSize = 500; - var page = 0; - var total = long.MaxValue; - while (page * pageSize < total) + var query = GetPagedDescendantQuery(originalPath); + long total; + do { - var descendants = GetPagedDescendantsLocked(originalPath, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + // We always page a page 0 because for each page, we are moving the result so the resulting total will be reduced + var descendants = GetPagedLocked(query, 0, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); + foreach (var descendant in descendants) { - moves.Add(Tuple.Create(descendant, descendant.Path)); // capture original path + moves.Add((descendant, descendant.Path)); // capture original path // update path and level since we do not update parentId descendant.Path = paths[descendant.Id] = paths[descendant.ParentId] + "," + descendant.Id; descendant.Level += levelDelta; PerformMoveMediaLocked(descendant, userId, trash); } - } + + } while (total > pageSize); + } private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash) @@ -1139,6 +1147,26 @@ namespace Umbraco.Core.Services.Implement } return true; + + } + + public ContentDataIntegrityReport CheckDataIntegrity(ContentDataIntegrityReportOptions options) + { + using (var scope = ScopeProvider.CreateScope(autoComplete: true)) + { + scope.WriteLock(Constants.Locks.MediaTree); + + var report = _mediaRepository.CheckDataIntegrity(options); + + if (report.FixedIssues.Count > 0) + { + //The event args needs a content item so we'll make a fake one with enough properties to not cause a null ref + var root = new Models.Media("root", -1, new MediaType(-1)) { Id = -1, Key = Guid.Empty }; + scope.Events.Dispatch(TreeChanged, this, new TreeChange.EventArgs(new TreeChange(root, TreeChangeTypes.RefreshAll))); + } + + return report; + } } #endregion @@ -1277,7 +1305,7 @@ namespace Umbraco.Core.Services.Implement // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. var changes = new List>(); - var moves = new List>(); + var moves = new List<(IMedia, string)>(); var mediaTypeIdsA = mediaTypeIds.ToArray(); using (var scope = ScopeProvider.CreateScope()) @@ -1358,5 +1386,7 @@ namespace Umbraco.Core.Services.Implement } #endregion + + } } diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 82cab07b25..c365f1ccc2 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Web.Security; using Umbraco.Core.Models.Membership; @@ -116,5 +117,17 @@ namespace Umbraco.Core.Services var permissionCollection = userService.GetPermissions(user, nodeId); return permissionCollection.SelectMany(c => c.AssignedPermissions).Distinct().ToArray(); } + + internal static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) + { + var fullUsers = userService.GetUsersById(ids); + + return fullUsers.Select(user => + { + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c0d14a580e..809074073b 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -128,13 +128,18 @@ --> + + + + + diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 9cbc311639..b8477a9047 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -1,9 +1,13 @@ using Examine; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -16,20 +20,46 @@ namespace Umbraco.Examine { private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IUserService _userService; + private readonly IScopeProvider _scopeProvider; + + [Obsolete("Use the other ctor instead")] + public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + bool publishedValuesOnly) + : this(propertyEditors, urlSegmentProviders, userService, Current.ScopeProvider, publishedValuesOnly) + { + } public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, IUserService userService, + IScopeProvider scopeProvider, bool publishedValuesOnly) : base(propertyEditors, publishedValuesOnly) { _urlSegmentProviders = urlSegmentProviders; _userService = userService; + _scopeProvider = scopeProvider; } /// public override IEnumerable GetValueSets(params IContent[] content) { + Dictionary creatorIds; + Dictionary writerIds; + + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (var scope = _scopeProvider.CreateScope()) + { + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); + } + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` @@ -58,8 +88,8 @@ namespace Umbraco.Examine {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (c.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, - {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, + {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name : "??").Yield() }, + {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name : "??").Yield() }, {"writerID", new object[] {c.WriterId}}, {"templateID", new object[] {c.TemplateId ?? 0}}, {UmbracoContentIndex.VariesByCultureFieldName, new object[] {"n"}}, diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index 8e8a19c729..0e125759c6 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -21,7 +21,7 @@ using File = System.IO.File; namespace Umbraco.ModelsBuilder.Embedded { - internal class PureLiveModelFactory : ILivePublishedModelFactory, IRegisteredObject + internal class PureLiveModelFactory : ILivePublishedModelFactory2, IRegisteredObject { private Assembly _modelsAssembly; private Infos _infos = new Infos { ModelInfos = null, ModelTypeMap = new Dictionary() }; @@ -134,6 +134,16 @@ namespace Umbraco.ModelsBuilder.Embedded return ctor(); } + /// + public bool Enabled => _config.Enable; + + /// + public void Reset() + { + if (_config.Enable) + ResetModels(); + } + #endregion #region Compilation diff --git a/src/Umbraco.Tests.AcceptanceTest/README.md b/src/Umbraco.Tests.AcceptanceTest/README.md new file mode 100644 index 0000000000..e8699b0733 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/README.md @@ -0,0 +1,35 @@ +# Umbraco Acceptance Tests + +### Prerequisite +- NodeJS 12+ +- A running installed Umbraco on url: [https://localhost:44331](https://localhost:44331) (Default development port) + - Install using a `SqlServer`/`LocalDb` as the tests execute too fast for `SqlCE` to handle. +- User information in `cypress.env.json` (See [Getting started](#getting-started)) + +### Getting started +The tests is located in the project/folder named `Umbraco.Tests.AcceptanceTests`. Ensur to run `npm install` in that folder, or let your IDE do that. + +Next, it is important you create a new file in the root of the project called `cypress.env.json`. +This file is already added to `.gitignore` and can contain values that is different for each developer machine. + +The file need the following content: +``` +{ + "username": "", + "password": "" +} +``` +Replace the `` and `` placeholders with correct info. + + + +### Executing tests + +There exists two npm scripts, that can be used to execute the test. + +1. `npm run test` + - Executes the tests headless. +1. `npm run ui` + - Executes the tests in a browser handled by a cypress application. + + In case of errors it is recommended to use the UI to debug. diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress.json b/src/Umbraco.Tests.AcceptanceTest/cypress.json new file mode 100644 index 0000000000..33978211ed --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress.json @@ -0,0 +1,11 @@ +{ + "baseUrl": "https://localhost:44331", + "viewportHeight": 1024, + "viewportWidth": 1200, + "env": { + "username": "", + "password": "" + }, + "supportFile": "cypress/support/index.ts", + "videoUploadOnPasses" : false +} diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Login/login.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Login/login.ts new file mode 100644 index 0000000000..4c7720d71f --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Login/login.ts @@ -0,0 +1,62 @@ +/// +context('Login', () => { + + beforeEach(() => { + cy.visit('/umbraco'); + }); + + it('Login with correct username and password', () => { + const username = Cypress.env('username'); + const password = Cypress.env('password'); + //Precondition + cy.get('.text-error').should('not.exist'); + + //Action + cy.get('#umb-username').type(username); + cy.get('#umb-passwordTwo').type(password); + cy.get('[label-key="general_login"]').click(); + + //Assert + cy.url().should('include', '/umbraco#/content') + cy.get('#umb-username').should('not.exist'); + cy.get('#umb-passwordTwo').should('not.exist'); + }); + + + it('Login with correct username but wrong password', () => { + const username = Cypress.env('username'); + const password = 'wrong'; + + //Precondition + cy.get('.text-error').should('not.exist'); + + //Action + cy.get('#umb-username').type(username); + cy.get('#umb-passwordTwo').type(password); + cy.get('[label-key="general_login"]').click(); + + //Assert + cy.get('.text-error').should('exist'); + cy.get('#umb-username').should('exist'); + cy.get('#umb-passwordTwo').should('exist'); + }); + + it('Login with wrong username and wrong password', () => { + const username = 'wrong-username'; + const password = 'wrong'; + + //Precondition + cy.get('.text-error').should('not.exist'); + + //Action + cy.get('#umb-username').type(username); + cy.get('#umb-passwordTwo').type(password); + cy.get('[label-key="general_login"]').click(); + + //Assert + cy.get('.text-error').should('exist'); + cy.get('#umb-username').should('exist'); + cy.get('#umb-passwordTwo').should('exist'); + }); + + }); \ No newline at end of file diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/dataTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/dataTypes.ts new file mode 100644 index 0000000000..5803810f54 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/dataTypes.ts @@ -0,0 +1,66 @@ +/// +import {LabelDataTypeBuilder} from 'umbraco-cypress-testhelpers'; +context('Data Types', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create data type', () => { + const name = "Test data type"; + + cy.umbracoEnsureDataTypeNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Data Types"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.umbracoContextMenuAction("action-data-type").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + + cy.get('select[name="selectedEditor"]').select('Label'); + + cy.get('.umb-property-editor select').select('Time'); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureDataTypeNameNotExists(name); + }); + + it('Delete data type', () => { + const name = "Test data type"; + cy.umbracoEnsureDataTypeNameNotExists(name); + + const dataType = new LabelDataTypeBuilder() + .withSaveNewAction() + .withName(name) + .build(); + + cy.saveDataType(dataType); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Data Types", name]).rightclick(); + + cy.umbracoContextMenuAction("action-delete").click(); + + cy.umbracoButtonByLabelKey("general_delete").click(); + + cy.contains(name).should('not.exist'); + + cy.umbracoEnsureDataTypeNameNotExists(name); + + + }); +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts new file mode 100644 index 0000000000..5a5ba0b3e0 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts @@ -0,0 +1,79 @@ +/// +import { DocumentTypeBuilder } from 'umbraco-cypress-testhelpers'; +context('Document Types', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create document type', () => { + const name = "Test document type"; + + cy.umbracoEnsureDocumentTypeNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Document Types"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.umbracoContextMenuAction("action-documentType").click(); + //Type name + cy.umbracoEditorHeaderName(name); + + + cy.get('[data-element="group-add"]').click(); + + + cy.get('.umb-group-builder__group-title-input').type('Group name'); + cy.get('[data-element="property-add"]').click(); + cy.get('.editor-label').type('property name'); + cy.get('[data-element="editor-add"]').click(); + + //Search for textstring + cy.get('.umb-search-field').type('Textstring'); + + // Choose first item + cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click(); + + // Save property + cy.get('.btn-success').last().click(); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureDocumentTypeNameNotExists(name); + }); + + it('Delete document type', () => { + const name = "Test document type"; + cy.umbracoEnsureDocumentTypeNameNotExists(name); + + const dataType = new DocumentTypeBuilder() + .withName(name) + .build(); + + cy.saveDocumentType(dataType); + + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Document Types", name]).rightclick(); + + cy.umbracoContextMenuAction("action-delete").click(); + + cy.get('label.checkbox').click(); + cy.umbracoButtonByLabelKey("general_ok").click(); + + cy.contains(name).should('not.exist'); + + cy.umbracoEnsureDocumentTypeNameNotExists(name); + + + }); +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts new file mode 100644 index 0000000000..49bcf94943 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/languages.ts @@ -0,0 +1,32 @@ +/// +context('Languages', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Add language', () => { + const name = "Kyrgyz (Kyrgyzstan)"; // Must be an option in the select box + + cy.umbracoEnsureLanguageNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Languages"]).click(); + + cy.umbracoButtonByLabelKey("languages_addLanguage").click(); + + cy.get('select[name="newLang"]').select(name); + + // //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureLanguageNameNotExists(name); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/macros.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/macros.ts new file mode 100644 index 0000000000..40b5d5483d --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/macros.ts @@ -0,0 +1,33 @@ +/// +context('Macros', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create macro', () => { + const name = "Test macro"; + + cy.umbracoEnsureMacroNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Macros"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + + cy.get('form[name="createMacroForm"]').within(($form) => { + cy.get('input[name="itemKey"]').type(name); + cy.get(".btn-primary").click(); + }); + + cy.location().should((loc) => { + expect(loc.hash).to.include('#/settings/macros/edit/') + }); + + //Clean up + cy.umbracoEnsureMacroNameNotExists(name); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/mediaTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/mediaTypes.ts new file mode 100644 index 0000000000..a963da754f --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/mediaTypes.ts @@ -0,0 +1,52 @@ +/// +context('Media Types', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create media type', () => { + const name = "Test media type"; + + cy.umbracoEnsureMediaTypeNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Media Types"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click(); + + + //Type name + cy.umbracoEditorHeaderName(name); + + + cy.get('[data-element="group-add"]').click(); + + cy.get('.umb-group-builder__group-title-input').type('Group name'); + cy.get('[data-element="property-add"]').click(); + cy.get('.editor-label').type('property name'); + cy.get('[data-element="editor-add"]').click(); + + //Search for textstring + cy.get('.umb-search-field').type('Textstring'); + + // Choose first item + cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click(); + + // Save property + cy.get('.btn-success').last().click(); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureMediaTypeNameNotExists(name); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/memberTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/memberTypes.ts new file mode 100644 index 0000000000..53823f2a69 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/memberTypes.ts @@ -0,0 +1,50 @@ +/// +context('Member Types', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create member type', () => { + const name = "Test member type"; + + cy.umbracoEnsureMemberTypeNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Member Types"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + + cy.get('[data-element="group-add"]').click(); + + cy.get('.umb-group-builder__group-title-input').type('Group name'); + cy.get('[data-element="property-add"]').click(); + cy.get('.editor-label').type('property name'); + cy.get('[data-element="editor-add"]').click(); + + //Search for textstring + cy.get('.umb-search-field').type('Textstring'); + + // Choose first item + cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click(); + + // Save property + cy.get('.btn-success').last().click(); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureMemberTypeNameNotExists(name); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViewMacroFiles.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViewMacroFiles.ts new file mode 100644 index 0000000000..f4c976de08 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViewMacroFiles.ts @@ -0,0 +1,37 @@ +/// +context('Partial View Macro Files', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create new partial view macro', () => { + const name = "TestPartialViewMacro"; + const fileName = name + ".cshtml"; + + cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName); + cy.umbracoEnsureMacroNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Partial View Macro Files"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-label").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsurePartialViewMacroFileNameNotExists(fileName); + cy.umbracoEnsureMacroNameNotExists(name); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts new file mode 100644 index 0000000000..b644c6642b --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/partialsViews.ts @@ -0,0 +1,35 @@ +/// +context('Partial Views', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create new empty partial view', () => { + const name = "TestPartialView"; + const fileName = name + ".cshtml"; + + cy.umbracoEnsurePartialViewNameNotExists(fileName); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Partial Views"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsurePartialViewNameNotExists(fileName); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/relationTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/relationTypes.ts new file mode 100644 index 0000000000..4773f37c37 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/relationTypes.ts @@ -0,0 +1,40 @@ +/// +context('Relation Types', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create relation type', () => { + const name = "Test relation type"; + + cy.umbracoEnsureRelationTypeNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Relation Types"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + + cy.get('form[name="createRelationTypeForm"]').within(($form) => { + cy.get('input[name="relationTypeName"]').type(name); + + cy.get('[name="relationType-direction"] input').first().click({force:true}); + + cy.get('select[name="relationType-parent"]').select('Document'); + + cy.get('select[name="relationType-child"]').select('Media'); + + cy.get(".btn-primary").click(); + }); + + cy.location().should((loc) => { + expect(loc.hash).to.include('#/settings/relationTypes/edit/') + }) + + //Clean up + cy.umbracoEnsureRelationTypeNameNotExists(name); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/scripts.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/scripts.ts new file mode 100644 index 0000000000..8cffd3e59b --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/scripts.ts @@ -0,0 +1,35 @@ +/// +context('Scripts', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create new JavaScript file', () => { + const name = "TestScript"; + const fileName = name + ".js"; + + cy.umbracoEnsureScriptNameNotExists(fileName); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Stylesheets"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureScriptNameNotExists(fileName); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/stylesheets.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/stylesheets.ts new file mode 100644 index 0000000000..2a1286c499 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/stylesheets.ts @@ -0,0 +1,35 @@ +/// +context('Stylesheets', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create new style sheet file', () => { + const name = "TestStylesheet"; + const fileName = name + ".css"; + + cy.umbracoEnsureStylesheetNameNotExists(fileName); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Stylesheets"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + cy.get('.menu-label').first().click(); // TODO: Fucked we cant use something like cy.umbracoContextMenuAction("action-mediaType").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + //Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureStylesheetNameNotExists(fileName); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts new file mode 100644 index 0000000000..6871db7ffe --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/templates.ts @@ -0,0 +1,57 @@ +/// +import {DocumentTypeBuilder, TemplateBuilder} from "umbraco-cypress-testhelpers"; + +context('Templates', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create template', () => { + const name = "Test template"; + + cy.umbracoEnsureTemplateNameNotExists(name); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Templates"]).rightclick(); + + cy.umbracoContextMenuAction("action-create").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + //Save + cy.get("form[name='contentForm']").submit(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureTemplateNameNotExists(name); + }); + + it('Delete template', () => { + const name = "Test template"; + cy.umbracoEnsureTemplateNameNotExists(name); + + const template = new TemplateBuilder() + .withName(name) + .build(); + + cy.saveTemplate(template); + + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + + cy.umbracoTreeItem("settings", ["Templates", name]).rightclick(); + cy.umbracoContextMenuAction("action-delete").click(); + + cy.umbracoButtonByLabelKey("general_ok").click(); + + cy.contains(name).should('not.exist'); + + cy.umbracoEnsureTemplateNameNotExists(name); + }); +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.ts new file mode 100644 index 0000000000..905d0fc25c --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/userGroups.ts @@ -0,0 +1,36 @@ + +context('User Groups', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create user group', () => { + const name = "Test Group"; + + cy.umbracoEnsureUserGroupNameNotExists(name); + + cy.umbracoSection('users'); + cy.get('[data-element="sub-view-userGroups"]').click(); + + cy.umbracoButtonByLabelKey("actions_createGroup").click(); + + //Type name + cy.umbracoEditorHeaderName(name); + + // Assign sections + cy.get('.umb-box:nth-child(1) .umb-property:nth-child(1) localize').click(); + cy.get('.umb-tree-item span').click({multiple:true}); + cy.get('.btn-success').last().click(); + + // Save + cy.get('.btn-success').click(); + + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + + //Clean up + cy.umbracoEnsureUserGroupNameNotExists(name); + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/users.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/users.ts new file mode 100644 index 0000000000..e122b21564 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Users/users.ts @@ -0,0 +1,35 @@ +/// +context('Users', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password')); + }); + + it('Create user', () => { + const name = "Alice Bobson"; + const email = "alice-bobson@acceptancetest.umbraco"; + + cy.umbracoEnsureUserEmailNotExists(email); + cy.umbracoSection('users'); + cy.umbracoButtonByLabelKey("user_createUser").click(); + + + cy.get('input[name="name"]').type(name); + cy.get('input[name="email"]').type(email); + + cy.get('.umb-node-preview-add').click(); + cy.get('.umb-user-group-picker-list-item:nth-child(1) > .umb-user-group-picker__action').click(); + cy.get('.umb-user-group-picker-list-item:nth-child(2) > .umb-user-group-picker__action').click(); + cy.get('.btn-success').click(); + + cy.get('.umb-button > .btn > .umb-button__content').click(); + + + cy.umbracoButtonByLabelKey("user_goToProfile").should('be.visible'); + + //Clean up + cy.umbracoEnsureUserEmailNotExists(email); + + }); + +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js b/src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js new file mode 100644 index 0000000000..aa9918d215 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/plugins/index.js @@ -0,0 +1,21 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js b/src/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js new file mode 100644 index 0000000000..5b0be47114 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/support/commands.js @@ -0,0 +1,30 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +import {Command} from 'umbraco-cypress-testhelpers'; +import {Chainable} from './chainable'; +new Chainable(); +new Command().registerCypressCommands(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/support/index.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/support/index.ts new file mode 100644 index 0000000000..d68db96df2 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/support/index.ts @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/tsconfig.json b/src/Umbraco.Tests.AcceptanceTest/cypress/tsconfig.json new file mode 100644 index 0000000000..70cf2f6751 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": [ + "../node_modules/cypress", + "*/*.ts" + ] +} diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/typings.d.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/typings.d.ts new file mode 100644 index 0000000000..3b7cf0b3e9 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/typings.d.ts @@ -0,0 +1,5 @@ +// type definitions for Cypress object "cy" +/// + +// type definitions for custom commands like "createDefaultTodos" +// diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json new file mode 100644 index 0000000000..ad125d090a --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -0,0 +1,15 @@ +{ + "scripts": { + "test": "npx cypress run", + "ui": "npx cypress open" + }, + "devDependencies": { + "cross-env": "^7.0.2", + "ncp": "^2.0.0", + "cypress": "^4.6.0", + "umbraco-cypress-testhelpers": "1.0.0-beta-39" + }, + "dependencies": { + "typescript": "^3.9.2" + } +} diff --git a/src/Umbraco.Tests.AcceptanceTest/tsconfig.json b/src/Umbraco.Tests.AcceptanceTest/tsconfig.json new file mode 100644 index 0000000000..6cb05bfcc7 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./lib", + "sourceMap": false, + "declaration": true, + "module": "CommonJS", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "esModuleInterop": true, + "importHelpers": true, + "target": "es5", + + "types": [ + "cypress" + ], + "lib": [ + "es5", + "dom" + ], + "plugins": [ + { + "name": "typescript-tslint-plugin", + "alwaysShowRuleFailuresAsWarnings": false, + "ignoreDefinitionFiles": true, + "configFile": "tslint.json", + "suppressWhileTypeErrorsPresent": false + } + ] + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/src/Umbraco.Tests.AcceptanceTest/tslint.json b/src/Umbraco.Tests.AcceptanceTest/tslint.json new file mode 100644 index 0000000000..a8571f3cda --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": ["tslint:recommended", "tslint-config-prettier"] +} diff --git a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs index f5c1ff9bc7..d7a19cb553 100644 --- a/src/Umbraco.Tests/Composing/ContainerConformingTests.cs +++ b/src/Umbraco.Tests/Composing/ContainerConformingTests.cs @@ -332,6 +332,24 @@ namespace Umbraco.Tests.Composing Assert.AreSame(s1, s2); } + [Test] + public void CanRegisterMultipleSameTypeParametersWithCreateInstance() + { + var register = GetRegister(); + + register.Register(c => + { + const string param1 = "param1"; + const string param2 = "param2"; + + return c.CreateInstance(param1, param2); + }); + + var factory = register.CreateFactory(); + var instance = factory.GetInstance(); + Assert.AreNotEqual(instance.Thing, instance.AnotherThing); + } + public interface IThing { } public abstract class ThingBase : IThing { } @@ -352,5 +370,17 @@ namespace Umbraco.Tests.Composing public IEnumerable Things { get; } } + + public class Thing4 : ThingBase + { + public readonly string Thing; + public readonly string AnotherThing; + + public Thing4(string thing, string anotherThing) + { + Thing = thing; + AnotherThing = anotherThing; + } + } } } diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index a96385a923..016085c352 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -924,6 +924,24 @@ namespace Umbraco.Tests.Services Assert.AreEqual(user.Id, profile.Id); } + [Test] + public void Get_By_Profile_Id_Must_return_null_if_user_not_exists() + { + var profile = ServiceContext.UserService.GetProfileById(42); + + // Assert + Assert.IsNull(profile); + } + + [Test] + public void GetProfilesById_Must_empty_if_users_not_exists() + { + var profiles = ServiceContext.UserService.GetProfilesById(42); + + // Assert + CollectionAssert.IsEmpty(profiles); + } + [Test] public void Get_User_By_Username() { diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 1653de827d..e9f18d8947 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -30,16 +30,22 @@ namespace Umbraco.Tests.UmbracoExamine /// internal static class IndexInitializer { - public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = new ContentValueSetBuilder(propertyEditors, new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), GetMockUserService(), publishedValuesOnly); + var contentValueSetBuilder = new ContentValueSetBuilder( + propertyEditors, + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + GetMockUserService(), + scopeProvider, + publishedValuesOnly); + return contentValueSetBuilder; } - public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, ISqlContext sqlContext, bool publishedValuesOnly) + public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, publishedValuesOnly); - var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, sqlContext, contentValueSetBuilder); + var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, scopeProvider, publishedValuesOnly); + var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, scopeProvider.SqlContext, contentValueSetBuilder); return contentIndexDataSource; } diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index 9e59422310..acb26fb8f6 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -29,7 +29,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Property_Data_With_Value_Indexer() { - var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), false); + var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, @@ -121,7 +121,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Rebuild_Index() { - var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); var mediaRebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); using (var luceneDir = new RandomIdRamDirectory()) @@ -149,7 +149,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Protected_Content_Not_Indexed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) @@ -274,7 +274,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Reindex_Content() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: new ContentValueSetValidator(false))) @@ -315,7 +315,7 @@ namespace Umbraco.Tests.UmbracoExamine public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index a45a33ec00..96e8892cd1 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -55,7 +55,7 @@ namespace Umbraco.Tests.UmbracoExamine allRecs); var propertyEditors = Factory.GetInstance(); - var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider.SqlContext, true); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider, true); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) diff --git a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs index d77867152a..0279f82f42 100644 --- a/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs +++ b/src/Umbraco.Tests/Web/Controllers/ContentControllerTests.cs @@ -489,7 +489,7 @@ namespace Umbraco.Tests.Web.Controllers var display = JsonConvert.DeserializeObject(response.Item2); Assert.AreEqual(2, display.Errors.Count()); Assert.IsTrue(display.Errors.ContainsKey("Variants[0].Name")); - Assert.IsTrue(display.Errors.ContainsKey("_content_variant_en-US_")); + Assert.IsTrue(display.Errors.ContainsKey("_content_variant_en-US_null_")); } // TODO: There are SOOOOO many more tests we should write - a lot of them to do with validation diff --git a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs b/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs index 3ce43b5fc2..7b25e60b5a 100644 --- a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs +++ b/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs @@ -22,19 +22,19 @@ namespace Umbraco.Tests.Web ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property ms.AddPropertyError(new ValidationResult("title missing"), "title", "en-US"); //variant property - var result = ms.GetCulturesWithErrors(localizationService.Object, "en-US"); + var result = ms.GetVariantsWithErrors("en-US"); //even though there are 2 errors, they are both for en-US since that is the default language and one of the errors is for an invariant property Assert.AreEqual(1, result.Count); - Assert.AreEqual("en-US", result[0]); + Assert.AreEqual("en-US", result[0].culture); ms = new ModelStateDictionary(); - ms.AddCultureValidationError("en-US", "generic culture error"); + ms.AddVariantValidationError("en-US", null, "generic culture error"); - result = ms.GetCulturesWithErrors(localizationService.Object, "en-US"); + result = ms.GetVariantsWithErrors("en-US"); Assert.AreEqual(1, result.Count); - Assert.AreEqual("en-US", result[0]); + Assert.AreEqual("en-US", result[0].culture); } [Test] @@ -47,11 +47,11 @@ namespace Umbraco.Tests.Web ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property ms.AddPropertyError(new ValidationResult("title missing"), "title", "en-US"); //variant property - var result = ms.GetCulturesWithPropertyErrors(localizationService.Object, "en-US"); + var result = ms.GetVariantsWithPropertyErrors("en-US"); //even though there are 2 errors, they are both for en-US since that is the default language and one of the errors is for an invariant property Assert.AreEqual(1, result.Count); - Assert.AreEqual("en-US", result[0]); + Assert.AreEqual("en-US", result[0].culture); } [Test] @@ -63,7 +63,7 @@ namespace Umbraco.Tests.Web ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null); //invariant property - Assert.AreEqual("_Properties.headerImage.invariant", ms.Keys.First()); + Assert.AreEqual("_Properties.headerImage.invariant.null", ms.Keys.First()); } [Test] @@ -73,9 +73,57 @@ namespace Umbraco.Tests.Web var localizationService = new Mock(); localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); - ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US"); //invariant property + ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US"); //variant property - Assert.AreEqual("_Properties.headerImage.en-US", ms.Keys.First()); + Assert.AreEqual("_Properties.headerImage.en-US.null", ms.Keys.First()); + } + + [Test] + public void Add_Invariant_Segment_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", null, "mySegment"); //invariant/segment property + + Assert.AreEqual("_Properties.headerImage.invariant.mySegment", ms.Keys.First()); + } + + [Test] + public void Add_Variant_Segment_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image"), "headerImage", "en-US", "mySegment"); //variant/segment property + + Assert.AreEqual("_Properties.headerImage.en-US.mySegment", ms.Keys.First()); + } + + [Test] + public void Add_Invariant_Segment_Field_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image", new[] { "myField" }), "headerImage", null, "mySegment"); //invariant/segment property + + Assert.AreEqual("_Properties.headerImage.invariant.mySegment.myField", ms.Keys.First()); + } + + [Test] + public void Add_Variant_Segment_Field_Property_Error() + { + var ms = new ModelStateDictionary(); + var localizationService = new Mock(); + localizationService.Setup(x => x.GetDefaultLanguageIsoCode()).Returns("en-US"); + + ms.AddPropertyError(new ValidationResult("no header image", new[] { "myField" }), "headerImage", "en-US", "mySegment"); //variant/segment property + + Assert.AreEqual("_Properties.headerImage.en-US.mySegment.myField", ms.Keys.First()); } } } diff --git a/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js new file mode 100644 index 0000000000..90eae85800 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/tinymce/langs/en_US.js @@ -0,0 +1,261 @@ +tinymce.addI18n('en_US',{ +"Redo": "Redo", +"Undo": "Undo", +"Cut": "Cut", +"Copy": "Copy", +"Paste": "Paste", +"Select all": "Select all", +"New document": "New document", +"Ok": "Ok", +"Cancel": "Cancel", +"Visual aids": "Visual aids", +"Bold": "Bold", +"Italic": "Italic", +"Underline": "Underline", +"Strikethrough": "Strikethrough", +"Superscript": "Superscript", +"Subscript": "Subscript", +"Clear formatting": "Clear formatting", +"Align left": "Align left", +"Align center": "Align center", +"Align right": "Align right", +"Justify": "Justify", +"Bullet list": "Bullet list", +"Numbered list": "Numbered list", +"Decrease indent": "Decrease indent", +"Increase indent": "Increase indent", +"Close": "Close", +"Formats": "Formats", +"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.", +"Headers": "Headers", +"Header 1": "Header 1", +"Header 2": "Header 2", +"Header 3": "Header 3", +"Header 4": "Header 4", +"Header 5": "Header 5", +"Header 6": "Header 6", +"Headings": "Headings", +"Heading 1": "Heading 1", +"Heading 2": "Heading 2", +"Heading 3": "Heading 3", +"Heading 4": "Heading 4", +"Heading 5": "Heading 5", +"Heading 6": "Heading 6", +"Preformatted": "Preformatted", +"Div": "Div", +"Pre": "Pre", +"Code": "Code", +"Paragraph": "Paragraph", +"Blockquote": "Blockquote", +"Inline": "Inline", +"Blocks": "Blocks", +"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.", +"Font Family": "Font Family", +"Font Sizes": "Font Sizes", +"Class": "Class", +"Browse for an image": "Browse for an image", +"OR": "OR", +"Drop an image here": "Drop an image here", +"Upload": "Upload", +"Block": "Blocks", +"Align": "Align", +"Default": "Default", +"Circle": "Circle", +"Disc": "Disc", +"Square": "Square", +"Lower Alpha": "Lower Alpha", +"Lower Greek": "Lower Greek", +"Lower Roman": "Lower Roman", +"Upper Alpha": "Upper Alpha", +"Upper Roman": "Upper Roman", +"Anchor": "Anchor", +"Name": "Name", +"Id": "ID", +"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "ID should start with a letter, followed only by letters, numbers, dashes, dots, colons, or underscores.", +"You have unsaved changes are you sure you want to navigate away?": "You have unsaved changes are you sure you want to navigate away?", +"Restore last draft": "Restore last draft", +"Special character": "Special character", +"Source code": "Source code", +"Insert\/Edit code sample": "Insert\/Edit code sample", +"Language": "Language", +"Code sample": "Code sample", +"Color": "color", +"R": "R", +"G": "G", +"B": "B", +"Left to right": "Left to right", +"Right to left": "Right to left", +"Emoticons": "Emoticons", +"Document properties": "Document properties", +"Title": "Title", +"Keywords": "Keywords", +"Description": "Description", +"Robots": "Robots", +"Author": "Author", +"Encoding": "Encoding", +"Fullscreen": "Fullscreen", +"Action": "Action", +"Shortcut": "Shortcut", +"Help": "Help", +"Address": "Address", +"Focus to menubar": "Focus to menubar", +"Focus to toolbar": "Focus to toolbar", +"Focus to element path": "Focus to element path", +"Focus to contextual toolbar": "Focus to contextual toolbar", +"Insert link (if link plugin activated)": "Insert link (if link plugin activated)", +"Save (if save plugin activated)": "Save (if save plugin activated)", +"Find (if searchreplace plugin activated)": "Find (if searchreplace plugin activated)", +"Plugins installed ({0}):": "Plugins installed ({0}):", +"Premium plugins:": "Premium plugins:", +"Learn more...": "Learn more...", +"You are using {0}": "You are using {0}", +"Plugins": "Plugins", +"Handy Shortcuts": "Handy Shortcuts", +"Horizontal line": "Horizontal line", +"Insert\/edit image": "Insert\/edit image", +"Image description": "Image description", +"Source": "Source", +"Dimensions": "Dimensions", +"Constrain proportions": "Constrain proportions", +"General": "General", +"Advanced": "Advanced", +"Style": "Style", +"Vertical space": "Vertical space", +"Horizontal space": "Horizontal space", +"Border": "Border", +"Insert image": "Insert image", +"Image": "Image", +"Image list": "Image list", +"Rotate counterclockwise": "Rotate counterclockwise", +"Rotate clockwise": "Rotate clockwise", +"Flip vertically": "Flip vertically", +"Flip horizontally": "Flip horizontally", +"Edit image": "Edit image", +"Image options": "Image options", +"Zoom in": "Zoom in", +"Zoom out": "Zoom out", +"Crop": "Crop", +"Resize": "Resize", +"Orientation": "Orientation", +"Brightness": "Brightness", +"Sharpen": "Sharpen", +"Contrast": "Contrast", +"Color levels": "color levels", +"Gamma": "Gamma", +"Invert": "Invert", +"Apply": "Apply", +"Back": "Back", +"Insert date\/time": "Insert date\/time", +"Date\/time": "Date\/time", +"Insert link": "Insert link", +"Insert\/edit link": "Insert\/edit link", +"Text to display": "Text to display", +"Url": "Url", +"Target": "Target", +"None": "None", +"New window": "New window", +"Remove link": "Remove link", +"Anchors": "Anchors", +"Link": "Link", +"Paste or type a link": "Paste or type a link", +"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?", +"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?", +"Link list": "Link list", +"Insert video": "Insert video", +"Insert\/edit video": "Insert\/edit video", +"Insert\/edit media": "Insert\/edit media", +"Alternative source": "Alternative source", +"Poster": "Poster", +"Paste your embed code below:": "Paste your embed code below:", +"Embed": "Embed", +"Media": "Media", +"Nonbreaking space": "Nonbreaking space", +"Page break": "Page break", +"Paste as text": "Paste as text", +"Preview": "Preview", +"Print": "Print", +"Save": "Save", +"Find": "Find", +"Replace with": "Replace with", +"Replace": "Replace", +"Replace all": "Replace all", +"Prev": "Prev", +"Next": "Next", +"Find and replace": "Find and replace", +"Could not find the specified string.": "Could not find the specified string.", +"Match case": "Match case", +"Whole words": "Whole words", +"Spellcheck": "Spellcheck", +"Ignore": "Ignore", +"Ignore all": "Ignore all", +"Finish": "Finish", +"Add to Dictionary": "Add to Dictionary", +"Insert table": "Insert table", +"Table properties": "Table properties", +"Delete table": "Delete table", +"Cell": "Cell", +"Row": "Row", +"Column": "Column", +"Cell properties": "Cell properties", +"Merge cells": "Merge cells", +"Split cell": "Split cell", +"Insert row before": "Insert row before", +"Insert row after": "Insert row after", +"Delete row": "Delete row", +"Row properties": "Row properties", +"Cut row": "Cut row", +"Copy row": "Copy row", +"Paste row before": "Paste row before", +"Paste row after": "Paste row after", +"Insert column before": "Insert column before", +"Insert column after": "Insert column after", +"Delete column": "Delete column", +"Cols": "Cols", +"Rows": "Rows", +"Width": "Width", +"Height": "Height", +"Cell spacing": "Cell spacing", +"Cell padding": "Cell padding", +"Caption": "Caption", +"Left": "Left", +"Center": "Center", +"Right": "Right", +"Cell type": "Cell type", +"Scope": "Scope", +"Alignment": "Alignment", +"H Align": "H Align", +"V Align": "V Align", +"Top": "Top", +"Middle": "Middle", +"Bottom": "Bottom", +"Header cell": "Header cell", +"Row group": "Row group", +"Column group": "Column group", +"Row type": "Row type", +"Header": "Header", +"Body": "Body", +"Footer": "Footer", +"Border color": "Border color", +"Insert template": "Insert template", +"Templates": "Templates", +"Template": "Template", +"Text color": "Text color", +"Background color": "Background color", +"Custom...": "Custom...", +"Custom color": "Custom color", +"No color": "No color", +"Table of Contents": "Table of Contents", +"Show blocks": "Show blocks", +"Show invisible characters": "Show invisible characters", +"Words: {0}": "Words: {0}", +"{0} words": "{0} words", +"File": "File", +"Edit": "Edit", +"Insert": "Insert", +"View": "View", +"Format": "Format", +"Table": "Table", +"Tools": "Tools", +"Powered by {0}": "Powered by {0}", +"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help" +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 14d00f9e41..1b0af95e13 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -42,7 +42,7 @@ "npm": "^6.14.0", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", - "tinymce": "4.9.7", + "tinymce": "4.9.10", "typeahead.js": "0.11.1", "underscore": "1.9.1" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js index ead54b3fc3..8efaf0c024 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function AppHeaderDirective(eventsService, appState, userService, focusService) { + function AppHeaderDirective(eventsService, appState, userService, focusService, backdropService) { function link(scope, el, attr, ctrl) { @@ -18,20 +18,20 @@ ]; // when a user logs out or timesout - evts.push(eventsService.on("app.notAuthenticated", function() { + evts.push(eventsService.on("app.notAuthenticated", function () { scope.authenticated = false; scope.user = null; })); // when the application is ready and the user is authorized setup the data - evts.push(eventsService.on("app.ready", function(evt, data) { - + evts.push(eventsService.on("app.ready", function (evt, data) { + scope.authenticated = true; scope.user = data.user; if (scope.user.avatars) { scope.avatar = []; - if (angular.isArray(scope.user.avatars)) { + if (Utilities.isArray(scope.user.avatars)) { for (var i = 0; i < scope.user.avatars.length; i++) { scope.avatar.push({ value: scope.user.avatars[i] }); } @@ -40,13 +40,13 @@ })); - evts.push(eventsService.on("app.userRefresh", function(evt) { - userService.refreshCurrentUser().then(function(data) { + evts.push(eventsService.on("app.userRefresh", function (evt) { + userService.refreshCurrentUser().then(function (data) { scope.user = data; - + if (scope.user.avatars) { scope.avatar = []; - if (angular.isArray(scope.user.avatars)) { + if (Utilities.isArray(scope.user.avatars)) { for (var i = 0; i < scope.user.avatars.length; i++) { scope.avatar.push({ value: scope.user.avatars[i] }); } @@ -54,10 +54,10 @@ } }); })); - + scope.rememberFocus = focusService.rememberFocus; - - scope.searchClick = function() { + + scope.searchClick = function () { var showSearch = appState.getSearchState("show"); appState.setSearchState("show", !showSearch); }; @@ -71,13 +71,15 @@ }; scope.avatarClick = function () { - if(!scope.userDialog) { + if (!scope.userDialog) { + backdropService.open(); scope.userDialog = { view: "user", show: true, close: function (oldModel) { scope.userDialog.show = false; scope.userDialog = null; + backdropService.close(); } }; } else { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbbackdrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbbackdrop.directive.js index 39e4f10666..4b24075748 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbbackdrop.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbbackdrop.directive.js @@ -7,8 +7,8 @@ var events = []; - scope.clickBackdrop = function(event) { - if(scope.disableEventsOnClick === true) { + scope.clickBackdrop = function (event) { + if (scope.disableEventsOnClick === true) { event.preventDefault(); event.stopPropagation(); } @@ -22,16 +22,16 @@ } - function setHighlight () { + function setHighlight() { scope.loading = true; $timeout(function () { // The element to highlight - var highlightElement = angular.element(scope.highlightElement); + var highlightElement = $(scope.highlightElement); - if(highlightElement && highlightElement.length > 0) { + if (highlightElement && highlightElement.length > 0) { var offset = highlightElement.offset(); var width = highlightElement.outerWidth(); @@ -48,7 +48,7 @@ var rectRight = el.find(".umb-backdrop__rect--right"); var rectBottom = el.find(".umb-backdrop__rect--bottom"); var rectLeft = el.find(".umb-backdrop__rect--left"); - + // Add the css scope.rectTopCss = { "height": topDistance, "left": leftDistance + "px", opacity: scope.backdropOpacity }; scope.rectRightCss = { "left": leftAndWidth + "px", "top": topDistance + "px", "height": height, opacity: scope.backdropOpacity }; @@ -56,14 +56,14 @@ scope.rectLeftCss = { "width": leftDistance, opacity: scope.backdropOpacity }; // Prevent interaction in the highlighted area - if(scope.highlightPreventClick) { + if (scope.highlightPreventClick) { var preventClickElement = el.find(".umb-backdrop__highlight-prevent-click"); preventClickElement.css({ "width": width, "height": height, "left": offset.left, "top": offset.top }); } } - scope.loading = false; + scope.loading = false; }); @@ -74,8 +74,8 @@ } events.push(scope.$watch("highlightElement", function (newValue, oldValue) { - if(!newValue) {return;} - if(newValue === oldValue) {return;} + if (!newValue) { return; } + if (newValue === oldValue) { return; } setHighlight(); })); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 541cc647fb..6dd740e08b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -63,10 +63,14 @@ vm.labels = {}; localizationService.localizeMany([ vm.usernameIsEmail ? "general_email" : "general_username", - vm.usernameIsEmail ? "placeholders_email" : "placeholders_usernameHint"] + vm.usernameIsEmail ? "placeholders_email" : "placeholders_usernameHint", + vm.usernameIsEmail ? "placeholders_emptyEmail" : "placeholders_emptyUsername", + "placeholders_emptyPassword"] ).then(function (data) { vm.labels.usernameLabel = data[0]; vm.labels.usernamePlaceholder = data[1]; + vm.labels.usernameError = data[2]; + vm.labels.passwordError = data[3]; }); vm.twoFactor = {}; @@ -193,70 +197,70 @@ } function loginSubmit() { - - // make sure that we are returning to the login view. - vm.view = "login"; - - // TODO: Do validation properly like in the invite password update + + if (formHelper.submitForm({ scope: $scope })) { + //if the login and password are not empty we need to automatically + // validate them - this is because if there are validation errors on the server + // then the user has to change both username & password to resubmit which isn't ideal, + // so if they're not empty, we'll just make sure to set them to valid. + if (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + + if (vm.loginForm.$invalid) { + SetTitle(); + return; + } + + // make sure that we are returning to the login view. + vm.view = "login"; - //if the login and password are not empty we need to automatically - // validate them - this is because if there are validation errors on the server - // then the user has to change both username & password to resubmit which isn't ideal, - // so if they're not empty, we'll just make sure to set them to valid. - if (vm.login && vm.password && vm.login.length > 0 && vm.password.length > 0) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } + vm.loginStates.submitButton = "busy"; - if (vm.loginForm.$invalid) { - return; - } + userService.authenticate(vm.login, vm.password) + .then(function(data) { + vm.loginStates.submitButton = "success"; + userService._retryRequestQueue(true); + if (vm.onLogin) { + vm.onLogin(); + } + }, + function(reason) { - vm.loginStates.submitButton = "busy"; + //is Two Factor required? + if (reason.status === 402) { + vm.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView); + } else { + vm.loginStates.submitButton = "error"; + vm.errorMsg = reason.errorMsg; - userService.authenticate(vm.login, vm.password) - .then(function (data) { - vm.loginStates.submitButton = "success"; - userService._retryRequestQueue(true); - if(vm.onLogin) { - vm.onLogin(); + //set the form inputs to invalid + vm.loginForm.username.$setValidity("auth", false); + vm.loginForm.password.$setValidity("auth", false); + } + + userService._retryRequestQueue(); + + }); + + //setup a watch for both of the model values changing, if they change + // while the form is invalid, then revalidate them so that the form can + // be submitted again. + vm.loginForm.username.$viewChangeListeners.push(function() { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); } - }, - function (reason) { - - //is Two Factor required? - if (reason.status === 402) { - vm.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView); - } - else { - vm.loginStates.submitButton = "error"; - vm.errorMsg = reason.errorMsg; - - //set the form inputs to invalid - vm.loginForm.username.$setValidity("auth", false); - vm.loginForm.password.$setValidity("auth", false); - } - - userService._retryRequestQueue(); - }); - - //setup a watch for both of the model values changing, if they change - // while the form is invalid, then revalidate them so that the form can - // be submitted again. - vm.loginForm.username.$viewChangeListeners.push(function () { - if (vm.loginForm.$invalid) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } - }); - vm.loginForm.password.$viewChangeListeners.push(function () { - if (vm.loginForm.$invalid) { - vm.loginForm.username.$setValidity('auth', true); - vm.loginForm.password.$setValidity('auth', true); - } - }); + vm.loginForm.password.$viewChangeListeners.push(function() { + if (vm.loginForm.$invalid) { + vm.loginForm.username.$setValidity('auth', true); + vm.loginForm.password.$setValidity('auth', true); + } + }); + } } function requestPasswordResetSubmit(email) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js index 8434a96ba5..e03e63b68f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsearch.directive.js @@ -12,7 +12,7 @@ onClose: "&" } }; - + function umbSearchController($timeout, backdropService, searchService, focusService) { var vm = this; @@ -25,7 +25,7 @@ vm.handleKeyDown = handleKeyDown; vm.closeSearch = closeSearch; vm.focusSearch = focusSearch; - + //we need to capture the focus before this element is initialized. vm.focusBeforeOpening = focusService.getLastKnownFocus(); @@ -66,8 +66,8 @@ */ function focusSearch() { vm.searchHasFocus = false; - $timeout(function(){ - vm.searchHasFocus = true; + $timeout(function () { + vm.searchHasFocus = true; }); } @@ -76,14 +76,14 @@ * @param {object} event */ function handleKeyDown(event) { - + // esc - if(event.keyCode === 27) { + if (event.keyCode === 27) { event.stopPropagation(); event.preventDefault(); - + closeSearch(); - return; + return; } // up/down (navigate search results) @@ -132,7 +132,7 @@ } $timeout(function () { - var resultElementLink = angular.element(".umb-search-item[active-result='true'] .umb-search-result__link"); + var resultElementLink = $(".umb-search-item[active-result='true'] .umb-search-result__link"); resultElementLink[0].focus(); }); } @@ -142,10 +142,10 @@ * Used to proxy a callback */ function closeSearch() { - if(vm.focusBeforeOpening) { + if (vm.focusBeforeOpening) { vm.focusBeforeOpening.focus(); } - if(vm.onClose) { + if (vm.onClose) { vm.onClose(); } } @@ -155,9 +155,9 @@ * @param {string} searchQuery */ function search(searchQuery) { - if(searchQuery.length > 0) { - var search = {"term": searchQuery}; - searchService.searchAll(search).then(function(result){ + if (searchQuery.length > 0) { + var search = { "term": searchQuery }; + searchService.searchAll(search).then(function (result) { //result is a dictionary of group Title and it's results var filtered = {}; _.each(result, function (value, key) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 287962b6d3..6f98dbca6e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -198,27 +198,27 @@ In the following example you see how to run some custom logic before a step goes scope.loadingStep = false; scope.elementNotFound = false; - scope.model.nextStep = function() { + scope.model.nextStep = function () { nextStep(); }; - scope.model.endTour = function() { + scope.model.endTour = function () { unbindEvent(); tourService.endTour(scope.model); backdropService.close(); }; - scope.model.completeTour = function() { + scope.model.completeTour = function () { unbindEvent(); - tourService.completeTour(scope.model).then(function() { - backdropService.close(); + tourService.completeTour(scope.model).then(function () { + backdropService.close(); }); }; - scope.model.disableTour = function() { + scope.model.disableTour = function () { unbindEvent(); - tourService.disableTour(scope.model).then(function() { - backdropService.close(); + tourService.disableTour(scope.model).then(function () { + backdropService.close(); }); } @@ -227,7 +227,7 @@ In the following example you see how to run some custom logic before a step goes pulseElement = el.find(".umb-tour__pulse"); popover.hide(); scope.model.currentStepIndex = 0; - backdropService.open({disableEventsOnClick: true}); + backdropService.open({ disableEventsOnClick: true }); startStep(); } @@ -249,20 +249,20 @@ In the following example you see how to run some custom logic before a step goes } function nextStep() { - + popover.hide(); pulseElement.hide(); $timeout.cancel(pulseTimer); scope.model.currentStepIndex++; // make sure we don't go too far - if(scope.model.currentStepIndex !== scope.model.steps.length) { + if (scope.model.currentStepIndex !== scope.model.steps.length) { startStep(); - // tour completed - final step + // tour completed - final step } else { scope.loadingStep = true; - waitForPendingRerequests().then(function(){ + waitForPendingRerequests().then(function () { scope.loadingStep = false; // clear current step scope.model.currentStep = {}; @@ -280,17 +280,17 @@ In the following example you see how to run some custom logic before a step goes backdropService.setOpacity(scope.model.steps[scope.model.currentStepIndex].backdropOpacity); backdropService.setHighlight(null); - waitForPendingRerequests().then(function() { + waitForPendingRerequests().then(function () { scope.model.currentStep = scope.model.steps[scope.model.currentStepIndex]; setView(); - + // if highlight element is set - find it findHighlightElement(); // if a custom event needs to be bound we do it now - if(scope.model.currentStep.event) { + if (scope.model.currentStep.event) { bindEvent(); } @@ -301,7 +301,7 @@ In the following example you see how to run some custom logic before a step goes function findHighlightElement() { - scope.elementNotFound = false; + scope.elementNotFound = false; $timeout(function () { // clear element when step as marked as intro, so it always displays in the center @@ -312,15 +312,15 @@ In the following example you see how to run some custom logic before a step goes } // if an element isn't set - show the popover in the center - if(scope.model.currentStep && !scope.model.currentStep.element) { + if (scope.model.currentStep && !scope.model.currentStep.element) { setPopoverPosition(null); return; } - var element = angular.element(scope.model.currentStep.element); + var element = $(scope.model.currentStep.element); // we couldn't find the element in the dom - abort and show error - if(element.length === 0) { + if (element.length === 0) { scope.elementNotFound = true; setPopoverPosition(null); return; @@ -337,7 +337,7 @@ In the following example you see how to run some custom logic before a step goes el = el.offsetParent(); } } - + var scrollToCenterOfContainer = offsetTop - (scrollParent[0].clientHeight / 2); if (element[0].clientHeight < scrollParent[0].clientHeight) { scrollToCenterOfContainer += (element[0].clientHeight / 2); @@ -366,7 +366,7 @@ In the following example you see how to run some custom logic before a step goes function setPopoverPosition(element) { $timeout(function () { - + var position = "center"; var margin = 20; var css = {}; @@ -374,10 +374,10 @@ In the following example you see how to run some custom logic before a step goes var popoverWidth = popover.outerWidth(); var popoverHeight = popover.outerHeight(); var popoverOffset = popover.offset(); - var documentWidth = angular.element(document).width(); - var documentHeight = angular.element(document).height(); + var documentWidth = $(document).width(); + var documentHeight = $(document).height(); - if(element) { + if (element) { var offset = element.offset(); var width = element.outerWidth(); @@ -436,29 +436,29 @@ In the following example you see how to run some custom logic before a step goes } else { // if there is no dom element center the popover - css.top = "calc(50% - " + popoverHeight/2 + "px)"; - css.left = "calc(50% - " + popoverWidth/2 + "px)"; + css.top = "calc(50% - " + popoverHeight / 2 + "px)"; + css.left = "calc(50% - " + popoverWidth / 2 + "px)"; } popover.css(css).fadeIn("fast"); - + }); } function setPulsePosition() { - if(scope.model.currentStep.event) { + if (scope.model.currentStep.event) { + + pulseTimer = $timeout(function () { - pulseTimer = $timeout(function(){ - var clickElementSelector = scope.model.currentStep.eventElement ? scope.model.currentStep.eventElement : scope.model.currentStep.element; var clickElement = $(clickElementSelector); - + var offset = clickElement.offset(); var width = clickElement.outerWidth(); var height = clickElement.outerHeight(); - + pulseElement.css({ "width": width, "height": height, "left": offset.left, "top": offset.top }); pulseElement.fadeIn(); @@ -468,24 +468,24 @@ In the following example you see how to run some custom logic before a step goes function waitForPendingRerequests() { var deferred = $q.defer(); - var timer = window.setInterval(function(){ - + var timer = window.setInterval(function () { + var requestsReady = false; var animationsDone = false; // check for pending requests both in angular and on the document - if($http.pendingRequests.length === 0 && document.readyState === "complete") { + if ($http.pendingRequests.length === 0 && document.readyState === "complete") { requestsReady = true; } // check for animations. ng-enter and ng-leave are default angular animations. // Also check for infinite editors animating - if(document.querySelectorAll(".ng-enter, .ng-leave, .umb-editor--animating").length === 0) { + if (document.querySelectorAll(".ng-enter, .ng-leave, .umb-editor--animating").length === 0) { animationsDone = true; } - if(requestsReady && animationsDone) { - $timeout(function(){ + if (requestsReady && animationsDone) { + $timeout(function () { deferred.resolve(); clearInterval(timer); }); @@ -512,14 +512,14 @@ In the following example you see how to run some custom logic before a step goes var bindToElement = scope.model.currentStep.element; var eventName = scope.model.currentStep.event + ".step-" + scope.model.currentStepIndex; var removeEventName = "remove.step-" + scope.model.currentStepIndex; - var handled = false; + var handled = false; - if(scope.model.currentStep.eventElement) { + if (scope.model.currentStep.eventElement) { bindToElement = scope.model.currentStep.eventElement; } - $(bindToElement).on(eventName, function(){ - if(!handled) { + $(bindToElement).on(eventName, function () { + if (!handled) { unbindEvent(); nextStep(); handled = true; @@ -530,7 +530,7 @@ In the following example you see how to run some custom logic before a step goes // for some reason it seems the elements gets removed before the event is raised. This is a temp solution which assumes: // "if you ask me to click on an element, and it suddenly gets removed from the dom, let's go on to the next step". $(bindToElement).on(removeEventName, function () { - if(!handled) { + if (!handled) { unbindEvent(); nextStep(); handled = true; @@ -542,13 +542,13 @@ In the following example you see how to run some custom logic before a step goes function unbindEvent() { var eventName = scope.model.currentStep.event + ".step-" + scope.model.currentStepIndex; var removeEventName = "remove.step-" + scope.model.currentStepIndex; - - if(scope.model.currentStep.eventElement) { - angular.element(scope.model.currentStep.eventElement).off(eventName); - angular.element(scope.model.currentStep.eventElement).off(removeEventName); + + if (scope.model.currentStep.eventElement) { + $(scope.model.currentStep.eventElement).off(eventName); + $(scope.model.currentStep.eventElement).off(removeEventName); } else { - angular.element(scope.model.currentStep.element).off(eventName); - angular.element(scope.model.currentStep.element).off(removeEventName); + $(scope.model.currentStep.element).off(eventName); + $(scope.model.currentStep.element).off(removeEventName); } } 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 91b0ba8754..c07bb9bc83 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 @@ -31,14 +31,14 @@ $scope.page.hideActionsMenu = infiniteMode ? true : false; $scope.page.hideChangeVariant = false; $scope.allowOpen = true; - $scope.app = null; + $scope.activeApp = null; //initializes any watches function startWatches(content) { //watch for changes to isNew, set the page.isNew accordingly and load the breadcrumb if we can $scope.$watch('isNew', function (newVal, oldVal) { - + $scope.page.isNew = Object.toBoolean(newVal); //We fetch all ancestors of the node to generate the footer breadcrumb navigation @@ -74,31 +74,23 @@ var isAppPresent = false; // on first init, we dont have any apps. but if we are re-initializing, we do, but ... - if ($scope.app) { + if ($scope.activeApp) { - // lets check if it still exists as part of our apps array. (if not we have made a change to our docType, even just a re-save of the docType it will turn into new Apps.) _.forEach(content.apps, function (app) { - if (app === $scope.app) { + if (app.alias === $scope.activeApp.alias) { isAppPresent = true; + $scope.appChanged(app); } }); - // if we did reload our DocType, but still have the same app we will try to find it by the alias. if (isAppPresent === false) { - _.forEach(content.apps, function (app) { - if (app.alias === $scope.app.alias) { - isAppPresent = true; - app.active = true; - $scope.appChanged(app); - } - }); + // active app does not exist anymore. + $scope.activeApp = null; } - } // if we still dont have a app, lets show the first one: - if (isAppPresent === false && content.apps.length) { - content.apps[0].active = true; + if ($scope.activeApp === null && content.apps.length) { $scope.appChanged(content.apps[0]); } // otherwise make sure the save options are up to date with the current content state @@ -151,8 +143,8 @@ } /** Returns true if the content item varies by culture */ - function isContentCultureVariant() { - return $scope.content.variants.length > 1; + function hasVariants(content) { + return content.variants.length > 1; } function reload() { @@ -182,32 +174,32 @@ } })); - evts.push(eventsService.on("editors.content.reload", function (name, args) { + evts.push(eventsService.on("editors.content.reload", function (name, args) { if (args && args.node && $scope.content.id === args.node.id) { reload(); loadBreadcrumb(); syncTreeNode($scope.content, $scope.content.path); } })); - - evts.push(eventsService.on("rte.file.uploading", function(){ + + evts.push(eventsService.on("rte.file.uploading", function () { $scope.page.saveButtonState = "busy"; $scope.page.buttonGroupState = "busy"; })); - evts.push(eventsService.on("rte.file.uploaded", function(){ + evts.push(eventsService.on("rte.file.uploaded", function () { $scope.page.saveButtonState = "success"; $scope.page.buttonGroupState = "success"; })); - evts.push(eventsService.on("rte.shortcut.save", function(){ + evts.push(eventsService.on("rte.shortcut.save", function () { if ($scope.page.showSaveButton) { $scope.save(); } })); - evts.push(eventsService.on("content.saved", function(){ + evts.push(eventsService.on("content.saved", function () { // Clear out localstorage keys that start with tinymce__ // When we save/perist a content node // NOTE: clearAll supports a RegEx pattern of items to remove @@ -215,6 +207,13 @@ })); } + function appendRuntimeData() { + $scope.content.variants.forEach((variant) => { + variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); + variant.htmlId = "_content_variant_" + variant.compositeId + "_"; + }); + } + /** * This does the content loading and initializes everything, called on first load */ @@ -226,6 +225,7 @@ $scope.content = data; + appendRuntimeData(); init(); syncTreeNode($scope.content, $scope.content.path, true); @@ -251,6 +251,7 @@ $scope.content = data; + appendRuntimeData(); init(); startWatches($scope.content); @@ -274,7 +275,7 @@ $scope.page.saveButtonStyle = content.trashed || content.isElement || content.isBlueprint ? "primary" : "info"; // only create the save/publish/preview buttons if the // content app is "Conent" - if ($scope.app && $scope.app.alias !== "umbContent" && $scope.app.alias !== "umbInfo" && $scope.app.alias !== "umbListView") { + if ($scope.activeApp && $scope.activeApp.alias !== "umbContent" && $scope.activeApp.alias !== "umbInfo" && $scope.activeApp.alias !== "umbListView") { $scope.defaultButton = null; $scope.subButtons = null; $scope.page.showSaveButton = false; @@ -323,7 +324,7 @@ .then(function (syncArgs) { $scope.page.menu.currentNode = syncArgs.node; if (reloadChildren && syncArgs.node.expanded) { - treeService.loadNodeChildren({node: syncArgs.node}); + treeService.loadNodeChildren({ node: syncArgs.node }); } }, function () { //handle the rejection @@ -589,7 +590,7 @@ $scope.sendToPublish = function () { clearNotifications($scope.content); - if (isContentCultureVariant()) { + if (hasVariants($scope.content)) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publish" })) { @@ -649,7 +650,7 @@ $scope.saveAndPublish = function () { clearNotifications($scope.content); - if (isContentCultureVariant()) { + if (hasVariants($scope.content)) { //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publish" })) { var dialog = { @@ -711,7 +712,7 @@ $scope.save = function () { 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 (isContentCultureVariant()) { + 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" })) { @@ -776,7 +777,7 @@ clearNotifications($scope.content); //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "schedule" })) { - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { //ensure the flags are set $scope.content.variants[0].save = true; } @@ -784,7 +785,7 @@ var dialog = { parentScope: $scope, view: "views/content/overlays/schedule.html", - variants: angular.copy($scope.content.variants), //set a model property for the dialog + variants: Utilities.copy($scope.content.variants), //set a model property for the dialog skipFormValidation: true, //when submitting the overlay form, skip any client side validation submitButtonLabelKey: "buttons_schedulePublish", submit: function (model) { @@ -813,12 +814,12 @@ }, function (err) { clearDirtyState($scope.content.variants); //if this is invariant, show the notification errors, else they'll be shown inline with the variant - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { formHelper.showNotifications(err.data); } model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties - dialog.variants = angular.copy($scope.content.variants); + dialog.variants = Utilities.copy($scope.content.variants); //don't reject, we've handled the error return $q.when(err); }); @@ -840,7 +841,7 @@ //before we launch the dialog we want to execute all client side validations first if (formHelper.submitForm({ scope: $scope, action: "publishDescendants" })) { - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { //ensure the flags are set $scope.content.variants[0].save = true; $scope.content.variants[0].publish = true; @@ -873,7 +874,7 @@ }, function (err) { clearDirtyState($scope.content.variants); //if this is invariant, show the notification errors, else they'll be shown inline with the variant - if (!isContentCultureVariant()) { + if (!hasVariants($scope.content)) { formHelper.showNotifications(err.data); } model.submitButtonState = "error"; @@ -963,11 +964,18 @@ * Call back when a content app changes * @param {any} app */ - $scope.appChanged = function (app) { + $scope.appChanged = function (activeApp) { - $scope.app = app; + $scope.activeApp = activeApp; + + _.forEach($scope.content.apps, function (app) { + app.active = false; + if (app.alias === $scope.activeApp.alias) { + app.active = true; + } + }); - $scope.$broadcast("editors.apps.appChanged", { app: app }); + $scope.$broadcast("editors.apps.appChanged", { app: activeApp }); createButtons($scope.content); @@ -1029,6 +1037,7 @@ getMethod: "&", getScaffoldMethod: "&?", culture: "=?", + segment: "=?", infiniteModel: "=?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index 95b2a520d1..1e929af6e9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -323,7 +323,7 @@ // find the urls for the currently selected language if (scope.node.variants.length > 1) { // nodes with variants - scope.currentUrls = _.filter(scope.node.urls, (url) => scope.currentVariant.language.culture === url.culture); + scope.currentUrls = _.filter(scope.node.urls, (url) => (scope.currentVariant.language && scope.currentVariant.language.culture === url.culture)); } else { // invariant nodes scope.currentUrls = scope.node.urls; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js index 06f426889f..3aa0470262 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js @@ -4,7 +4,7 @@ /** This directive is used to render out the current variant tabs and properties and exposes an API for other directives to consume */ function tabbedContentDirective($timeout) { - function link($scope, $element, $attrs) { + function link($scope, $element) { var appRootNode = $element[0]; @@ -115,21 +115,18 @@ } - function controller($scope, $element, $attrs) { - + function controller($scope) { //expose the property/methods for other directives to use this.content = $scope.content; - this.activeVariant = _.find(this.content.variants, variant => { - return variant.active; - }); - - $scope.activeVariant = this.activeVariant; - - $scope.defaultVariant = _.find(this.content.variants, variant => { - return variant.language.isDefault; - }); - + + if($scope.contentNodeModel) { + $scope.defaultVariant = _.find($scope.contentNodeModel.variants, variant => { + // defaultVariant will never have segment. Wether it has a language or not depends on the setup. + return !variant.segment && ((variant.language && variant.language.isDefault) || (!variant.language)); + }); + } + $scope.unlockInvariantValue = function(property) { property.unlockInvariantValue = !property.unlockInvariantValue; }; @@ -141,6 +138,24 @@ } } ); + + $scope.propertyEditorDisabled = function (property) { + if (property.unlockInvariantValue) { + return false; + } + + var contentLanguage = $scope.content.language; + + var canEditCulture = !contentLanguage || + // If the property culture equals the content culture it can be edited + property.culture === contentLanguage.culture || + // A culture-invariant property can only be edited by the default language variant + (property.culture == null && contentLanguage.isDefault); + + var canEditSegment = property.segment === $scope.content.segment; + + return !canEditCulture || !canEditSegment; + } } var directive = { @@ -150,7 +165,8 @@ controller: controller, link: link, scope: { - content: "=" + content: "=", // in this context the content is the variant model. + contentNodeModel: "=?" //contentNodeModel is the content model for the node, } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js index 6fec20b256..2d3a8e2238 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js @@ -12,7 +12,6 @@ editor: "<", editorIndex: "<", editorCount: "<", - openVariants: "<", onCloseSplitView: "&", onSelectVariant: "&", onOpenSplitView: "&", @@ -25,7 +24,7 @@ controller: umbVariantContentController }; - function umbVariantContentController($scope, $element, $location) { + function umbVariantContentController($scope) { var unsubscribe = []; @@ -42,13 +41,14 @@ vm.showBackButton = showBackButton; function onInit() { - // disable the name field if the active content app is not "Content" - vm.nameDisabled = false; - angular.forEach(vm.editor.content.apps, function(app){ - if(app.active && app.alias !== "umbContent" && app.alias !== "umbInfo" && app.alias !== "umbListView") { - vm.nameDisabled = true; - } - }); + + // Make copy of apps, so we can have a variant specific model for the App. (needed for validation etc.) + vm.editor.variantApps = Utilities.copy(vm.content.apps); + + var activeApp = vm.content.apps.find((app) => app.active); + + onAppChanged(activeApp); + } function showBackButton() { @@ -94,14 +94,23 @@ } $scope.$on("editors.apps.appChanged", function($event, $args) { - var app = $args.app; - // disable the name field if the active content app is not "Content" or "Info" - vm.nameDisabled = false; - if(app && app.alias !== "umbContent" && app.alias !== "umbInfo" && app.alias !== "umbListView") { - vm.nameDisabled = true; - } + var activeApp = $args.app; + + // sync varaintApps active with new active. + _.forEach(vm.editor.variantApps, function (app) { + app.active = (app.alias === activeApp.alias); + }); + + onAppChanged(activeApp); }); + function onAppChanged(activeApp) { + + // disable the name field if the active content app is not "Content" or "Info" + vm.nameDisabled = (activeApp && activeApp.alias !== "umbContent" && activeApp.alias !== "umbInfo" && activeApp.alias !== "umbListView"); + + } + /** * Used to proxy a callback * @param {any} item diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index a4dac046e5..a188a83d83 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -8,8 +8,9 @@ templateUrl: 'views/components/content/umb-variant-content-editors.html', bindings: { page: "<", - content: "<", // TODO: Not sure if this should be = since we are changing the 'active' property of a variant + content: "<", culture: "<", + segment: "<", onSelectApp: "&?", onSelectAppAnchor: "&?", onBack: "&?", @@ -19,12 +20,11 @@ controller: umbVariantContentEditorsController }; - function umbVariantContentEditorsController($scope, $location, $timeout) { + function umbVariantContentEditorsController($scope, $location, contentEditingHelper) { var prevContentDateUpdated = null; var vm = this; - var activeAppAlias = null; vm.$onInit = onInit; vm.$onChanges = onChanges; @@ -39,13 +39,11 @@ //Used to track how many content views there are (for split view there will be 2, it could support more in theory) vm.editors = []; - //Used to track the open variants across the split views - vm.openVariants = []; /** Called when the component initializes */ function onInit() { - prevContentDateUpdated = angular.copy(vm.content.updateDate); - setActiveCulture(); + prevContentDateUpdated = Utilities.copy(vm.content.updateDate); + setActiveVariant(); } /** Called when the component has linked all elements, this is when the form controller is available */ @@ -60,15 +58,17 @@ function onChanges(changes) { if (changes.culture && !changes.culture.isFirstChange() && changes.culture.currentValue !== changes.culture.previousValue) { - setActiveCulture(); + setActiveVariant(); + } else if (changes.segment && !changes.segment.isFirstChange() && changes.segment.currentValue !== changes.segment.previousValue) { + setActiveVariant(); } } /** Allows us to deep watch whatever we want - executes on every digest cycle */ function doCheck() { if (!angular.equals(vm.content.updateDate, prevContentDateUpdated)) { - setActiveCulture(); - prevContentDateUpdated = angular.copy(vm.content.updateDate); + setActiveVariant(); + prevContentDateUpdated = Utilities.copy(vm.content.updateDate); } } @@ -79,37 +79,32 @@ } /** - * Set the active variant based on the current culture (query string) + * Set the active variant based on the current culture or segment (query string) */ - function setActiveCulture() { + function setActiveVariant() { // set the active variant var activeVariant = null; _.each(vm.content.variants, function (v) { - if (v.language && v.language.culture === vm.culture) { - v.active = true; + if ((vm.culture === "invariant" || v.language && v.language.culture === vm.culture) && v.segment === vm.segment) { activeVariant = v; } - else { - v.active = false; - } }); if (!activeVariant) { // Set the first variant to active if we can't find it. // If the content item is invariant, then only one item exists in the array. - vm.content.variants[0].active = true; activeVariant = vm.content.variants[0]; } - insertVariantEditor(0, initVariant(activeVariant, 0)); + insertVariantEditor(0, activeVariant); if (vm.editors.length > 1) { //now re-sync any other editor content (i.e. if split view is open) for (var s = 1; s < vm.editors.length; s++) { //get the variant from the scope model var variant = _.find(vm.content.variants, function (v) { - return v.language.culture === vm.editors[s].content.language.culture; + return (!v.language || v.language.culture === vm.editors[s].content.language.culture) && v.segment === vm.editors[s].content.segment; }); - vm.editors[s].content = initVariant(variant, s); + vm.editors[s].content = variant; } } @@ -122,157 +117,84 @@ */ function insertVariantEditor(index, variant) { + if (vm.editors[index]) { + if (vm.editors[index].content === variant) { + // This variant is already the content of the editor in this index. + return; + } + vm.editors[index].content.active = false; + } + variant.active = true; + var variantCulture = variant.language ? variant.language.culture : "invariant"; + var variantSegment = variant.segment; - //check if the culture at the index is the same, if it's null an editor will be added - var currentCulture = vm.editors.length === 0 || vm.editors.length <= index ? null : vm.editors[index].culture; + var currentCulture = index < vm.editors.length ? vm.editors[index].culture : null; + var currentSegment = index < vm.editors.length ? vm.editors[index].segment : null; + + // if index not already exists or if the culture or segment isnt identical then we do a replacement. + if (index >= vm.editors.length || currentCulture !== variantCulture || currentSegment !== variantSegment) { - if (currentCulture !== variantCulture) { - //Not the current culture which means we need to modify the array. + //Not the current culture or segment which means we need to modify the array. //NOTE: It is not good enough to just replace the `content` object at a given index in the array // since that would mean that directives are not re-initialized. vm.editors.splice(index, 1, { + compositeId: variant.compositeId, content: variant, - //used for "track-by" ng-repeat - culture: variantCulture + culture: variantCulture, + segment: variantSegment }); } else { - //replace the editor for the same culture + //replace the content of the editor, since the culture and segment is the same. vm.editors[index].content = variant; } + } - - function initVariant(variant, editorIndex) { - //The model that is assigned to the editor contains the current content variant along - //with a copy of the contentApps. This is required because each editor renders it's own - //header and content apps section and the content apps contains the view for editing content itself - //and we need to assign a view model to the subView so that it is scoped to the current - //editor so that split views work. - - //copy the apps from the main model if not assigned yet to the variant - if (!variant.apps) { - variant.apps = angular.copy(vm.content.apps); - } - - //if this is a variant has a culture/language than we need to assign the language drop down info - if (variant.language) { - //if the variant list that defines the header drop down isn't assigned to the variant then assign it now - if (!variant.variants) { - variant.variants = _.map(vm.content.variants, - function (v) { - return _.pick(v, "active", "language", "state"); - }); - } - else { - //merge the scope variants on top of the header variants collection (handy when needing to refresh) - angular.extend(variant.variants, - _.map(vm.content.variants, - function (v) { - return _.pick(v, "active", "language", "state"); - })); - } - - //ensure the current culture is set as the active one - for (var i = 0; i < variant.variants.length; i++) { - if (variant.variants[i].language.culture === variant.language.culture) { - variant.variants[i].active = true; - } - else { - variant.variants[i].active = false; - } - } - - // keep track of the open variants across the different split views - // push the first variant then update the variant index based on the editor index - if(vm.openVariants && vm.openVariants.length === 0) { - vm.openVariants.push(variant.language.culture); - } else { - vm.openVariants[editorIndex] = variant.language.culture; - } - - } - - //then assign the variant to a view model to the content app - var contentApp = _.find(variant.apps, function (a) { - return a.alias === "umbContent"; - }); - - if (contentApp) { - //The view model for the content app is simply the index of the variant being edited - var variantIndex = vm.content.variants.indexOf(variant); - contentApp.viewModel = variantIndex; - } - - // make sure the same app it set to active in the new variant - if(activeAppAlias) { - angular.forEach(variant.apps, function(app) { - app.active = false; - if(app.alias === activeAppAlias) { - app.active = true; - } - }); - } - - return variant; - } + /** * Adds a new editor to the editors array to show content in a split view * @param {any} selectedVariant */ function openSplitView(selectedVariant) { - var selectedCulture = selectedVariant.language.culture; + // enforce content contentApp in splitview. + var contentApp = vm.content.apps.find((app) => app.alias === "umbContent"); + if(contentApp) { + selectApp(contentApp); + } + + insertVariantEditor(vm.editors.length, selectedVariant); + + splitViewChanged(); + + } + + $scope.$on("editors.content.splitViewRequest", function(event, args) {requestSplitView(args);}); + vm.requestSplitView = requestSplitView; + function requestSplitView(args) { + var culture = args.culture; + var segment = args.segment; - //Find the whole variant model based on the culture that was chosen var variant = _.find(vm.content.variants, function (v) { - return v.language.culture === selectedCulture; + return (!v.language || v.language.culture === culture) && v.segment === segment; }); - insertVariantEditor(vm.editors.length, initVariant(variant, vm.editors.length)); - - //only the content app can be selected since no other apps are shown, and because we copy all of these apps - //to the "editors" we need to update this across all editors - for (var e = 0; e < vm.editors.length; e++) { - var editor = vm.editors[e]; - for (var i = 0; i < editor.content.apps.length; i++) { - var app = editor.content.apps[i]; - if (app.alias === "umbContent") { - app.active = true; - // tell the world that the app has changed (but do it only once) - if (e === 0) { - selectApp(app); - } - } - else { - app.active = false; - } - } + if (variant != null) { + openSplitView(variant); } - - // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular - editor.collapsed = true; - editor.loading = true; - $timeout(function () { - editor.collapsed = false; - editor.loading = false; - splitViewChanged(); - }, 100); } /** Closes the split view */ function closeSplitView(editorIndex) { // TODO: hacking animation states - these should hopefully be easier to do when we upgrade angular var editor = vm.editors[editorIndex]; - editor.loading = true; - editor.collapsed = true; - $timeout(function () { - vm.editors.splice(editorIndex, 1); - //remove variant from open variants - vm.openVariants.splice(editorIndex, 1); - //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) - $location.search("cculture", vm.openVariants[0]); - splitViewChanged(); - }, 400); + vm.editors.splice(editorIndex, 1); + editor.content.active = false; + + //update the current culture to reflect the last open variant (closing the split view corresponds to selecting the other variant) + + $location.search({"cculture": vm.editors[0].content.language ? vm.editors[0].content.language.culture : null, "csegment": vm.editors[0].content.segment}); + splitViewChanged(); } /** @@ -282,8 +204,11 @@ */ function selectVariant(variant, editorIndex) { - // prevent variants already open in a split view to be opened - if(vm.openVariants.indexOf(variant.language.culture) !== -1) { + var variantCulture = variant.language ? variant.language.culture : "invariant"; + var variantSegment = variant.segment || null; + + // Check if we already have this editor open, if so, do nothing. + if (vm.editors.find((editor) => (!editor.content.language || editor.content.language.culture === variantCulture) && editor.content.segment === variantSegment)) { return; } @@ -292,27 +217,12 @@ if (editorIndex === 0) { //If we've made it this far, then update the query string. //The editor will respond to this query string changing. - $location.search("cculture", variant.language.culture); + $location.search("cculture", variantCulture).search("csegment", variantSegment); } else { - //Update the 'active' variant for this editor - var editor = vm.editors[editorIndex]; - //set all variant drop down items as inactive for this editor and then set the selected one as active - for (var i = 0; i < editor.content.variants.length; i++) { - editor.content.variants[i].active = false; - } - variant.active = true; - - //get the variant content model and initialize the editor with that - var contentVariant = _.find(vm.content.variants, - function (v) { - return v.language.culture === variant.language.culture; - }); - editor.content = initVariant(contentVariant, editorIndex); - //update the editors collection - insertVariantEditor(editorIndex, contentVariant); + insertVariantEditor(editorIndex, variant); } } @@ -332,14 +242,6 @@ vm.onSelectAppAnchor({"app": app, "anchor": anchor}); } } - - - $scope.$on("editors.apps.appChanged", function($event, $args) { - var app = $args.app; - if(app && app.alias) { - activeAppAlias = app.alias; - } - }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js index 7755d9d63b..f80b3ceb3e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js @@ -73,7 +73,7 @@ Use this directive to generate a list of breadcrumbs. var path = scope.pathTo(ancestor); $location.path(path); - navigationService.clearSearch(["cculture"]); + navigationService.clearSearch(["cculture", "csegment"]); } scope.pathTo = function (ancestor) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js index fe2a6aa40a..7bd812e321 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js @@ -2,11 +2,10 @@ 'use strict'; function EditorContentHeader(serverValidationManager, localizationService, editorState) { + function link(scope) { - - function link(scope, el, attr, ctrl) { var unsubscribe = []; - + if (!scope.serverValidationNameField) { scope.serverValidationNameField = "Name"; } @@ -14,19 +13,20 @@ scope.serverValidationAliasField = "Alias"; } - scope.isNew = scope.content.state == "NotCreated"; - - localizationService.localizeMany([ - scope.isNew ? "visuallyHiddenTexts_createItem" : "visuallyHiddenTexts_edit", - "visuallyHiddenTexts_name", - scope.isNew ? "general_new" : "general_edit"] - ).then(function (data) { + scope.isNew = scope.editor.content.state == "NotCreated"; + localizationService.localizeMany( + [ + scope.isNew ? "placeholders_a11yCreateItem" : "placeholders_a11yEdit", + "placeholders_a11yName", + scope.isNew ? "general_new" : "general_edit" + ] + ).then(function (data) { scope.a11yMessage = data[0]; scope.a11yName = data[1]; var title = data[2] + ": "; if (!scope.isNew) { - scope.a11yMessage += " " + scope.content.name; + scope.a11yMessage += " " + scope.editor.content.name; title += scope.content.name; } else { var name = editorState.current.contentTypeName; @@ -34,199 +34,212 @@ scope.a11yName = name + " " + scope.a11yName; title += name; } - + scope.$emit("$changeTitle", title); }); scope.vm = {}; + scope.vm.hasVariants = false; + scope.vm.hasSubVariants = false; + scope.vm.hasCulture = false; + scope.vm.hasSegments = false; scope.vm.dropdownOpen = false; - scope.vm.currentVariant = ""; scope.vm.variantsWithError = []; scope.vm.defaultVariant = null; - scope.vm.errorsOnOtherVariants = false;// indicating wether to show that other variants, than the current, have errors. + + function updateVaraintErrors() { + scope.content.variants.forEach( function (variant) { + variant.hasError = scope.variantHasError(variant); + + }); + checkErrorsOnOtherVariants(); + } function checkErrorsOnOtherVariants() { var check = false; - angular.forEach(scope.content.variants, function (variant) { - if (scope.openVariants.indexOf(variant.language.culture) === -1 && scope.variantHasError(variant.language.culture)) { + scope.content.variants.forEach( function (variant) { + if (variant.active !== true && variant.hasError) { check = true; } }); scope.vm.errorsOnOtherVariants = check; } + + function onVariantValidation(valid, errors, allErrors, culture, segment) { - function onCultureValidation(valid, errors, allErrors, culture) { - var index = scope.vm.variantsWithError.indexOf(culture); - if (valid === true) { + // only want to react to property errors: + if(errors.findIndex(error => {return error.propertyAlias !== null;}) === -1) { + // we dont have any errors for properties, meaning we will back out. + return; + } + + // If error coming back is invariant, we will assign the error to the default variant by picking the defaultVariant language. + if(culture === "invariant") { + culture = scope.vm.defaultVariant.language.culture; + } + + var index = scope.vm.variantsWithError.findIndex((item) => item.culture === culture && item.segment === segment) + if(valid === true) { if (index !== -1) { scope.vm.variantsWithError.splice(index, 1); } } else { if (index === -1) { - scope.vm.variantsWithError.push(culture); + scope.vm.variantsWithError.push({"culture": culture, "segment": segment}); } } - checkErrorsOnOtherVariants(); + scope.$evalAsync(updateVaraintErrors); } - + function onInit() { - - // find default. - angular.forEach(scope.content.variants, function (variant) { - if (variant.language.isDefault) { + + // find default + check if we have variants. + scope.content.variants.forEach( function (variant) { + if (variant.language !== null && variant.language.isDefault) { scope.vm.defaultVariant = variant; } - }); - - setCurrentVariant(); - - angular.forEach(scope.content.apps, (app) => { - if (app.alias === "umbContent") { - app.anchors = scope.content.tabs; + if (variant.language !== null) { + scope.vm.hasCulture = true; + } + if (variant.segment !== null) { + scope.vm.hasSegments = true; } }); + scope.vm.hasVariants = (scope.vm.hasCulture || scope.vm.hasSegments); + scope.vm.hasSubVariants = (scope.vm.hasCulture && scope.vm.hasSegments); - angular.forEach(scope.content.variants, function (variant) { - unsubscribe.push(serverValidationManager.subscribe(null, variant.language.culture, null, onCultureValidation)); - }); + updateVaraintErrors(); - unsubscribe.push(serverValidationManager.subscribe(null, null, null, onCultureValidation)); - - - - } - - function setCurrentVariant() { - angular.forEach(scope.content.variants, function (variant) { - if (variant.active) { - scope.vm.currentVariant = variant; - checkErrorsOnOtherVariants(); + scope.vm.variantMenu = []; + if (scope.vm.hasCulture) { + scope.content.variants.forEach( (v) => { + if (v.language !== null && v.segment === null) { + var variantMenuEntry = { + key: String.CreateGuid(), + open: v.language && v.language.culture === scope.editor.culture, + variant: v, + subVariants: scope.content.variants.filter( (subVariant) => subVariant.language.culture === v.language.culture && subVariant.segment !== null) + }; + scope.vm.variantMenu.push(variantMenuEntry); + } + }); + } else { + scope.content.variants.forEach( (v) => { + scope.vm.variantMenu.push({ + key: String.CreateGuid(), + variant: v + }); + }); } - }); - } + + scope.editor.variantApps.forEach( (app) => { + if (app.alias === "umbContent") { + app.anchors = scope.editor.content.tabs; + } + }); - scope.goBack = function () { - if (scope.onBack) { - scope.onBack(); + scope.content.variants.forEach( function (variant) { + + // if we are looking for the variant with default language then we also want to check for invariant variant. + if (variant.language && variant.language.culture === scope.vm.defaultVariant.language.culture && variant.segment === null) { + unsubscribe.push(serverValidationManager.subscribe(null, "invariant", null, onVariantValidation, null)); + } + unsubscribe.push(serverValidationManager.subscribe(null, variant.language !== null ? variant.language.culture : null, null, onVariantValidation, variant.segment)); + }); + } - }; - scope.selectVariant = function (event, variant) { + scope.goBack = function () { + if (scope.onBack) { + scope.onBack(); + } + }; - if (scope.onSelectVariant) { - scope.vm.dropdownOpen = false; - scope.onSelectVariant({ "variant": variant }); + scope.selectVariant = function (event, variant) { + + if (scope.onSelectVariant) { + scope.vm.dropdownOpen = false; + scope.onSelectVariant({ "variant": variant }); + } + }; + + scope.selectNavigationItem = function(item) { + if(scope.onSelectNavigationItem) { + scope.onSelectNavigationItem({"item": item}); + } } - }; - scope.selectNavigationItem = function (item) { - if (scope.onSelectNavigationItem) { - scope.onSelectNavigationItem({ "item": item }); + scope.selectAnchorItem = function(item, anchor) { + if(scope.onSelectAnchorItem) { + scope.onSelectAnchorItem({"item": item, "anchor": anchor}); + } } - } - scope.selectAnchorItem = function (item, anchor) { - if (scope.onSelectAnchorItem) { - scope.onSelectAnchorItem({ "item": item, "anchor": anchor }); - } - } + scope.closeSplitView = function () { + if (scope.onCloseSplitView) { + scope.onCloseSplitView(); + } + }; - scope.closeSplitView = function () { - if (scope.onCloseSplitView) { - scope.onCloseSplitView(); - } - }; - - scope.openInSplitView = function (event, variant) { - if (scope.onOpenInSplitView) { - scope.vm.dropdownOpen = false; - scope.onOpenInSplitView({ "variant": variant }); - } - }; - - /** - * keep track of open variants - this is used to prevent the same variant to be open in more than one split view - * @param {any} culture - */ - scope.variantIsOpen = function (culture) { - return (scope.openVariants.indexOf(culture) !== -1); - } - - /** - * Check whether a variant has a error, used to display errors in variant switcher. - * @param {any} culture - */ - scope.variantHasError = function (culture) { - // if we are looking for the default language we also want to check for invariant. - if (culture === scope.vm.defaultVariant.language.culture) { - if (scope.vm.variantsWithError.indexOf("invariant") !== -1) { + scope.openInSplitView = function (event, variant) { + if (scope.onOpenInSplitView) { + scope.vm.dropdownOpen = false; + scope.onOpenInSplitView({ "variant": variant }); + } + }; + + /** + * Check whether a variant has a error, used to display errors in variant switcher. + * @param {any} culture + */ + scope.variantHasError = function(variant) { + if(scope.vm.variantsWithError.find((item) => (!variant.language || item.culture === variant.language.culture) && item.segment === variant.segment) !== undefined) { return true; } + return false; } - if (scope.vm.variantsWithError.indexOf(culture) !== -1) { - return true; - } - return false; - } - onInit(); - - //watch for the active culture changing, if it changes, update the current variant - if (scope.content.variants) { - scope.$watch(function () { - for (var i = 0; i < scope.content.variants.length; i++) { - var v = scope.content.variants[i]; - if (v.active) { - return v.language.culture; - } - } - return scope.vm.currentVariant.language.culture; //should never get here - }, function (newValue, oldValue) { - if (newValue !== scope.vm.currentVariant.language.culture) { - setCurrentVariant(); + onInit(); + + scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); } }); } - scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-editor-content-header.html', + scope: { + name: "=", + nameDisabled: "'); + function resizeInput() { - function isIE() { - - var ua = window.navigator.userAgent; - var msie = ua.indexOf("MSIE "); - - if (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./) || navigator.userAgent.match(/Edge\/\d+/)) { - return true; - } else { - return false; - } - - } - - function activate() { - - // check if browser is Internet Explorere - isIEFlag = isIE(); - - // scrollWidth on element does not work in IE on inputs - // we have to do some dirty dom element copying. - if (isIEFlag === true && domElType === "text") { - setupInternetExplorerElements(); - } - - } - - function setupInternetExplorerElements() { - - if (!wrapper.length) { - wrapper = angular.element('
'); - angular.element('body').append(wrapper); - } - - angular.forEach(['fontFamily', 'fontSize', 'fontWeight', 'fontStyle', - 'letterSpacing', 'textTransform', 'wordSpacing', 'textIndent', - 'boxSizing', 'borderRightWidth', 'borderLeftWidth', 'borderLeftStyle', 'borderRightStyle', - 'paddingLeft', 'paddingRight', 'marginLeft', 'marginRight' - ], function(value) { - mirror.css(value, element.css(value)); - }); - - wrapper.append(mirror); - - } - - function resizeInternetExplorerInput() { - - mirror.text(element.val() || attr.placeholder); - element.css('width', mirror.outerWidth() + 1); - - } - - function resizeInput() { - - if (domEl.scrollWidth !== domEl.clientWidth) { - if (ngModelController.$modelValue) { - element.width(domEl.scrollWidth); - } - } - - if(!ngModelController.$modelValue && attr.placeholder) { - attr.$set('size', attr.placeholder.length); - element.width('auto'); - } - - } - - function resizeTextarea() { - - if(domEl.scrollHeight !== domEl.clientHeight) { - - element.height(domEl.scrollHeight); - - } - - } - - var update = function(force) { - - - if (force === true) { - - if (domElType === "textarea") { - element.height(0); - } else if (domElType === "text") { - element.width(0); - } - - } - - - if (isIEFlag === true && domElType === "text") { - - resizeInternetExplorerInput(); - - } else { - - if (domElType === "textarea") { - - resizeTextarea(); - - } else if (domElType === "text") { - - resizeInput(); - - } - - } - - }; - - activate(); - - //listen for tab changes - if (umbTabsController != null) { - umbTabsController.onTabShown(function(args) { - update(); - }); - } - - // listen for ng-model changes - var unbindModelWatcher = scope.$watch(function() { - return ngModelController.$modelValue; - }, function(newValue) { - $timeout( - function() { - update(true); + if (domEl.scrollWidth !== domEl.clientWidth) { + if (ngModelController.$modelValue) { + element.width(domEl.scrollWidth); + } } - ); - }); - scope.$on('$destroy', function() { - element.off('keyup keydown keypress change', update); - element.off('blur', update(true)); - unbindModelWatcher(); + if (!ngModelController.$modelValue && attr.placeholder) { + attr.$set('size', attr.placeholder.length); + element.width('auto'); + } - // clean up IE dom element - if (isIEFlag === true && domElType === "text") { - mirror.remove(); - } + } - }); - } - }; - }); + function resizeTextarea() { + + if (domEl.scrollHeight !== domEl.clientHeight) { + + element.height(domEl.scrollHeight); + + } + + } + + var update = function (force) { + + + if (force === true) { + + if (domElType === "textarea") { + element.height(0); + } else if (domElType === "text") { + element.width(0); + } + + } + + + if (domElType === "textarea") { + + resizeTextarea(); + + } else if (domElType === "text") { + + resizeInput(); + + } + + }; + + //listen for tab changes + if (umbTabsController != null) { + umbTabsController.onTabShown(function (args) { + update(); + }); + } + + // listen for ng-model changes + var unbindModelWatcher = scope.$watch(function () { + return ngModelController.$modelValue; + }, function (newValue) { + $timeout( + function () { + update(true); + } + ); + }); + + scope.$on('$destroy', function () { + element.off('keyup keydown keypress change', update); + element.off('blur', update(true)); + unbindModelWatcher(); + }); + } + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbrawmodel.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbrawmodel.directive.js index 9b479b60ae..6098cb8a0d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbrawmodel.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbrawmodel.directive.js @@ -9,91 +9,91 @@ will override element type to textarea and add own attribute ngModel tied to jso */ angular.module("umbraco.directives") - .directive('umbRawModel', function () { - return { - restrict: 'A', - require: 'ngModel', - template: '', - replace : true, - scope: { - model: '=umbRawModel', - validateOn:'=' - }, - link: function (scope, element, attrs, ngModelCtrl) { + .directive('umbRawModel', function () { + return { + restrict: 'A', + require: 'ngModel', + template: '', + replace: true, + scope: { + model: '=umbRawModel', + validateOn: '=' + }, + link: function (scope, element, attrs, ngModelCtrl) { - function setEditing (value) { - scope.jsonEditing = angular.copy( jsonToString(value)); - } + function setEditing(value) { + scope.jsonEditing = Utilities.copy(jsonToString(value)); + } - function updateModel (value) { - scope.model = stringToJson(value); - } + function updateModel(value) { + scope.model = stringToJson(value); + } - function setValid() { - ngModelCtrl.$setValidity('json', true); - } + function setValid() { + ngModelCtrl.$setValidity('json', true); + } - function setInvalid () { - ngModelCtrl.$setValidity('json', false); - } + function setInvalid() { + ngModelCtrl.$setValidity('json', false); + } - function stringToJson(text) { - try { - return angular.fromJson(text); - } catch (err) { - setInvalid(); - return text; - } - } + function stringToJson(text) { + try { + return Utilities.fromJson(text); + } catch (err) { + setInvalid(); + return text; + } + } - function jsonToString(object) { - // better than JSON.stringify(), because it formats + filters $$hashKey etc. - // NOTE that this will remove all $-prefixed values - return angular.toJson(object, true); - } + function jsonToString(object) { + // better than JSON.stringify(), because it formats + filters $$hashKey etc. + // NOTE that this will remove all $-prefixed values + return Utilities.toJson(object, true); + } - function isValidJson(model) { - var flag = true; - try { - angular.fromJson(model); - } catch (err) { - flag = false; - } - return flag; - } + function isValidJson(model) { + var flag = true; + try { + Utilities.fromJson(model) + } catch (err) { + flag = false; + } + return flag; + } - //init - setEditing(scope.model); + //init + setEditing(scope.model); - var onInputChange = function(newval,oldval){ - if (newval !== oldval) { - if (isValidJson(newval)) { - setValid(); - updateModel(newval); - } else { - setInvalid(); - } - } - }; + var onInputChange = function (newval, oldval) { + if (newval !== oldval) { + if (isValidJson(newval)) { + setValid(); + updateModel(newval); + } else { + setInvalid(); + } + } + }; - if(scope.validateOn){ - element.on(scope.validateOn, function(){ - scope.$apply(function(){ - onInputChange(scope.jsonEditing); - }); - }); - }else{ - //check for changes going out - scope.$watch('jsonEditing', onInputChange, true); - } + if (scope.validateOn) { + element.on(scope.validateOn, function () { + scope.$apply(function () { + onInputChange(scope.jsonEditing); + }); + }); + } else { + //check for changes going out + scope.$watch('jsonEditing', onInputChange, true); + } - //check for changes coming in - scope.$watch('model', function (newval, oldval) { - if (newval !== oldval) { - setEditing(newval); - } - }, true); + //check for changes coming in + scope.$watch('model', function (newval, oldval) { + if (newval !== oldval) { + setEditing(newval); + } + }, true); - } - }; - }); + } + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index f606c0539a..3578624b50 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -29,7 +29,7 @@ angular.module("umbraco.directives") } var editorConfig = scope.configuration ? scope.configuration : null; - if (!editorConfig || angular.isString(editorConfig)) { + if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); //for the grid by default, we don't want to include the macro toolbar editorConfig.toolbar = _.without(editorConfig, "umbmacro"); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js index df3770056e..e957d78660 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js @@ -30,7 +30,7 @@ angular.module("umbraco.directives") .directive('localize', function ($log, localizationService) { return { restrict: 'E', - scope:{ + scope: { key: '@', tokens: '=', watchTokens: '@' @@ -40,13 +40,13 @@ angular.module("umbraco.directives") link: function (scope, element, attrs) { var key = scope.key; scope.text = ""; - + // A render function to be able to update tokens as values update. function render() { element.html(localizationService.tokenReplace(scope.text, scope.tokens || null)); } - - localizationService.localize(key).then(function(value){ + + localizationService.localize(key).then(function (value) { scope.text = value; render(); }); @@ -64,19 +64,19 @@ angular.module("umbraco.directives") //Support one or more attribute properties to update var keys = attrs.localize.split(','); - angular.forEach(keys, function(value, key){ + Utilities.forEach(keys, (value, key) => { var attr = element.attr(value); - if(attr){ - if(attr[0] === '@'){ + if (attr) { + if (attr[0] === '@') { //If the translation key starts with @ then remove it attr = attr.substring(1); } var t = localizationService.tokenize(attr, scope); - - localizationService.localize(t.key, t.tokens).then(function(val){ - element.attr(value, val); + + localizationService.localize(t.key, t.tokens).then(function (val) { + element.attr(value, val); }); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index fa1f4227a2..ad396e7a9a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -193,352 +193,351 @@ Opens an overlay to show a custom YSOD.
@param {string} position The overlay position ("left", "right", "center": "target"). **/ -(function() { - 'use strict'; +(function () { + 'use strict'; function OverlayDirective($timeout, formHelper, overlayHelper, localizationService, $q, $templateCache, $http, $compile) { - function link(scope, el, attr, ctrl) { + function link(scope, el, attr, ctrl) { - scope.directive = { - enableConfirmButton: false - }; - - var overlayNumber = 0; - var numberOfOverlays = 0; - var isRegistered = false; - - - var modelCopy = {}; - var unsubscribe = []; - - function activate() { - setView(); - - setButtonText(); - - modelCopy = makeModelCopy(scope.model); - - $timeout(function() { - - if (scope.position === "target" && scope.model.event) { - setTargetPosition(); - - // update the position of the overlay on content changes - // as these affect the layout/size of the overlay - if ('ResizeObserver' in window) - { - var resizeObserver = new ResizeObserver(setTargetPosition); - var contentArea = document.getElementById("contentwrapper"); - resizeObserver.observe(el[0]); - if (contentArea) { - resizeObserver.observe(contentArea); - } - unsubscribe.push(function () { - resizeObserver.disconnect(); - }); - } - } - - // this has to be done inside a timeout to ensure the destroy - // event on other overlays is run before registering a new one - registerOverlay(); - - setOverlayIndent(); - - focusOnOverlayHeading() - }); - - } - - // Ideally this would focus on the first natively focusable element in the overlay, but as the content can be dynamic, it is focusing on the heading. - function focusOnOverlayHeading() { - var heading = el.find(".umb-overlay__title"); - - if(heading) { - heading.focus(); - } - } - - function setView() { - - if (scope.view) { - - if (scope.view.indexOf(".html") === -1) { - var viewAlias = scope.view.toLowerCase(); - scope.view = "views/common/overlays/" + viewAlias + "/" + viewAlias + ".html"; - } - - //if a custom parent scope is defined then we need to manually compile the view - if (scope.parentScope) { - var element = el.find(".scoped-view"); - $http.get(scope.view, { cache: $templateCache }) - .then(function (response) { - var templateScope = scope.parentScope.$new(); - unsubscribe.push(function() { - templateScope.$destroy(); - }); - templateScope.model = scope.model; - element.html(response.data); - element.show(); - $compile(element.contents())(templateScope); - }); - } - } - - } - - function setButtonText() { - - var labelKeys = [ - "general_close", - "general_submit" - ]; - - localizationService.localizeMany(labelKeys).then(function (values) { - if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) { - scope.model.closeButtonLabel = values[0]; - } - if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) { - scope.model.submitButtonLabel = values[1]; - } - }); - } - - function registerOverlay() { - - overlayNumber = overlayHelper.registerOverlay(); - - $(document).on("keydown.overlay-" + overlayNumber, function(event) { - - if (event.which === 27) { - - numberOfOverlays = overlayHelper.getNumberOfOverlays(); - - if (numberOfOverlays === overlayNumber && !scope.model.disableEscKey) { - scope.$apply(function () { - scope.closeOverLay(); - }); - } - - event.stopPropagation(); - event.preventDefault(); - } - - if (event.which === 13) { - - numberOfOverlays = overlayHelper.getNumberOfOverlays(); - - if(numberOfOverlays === overlayNumber) { - - var activeElementType = document.activeElement.tagName; - var clickableElements = ["A", "BUTTON"]; - var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter"); - var submitOnEnterValue = submitOnEnter ? document.activeElement.getAttribute("overlay-submit-on-enter") : ""; - - if(clickableElements.indexOf(activeElementType) >= 0) { - // don't do anything, let the browser Enter key handle this - } else if(activeElementType === "TEXTAREA" && !submitOnEnter) { - - - } else if (submitOnEnter && submitOnEnterValue === "false") { - // don't do anything - }else { - scope.$apply(function () { - scope.submitForm(scope.model); - }); - event.preventDefault(); - } - - } - - } - - }); - - isRegistered = true; - - } - - function unregisterOverlay() { - - if(isRegistered) { - - overlayHelper.unregisterOverlay(); - - $(document).off("keydown.overlay-" + overlayNumber); - - isRegistered = false; - } - - } - - function makeModelCopy(object) { - - var newObject = {}; - - for (var key in object) { - if (key !== "event" && key !== "parentScope") { - newObject[key] = angular.copy(object[key]); - } - } - - return newObject; - - } - - function setOverlayIndent() { - - var overlayIndex = overlayNumber - 1; - var indentSize = overlayIndex * 20; - var overlayWidth = el[0].clientWidth; - - el.css('width', overlayWidth - indentSize); - - if(scope.position === "center" && overlayIndex > 0 || scope.position === "target" && overlayIndex > 0) { - var overlayTopPosition = el[0].offsetTop; - el.css('top', overlayTopPosition + indentSize); - } - - } - - function setTargetPosition() { - - var container = $("#contentwrapper"); - var containerLeft = container[0].offsetLeft; - var containerRight = containerLeft + container[0].offsetWidth; - var containerTop = container[0].offsetTop; - var containerBottom = containerTop + container[0].offsetHeight; - - var mousePositionClickX = null; - var mousePositionClickY = null; - var elementHeight = null; - var elementWidth = null; - - var position = { - right: "inherit", - left: "inherit", - top: "inherit", - bottom: "inherit" + scope.directive = { + enableConfirmButton: false }; - // click position - mousePositionClickX = scope.model.event.pageX; - mousePositionClickY = scope.model.event.pageY; + var overlayNumber = 0; + var numberOfOverlays = 0; + var isRegistered = false; - // element size - elementHeight = el[0].clientHeight; - elementWidth = el[0].clientWidth; - // move element to this position - position.left = mousePositionClickX - (elementWidth / 2); - position.top = mousePositionClickY - (elementHeight / 2); + var modelCopy = {}; + var unsubscribe = []; + + function activate() { + setView(); + + setButtonText(); + + modelCopy = makeModelCopy(scope.model); + + $timeout(function () { + + if (scope.position === "target" && scope.model.event) { + setTargetPosition(); + + // update the position of the overlay on content changes + // as these affect the layout/size of the overlay + if ('ResizeObserver' in window) { + var resizeObserver = new ResizeObserver(setTargetPosition); + var contentArea = document.getElementById("contentwrapper"); + resizeObserver.observe(el[0]); + if (contentArea) { + resizeObserver.observe(contentArea); + } + unsubscribe.push(function () { + resizeObserver.disconnect(); + }); + } + } + + // this has to be done inside a timeout to ensure the destroy + // event on other overlays is run before registering a new one + registerOverlay(); + + setOverlayIndent(); + + focusOnOverlayHeading() + }); - // check to see if element is outside screen - // outside right - if (position.left + elementWidth > containerRight) { - position.right = 10; - position.left = "inherit"; } - // outside bottom - if (position.top + elementHeight > containerBottom) { - position.bottom = 10; - position.top = "inherit"; + // Ideally this would focus on the first natively focusable element in the overlay, but as the content can be dynamic, it is focusing on the heading. + function focusOnOverlayHeading() { + var heading = el.find(".umb-overlay__title"); + + if (heading) { + heading.focus(); + } } - // outside left - if (position.left < containerLeft) { - position.left = containerLeft + 10; - position.right = "inherit"; + function setView() { + + if (scope.view) { + + if (scope.view.indexOf(".html") === -1) { + var viewAlias = scope.view.toLowerCase(); + scope.view = "views/common/overlays/" + viewAlias + "/" + viewAlias + ".html"; + } + + //if a custom parent scope is defined then we need to manually compile the view + if (scope.parentScope) { + var element = el.find(".scoped-view"); + $http.get(scope.view, { cache: $templateCache }) + .then(function (response) { + var templateScope = scope.parentScope.$new(); + unsubscribe.push(function () { + templateScope.$destroy(); + }); + templateScope.model = scope.model; + element.html(response.data); + element.show(); + $compile(element.contents())(templateScope); + }); + } + } + } - // outside top - if (position.top < containerTop) { - position.top = 10; - position.bottom = "inherit"; + function setButtonText() { + + var labelKeys = [ + "general_close", + "general_submit" + ]; + + localizationService.localizeMany(labelKeys).then(function (values) { + if (!scope.model.closeButtonLabelKey && !scope.model.closeButtonLabel) { + scope.model.closeButtonLabel = values[0]; + } + if (!scope.model.submitButtonLabelKey && !scope.model.submitButtonLabel) { + scope.model.submitButtonLabel = values[1]; + } + }); } - el.css(position); - } + function registerOverlay() { - scope.submitForm = function(model) { - if(scope.model.submit) { - if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation})) { - - if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { - //wrap in a when since we don't know if this is a promise or not - $q.when(scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton)).then( - function() { - formHelper.resetForm({ scope: scope }); - }); - } else { - unregisterOverlay(); - //wrap in a when since we don't know if this is a promise or not - $q.when(scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton)).then( - function() { - formHelper.resetForm({ scope: scope }); - }); - } + overlayNumber = overlayHelper.registerOverlay(); - } - } - }; + $(document).on("keydown.overlay-" + overlayNumber, function (event) { - scope.cancelConfirmSubmit = function() { - scope.model.confirmSubmit.show = false; - }; + if (event.which === 27) { - scope.closeOverLay = function() { + numberOfOverlays = overlayHelper.getNumberOfOverlays(); - unregisterOverlay(); + if (numberOfOverlays === overlayNumber && !scope.model.disableEscKey) { + scope.$apply(function () { + scope.closeOverLay(); + }); + } + + event.stopPropagation(); + event.preventDefault(); + } + + if (event.which === 13) { + + numberOfOverlays = overlayHelper.getNumberOfOverlays(); + + if (numberOfOverlays === overlayNumber) { + + var activeElementType = document.activeElement.tagName; + var clickableElements = ["A", "BUTTON"]; + var submitOnEnter = document.activeElement.hasAttribute("overlay-submit-on-enter"); + var submitOnEnterValue = submitOnEnter ? document.activeElement.getAttribute("overlay-submit-on-enter") : ""; + + if (clickableElements.indexOf(activeElementType) >= 0) { + // don't do anything, let the browser Enter key handle this + } else if (activeElementType === "TEXTAREA" && !submitOnEnter) { + + + } else if (submitOnEnter && submitOnEnterValue === "false") { + // don't do anything + } else { + scope.$apply(function () { + scope.submitForm(scope.model); + }); + event.preventDefault(); + } + + } + + } + + }); + + isRegistered = true; - if (scope.model && scope.model.close) { - scope.model = modelCopy; - scope.model.close(scope.model); - } else { - scope.model.show = false; - scope.model = null; } - }; + function unregisterOverlay() { + + if (isRegistered) { + + overlayHelper.unregisterOverlay(); + + $(document).off("keydown.overlay-" + overlayNumber); + + isRegistered = false; + } - scope.outSideClick = function() { - if(!scope.model.disableBackdropClick) { - scope.closeOverLay(); } + + function makeModelCopy(object) { + + var newObject = {}; + + for (var key in object) { + if (key !== "event" && key !== "parentScope") { + newObject[key] = Utilities.copy(object[key]); + } + } + + return newObject; + + } + + function setOverlayIndent() { + + var overlayIndex = overlayNumber - 1; + var indentSize = overlayIndex * 20; + var overlayWidth = el[0].clientWidth; + + el.css('width', overlayWidth - indentSize); + + if (scope.position === "center" && overlayIndex > 0 || scope.position === "target" && overlayIndex > 0) { + var overlayTopPosition = el[0].offsetTop; + el.css('top', overlayTopPosition + indentSize); + } + + } + + function setTargetPosition() { + + var container = $("#contentwrapper"); + var containerLeft = container[0].offsetLeft; + var containerRight = containerLeft + container[0].offsetWidth; + var containerTop = container[0].offsetTop; + var containerBottom = containerTop + container[0].offsetHeight; + + var mousePositionClickX = null; + var mousePositionClickY = null; + var elementHeight = null; + var elementWidth = null; + + var position = { + right: "inherit", + left: "inherit", + top: "inherit", + bottom: "inherit" + }; + + // click position + mousePositionClickX = scope.model.event.pageX; + mousePositionClickY = scope.model.event.pageY; + + // element size + elementHeight = el[0].clientHeight; + elementWidth = el[0].clientWidth; + + // move element to this position + position.left = mousePositionClickX - (elementWidth / 2); + position.top = mousePositionClickY - (elementHeight / 2); + + // check to see if element is outside screen + // outside right + if (position.left + elementWidth > containerRight) { + position.right = 10; + position.left = "inherit"; + } + + // outside bottom + if (position.top + elementHeight > containerBottom) { + position.bottom = 10; + position.top = "inherit"; + } + + // outside left + if (position.left < containerLeft) { + position.left = containerLeft + 10; + position.right = "inherit"; + } + + // outside top + if (position.top < containerTop) { + position.top = 10; + position.bottom = "inherit"; + } + + el.css(position); + } + + scope.submitForm = function (model) { + if (scope.model.submit) { + if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation })) { + + if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { + //wrap in a when since we don't know if this is a promise or not + $q.when(scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton)).then( + function () { + formHelper.resetForm({ scope: scope }); + }); + } else { + unregisterOverlay(); + //wrap in a when since we don't know if this is a promise or not + $q.when(scope.model.submit(model, modelCopy, scope.directive.enableConfirmButton)).then( + function () { + formHelper.resetForm({ scope: scope }); + }); + } + + } + } + }; + + scope.cancelConfirmSubmit = function () { + scope.model.confirmSubmit.show = false; + }; + + scope.closeOverLay = function () { + + unregisterOverlay(); + + if (scope.model && scope.model.close) { + scope.model = modelCopy; + scope.model.close(scope.model); + } else { + scope.model.show = false; + scope.model = null; + } + + }; + + scope.outSideClick = function () { + if (!scope.model.disableBackdropClick) { + scope.closeOverLay(); + } + }; + + unsubscribe.push(unregisterOverlay); + scope.$on('$destroy', function () { + for (var i = 0; i < unsubscribe.length; i++) { + unsubscribe[i](); + } + }); + + activate(); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/overlays/umb-overlay.html', + scope: { + ngShow: "=", + model: "=", + view: "=", + position: "@", + size: "=?", + parentScope: "=?" + }, + link: link }; - unsubscribe.push(unregisterOverlay); - scope.$on('$destroy', function () { - for (var i = 0; i < unsubscribe.length; i++) { - unsubscribe[i](); - } - }); + return directive; + } - activate(); - - } - - var directive = { - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/overlays/umb-overlay.html', - scope: { - ngShow: "=", - model: "=", - view: "=", - position: "@", - size: "=?", - parentScope: "=?" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective); + angular.module('umbraco.directives').directive('umbOverlay', OverlayDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js index 7ff7f7fa66..1308515e8e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tags/umbtagseditor.directive.js @@ -159,7 +159,7 @@ function configureViewModel(isInitLoad) { if (vm.value) { - if (angular.isString(vm.value) && vm.value.length > 0) { + if (Utilities.isString(vm.value) && vm.value.length > 0) { if (vm.config.storageType === "Json") { //json storage vm.viewModel = JSON.parse(vm.value); @@ -191,7 +191,7 @@ } } } - else if (angular.isArray(vm.value)) { + else if (Utilities.isArray(vm.value)) { vm.viewModel = vm.value; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 3d743c7e9a..5a7da80eff 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -87,7 +87,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use /** Helper function to emit tree events */ function emitEvent(eventName, args) { - if (registeredCallbacks[eventName] && angular.isArray(registeredCallbacks[eventName])) { + if (registeredCallbacks[eventName] && Utilities.isArray(registeredCallbacks[eventName])) { _.each(registeredCallbacks[eventName], function (c) { c(args);//call it }); @@ -100,7 +100,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use * @param {any} args either a string representing the 'section' or an object containing: 'section', 'treeAlias', 'customTreeParams', 'cacheKey' */ function load(args) { - if (angular.isString(args)) { + if (Utilities.isString(args)) { $scope.section = args; } else if (args) { @@ -147,7 +147,7 @@ function umbTreeDirective($q, $rootScope, treeService, notificationsService, use throw "args.path cannot be null"; } - if (angular.isString(args.path)) { + if (Utilities.isString(args.path)) { args.path = args.path.replace('"', '').split(','); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js index cd1b1d8181..10fc44c2c6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbaceeditor.directive.js @@ -24,40 +24,40 @@ // sets the ace worker path, if running from concatenated // or minified source - if (angular.isDefined(opts.workerPath)) { + if (Utilities.isDefined(opts.workerPath)) { var config = window.ace.require('ace/config'); config.set('workerPath', opts.workerPath); } // ace requires loading - if (angular.isDefined(opts.require)) { + if (Utilities.isDefined(opts.require)) { opts.require.forEach(function(n) { window.ace.require(n); }); } // Boolean options - if (angular.isDefined(opts.showGutter)) { + if (Utilities.isDefined(opts.showGutter)) { acee.renderer.setShowGutter(opts.showGutter); } - if (angular.isDefined(opts.useWrapMode)) { + if (Utilities.isDefined(opts.useWrapMode)) { session.setUseWrapMode(opts.useWrapMode); } - if (angular.isDefined(opts.showInvisibles)) { + if (Utilities.isDefined(opts.showInvisibles)) { acee.renderer.setShowInvisibles(opts.showInvisibles); } - if (angular.isDefined(opts.showIndentGuides)) { + if (Utilities.isDefined(opts.showIndentGuides)) { acee.renderer.setDisplayIndentGuides(opts.showIndentGuides); } - if (angular.isDefined(opts.useSoftTabs)) { + if (Utilities.isDefined(opts.useSoftTabs)) { session.setUseSoftTabs(opts.useSoftTabs); } - if (angular.isDefined(opts.showPrintMargin)) { + if (Utilities.isDefined(opts.showPrintMargin)) { acee.setShowPrintMargin(opts.showPrintMargin); } // commands - if (angular.isDefined(opts.disableSearch) && opts.disableSearch) { + if (Utilities.isDefined(opts.disableSearch) && opts.disableSearch) { acee.commands.addCommands([{ name: 'unfind', bindKey: { @@ -72,15 +72,15 @@ } // Basic options - if (angular.isString(opts.theme)) { + if (Utilities.isString(opts.theme)) { acee.setTheme('ace/theme/' + opts.theme); } - if (angular.isString(opts.mode)) { + if (Utilities.isString(opts.mode)) { session.setMode('ace/mode/' + opts.mode); } // Advanced options - if (angular.isDefined(opts.firstLineNumber)) { - if (angular.isNumber(opts.firstLineNumber)) { + if (Utilities.isDefined(opts.firstLineNumber)) { + if (Utilities.isNumber(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber); } else if (angular.isFunction(opts.firstLineNumber)) { session.setOption('firstLineNumber', opts.firstLineNumber()); @@ -89,7 +89,7 @@ // advanced options var key, obj; - if (angular.isDefined(opts.advanced)) { + if (Utilities.isDefined(opts.advanced)) { for (key in opts.advanced) { // create a javascript object with the key and value obj = { @@ -102,7 +102,7 @@ } // advanced options for the renderer - if (angular.isDefined(opts.rendererOptions)) { + if (Utilities.isDefined(opts.rendererOptions)) { for (key in opts.rendererOptions) { // create a javascript object with the key and value obj = { @@ -206,7 +206,7 @@ */ var args = Array.prototype.slice.call(arguments, 1); - if (angular.isDefined(callback)) { + if (Utilities.isDefined(callback)) { scope.$evalAsync(function() { if (angular.isFunction(callback)) { callback(args); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js index 5433f73fa6..0498b81963 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js @@ -67,37 +67,37 @@ Use this directive to render a date time picker @param {callback} onDayCreate (callback): Take full control of every date cell with theonDayCreate()hook. **/ -(function() { - 'use strict'; +(function () { + 'use strict'; - var umbDateTimePicker = { - template: '' + - '' + - '
' + - '
', - controller: umbDateTimePickerCtrl, - transclude: true, - bindings: { - ngModel: '<', - options: '<', - onSetup: '&?', - onChange: '&?', - onOpen: '&?', - onClose: '&?', - onMonthChange: '&?', - onYearChange: '&?', - onReady: '&?', - onValueUpdate: '&?', - onDayCreate: '&?' - } + var umbDateTimePicker = { + template: '' + + '' + + '
' + + '
', + controller: umbDateTimePickerCtrl, + transclude: true, + bindings: { + ngModel: '<', + options: '<', + onSetup: '&?', + onChange: '&?', + onOpen: '&?', + onClose: '&?', + onMonthChange: '&?', + onYearChange: '&?', + onReady: '&?', + onValueUpdate: '&?', + onDayCreate: '&?' + } }; - + function umbDateTimePickerCtrl($element, $timeout, $scope, assetsService, userService) { var ctrl = this; var userLocale = null; - ctrl.$onInit = function() { + ctrl.$onInit = function () { // load css file for the date picker assetsService.loadCss('lib/flatpickr/flatpickr.css', $scope).then(function () { @@ -113,27 +113,27 @@ Use this directive to render a date time picker }); }); - }; + }; - function grabElementAndRunFlatpickr() { - $timeout(function() { - var transcludeEl = $element.find('ng-transclude')[0]; - var element = transcludeEl.children[0]; + function grabElementAndRunFlatpickr() { + $timeout(function () { + var transcludeEl = $element.find('ng-transclude')[0]; + var element = transcludeEl.children[0]; - setDatepicker(element); - }, 0, true); - } + setDatepicker(element); + }, 0, true); + } - function setDatepicker(element) { - var fpLib = flatpickr ? flatpickr : FlatpickrInstance; + function setDatepicker(element) { + var fpLib = flatpickr ? flatpickr : FlatpickrInstance; - if (!fpLib) { - return console.warn('Unable to find any flatpickr installation'); - } + if (!fpLib) { + return console.warn('Unable to find any flatpickr installation'); + } var fpInstance; - setUpCallbacks(); + setUpCallbacks(); if (!ctrl.options.locale) { ctrl.options.locale = userLocale; @@ -149,101 +149,101 @@ Use this directive to render a date time picker }; fpInstance = new fpLib(element, ctrl.options); - - if (ctrl.onSetup) { - ctrl.onSetup({ - fpItem: fpInstance - }); - } - // If has ngModel set the date - if (ctrl.ngModel) { - fpInstance.setDate(ctrl.ngModel); - } + if (ctrl.onSetup) { + ctrl.onSetup({ + fpItem: fpInstance + }); + } - // destroy the flatpickr instance when the dom element is removed - angular.element(element).on('$destroy', function() { - fpInstance.destroy(); - }); + // If has ngModel set the date + if (ctrl.ngModel) { + fpInstance.setDate(ctrl.ngModel); + } - // Refresh the scope - $scope.$applyAsync(); - } + // destroy the flatpickr instance when the dom element is removed + $(element).on('$destroy', function () { + fpInstance.destroy(); + }); - function setUpCallbacks() { - // bind hook for onChange - if(ctrl.options && ctrl.onChange) { - ctrl.options.onChange = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onChange({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + // Refresh the scope + $scope.$applyAsync(); + } - // bind hook for onOpen - if(ctrl.options && ctrl.onOpen) { - ctrl.options.onOpen = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onOpen({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + function setUpCallbacks() { + // bind hook for onChange + if (ctrl.options && ctrl.onChange) { + ctrl.options.onChange = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onChange({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } - // bind hook for onOpen - if(ctrl.options && ctrl.onClose) { - ctrl.options.onClose = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onClose({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + // bind hook for onOpen + if (ctrl.options && ctrl.onOpen) { + ctrl.options.onOpen = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onOpen({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } - // bind hook for onMonthChange - if(ctrl.options && ctrl.onMonthChange) { - ctrl.options.onMonthChange = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onMonthChange({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + // bind hook for onOpen + if (ctrl.options && ctrl.onClose) { + ctrl.options.onClose = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onClose({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } - // bind hook for onYearChange - if(ctrl.options && ctrl.onYearChange) { - ctrl.options.onYearChange = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onYearChange({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + // bind hook for onMonthChange + if (ctrl.options && ctrl.onMonthChange) { + ctrl.options.onMonthChange = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onMonthChange({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } - // bind hook for onReady - if(ctrl.options && ctrl.onReady) { - ctrl.options.onReady = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onReady({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + // bind hook for onYearChange + if (ctrl.options && ctrl.onYearChange) { + ctrl.options.onYearChange = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onYearChange({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } - // bind hook for onValueUpdate - if(ctrl.onValueUpdate) { - ctrl.options.onValueUpdate = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onValueUpdate({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + // bind hook for onReady + if (ctrl.options && ctrl.onReady) { + ctrl.options.onReady = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onReady({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } - // bind hook for onDayCreate - if(ctrl.onDayCreate) { - ctrl.options.onDayCreate = function(selectedDates, dateStr, instance) { - $timeout(function() { - ctrl.onDayCreate({selectedDates: selectedDates, dateStr: dateStr, instance: instance}); - }); - }; - } + // bind hook for onValueUpdate + if (ctrl.onValueUpdate) { + ctrl.options.onValueUpdate = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onValueUpdate({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } - } + // bind hook for onDayCreate + if (ctrl.onDayCreate) { + ctrl.options.onDayCreate = function (selectedDates, dateStr, instance) { + $timeout(function () { + ctrl.onDayCreate({ selectedDates: selectedDates, dateStr: dateStr, instance: instance }); + }); + }; + } + + } } // umbFlatpickr (umb-flatpickr) is deprecated, but we keep it for backwards compatibility diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index a9b9cc52b1..3f53a1e18c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -1,738 +1,740 @@ -(function() { - 'use strict'; +(function () { + 'use strict'; - function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, - dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, - localizationService, editorService, eventsService, overlayService) { - - function link(scope, el, attr, ctrl) { - - var eventBindings = []; - var validationTranslated = ""; - var tabNoSortOrderTranslated = ""; + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, + dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, + localizationService, editorService, eventsService, overlayService) { - scope.dataTypeHasChanged = false; - scope.sortingMode = false; - scope.toolbar = []; - scope.sortableOptionsGroup = {}; - scope.sortableOptionsProperty = {}; - scope.sortingButtonKey = "general_reorder"; - scope.compositionsButtonState = "init"; + function link(scope, el, attr, ctrl) { - function activate() { + var eventBindings = []; + var validationTranslated = ""; + var tabNoSortOrderTranslated = ""; - setSortingOptions(); + scope.dataTypeHasChanged = false; + scope.sortingMode = false; + scope.toolbar = []; + scope.sortableOptionsGroup = {}; + scope.sortableOptionsProperty = {}; + scope.sortingButtonKey = "general_reorder"; + scope.compositionsButtonState = "init"; - // set placeholder property on each group - if (scope.model.groups.length !== 0) { - angular.forEach(scope.model.groups, function(group) { - addInitProperty(group); - }); - } + function activate() { - // add init tab - addInitGroup(scope.model.groups); + setSortingOptions(); - activateFirstGroup(scope.model.groups); - - // localize texts - localizationService.localize("validation_validation").then(function(value) { - validationTranslated = value; - }); - - localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function(value) { - tabNoSortOrderTranslated = value; - }); - } - - function setSortingOptions() { - - scope.sortableOptionsGroup = { - axis: 'y', - distance: 10, - tolerance: "pointer", - opacity: 0.7, - scroll: true, - cursor: "move", - placeholder: "umb-group-builder__group-sortable-placeholder", - zIndex: 6000, - handle: ".umb-group-builder__group-handle", - items: ".umb-group-builder__group-sortable", - start: function(e, ui) { - ui.placeholder.height(ui.item.height()); - }, - stop: function(e, ui) { - updateTabsSortOrder(); - } - }; - - scope.sortableOptionsProperty = { - axis: 'y', - distance: 10, - tolerance: "pointer", - connectWith: ".umb-group-builder__properties", - opacity: 0.7, - scroll: true, - cursor: "move", - placeholder: "umb-group-builder__property_sortable-placeholder", - zIndex: 6000, - handle: ".umb-group-builder__property-handle", - items: ".umb-group-builder__property-sortable", - start: function(e, ui) { - ui.placeholder.height(ui.item.height()); - }, - stop: function(e, ui) { - updatePropertiesSortOrder(); - } - }; - - } - - function updateTabsSortOrder() { - - var first = true; - var prevSortOrder = 0; - - scope.model.groups.map(function(group){ - - var index = scope.model.groups.indexOf(group); - - if(group.tabState !== "init") { - - // set the first not inherited tab to sort order 0 - if(!group.inherited && first) { - - // set the first tab sort order to 0 if prev is 0 - if( prevSortOrder === 0 ) { - group.sortOrder = 0; - // when the first tab is inherited and sort order is not 0 - } else { - group.sortOrder = prevSortOrder + 1; - } - - first = false; - - } else if(!group.inherited && !first) { - - // find next group - var nextGroup = scope.model.groups[index + 1]; - - // if a groups is dropped in the middle of to groups with - // same sort order. Give it the dropped group same sort order - if( prevSortOrder === nextGroup.sortOrder ) { - group.sortOrder = prevSortOrder; - } else { - group.sortOrder = prevSortOrder + 1; + // set placeholder property on each group + if (scope.model.groups.length !== 0) { + angular.forEach(scope.model.groups, function (group) { + addInitProperty(group); + }); } + // add init tab + addInitGroup(scope.model.groups); + + activateFirstGroup(scope.model.groups); + + // localize texts + localizationService.localize("validation_validation").then(function (value) { + validationTranslated = value; + }); + + localizationService.localize("contentTypeEditor_tabHasNoSortOrder").then(function (value) { + tabNoSortOrderTranslated = value; + }); + } + + function setSortingOptions() { + + scope.sortableOptionsGroup = { + axis: 'y', + distance: 10, + tolerance: "pointer", + opacity: 0.7, + scroll: true, + cursor: "move", + placeholder: "umb-group-builder__group-sortable-placeholder", + zIndex: 6000, + handle: ".umb-group-builder__group-handle", + items: ".umb-group-builder__group-sortable", + start: function (e, ui) { + ui.placeholder.height(ui.item.height()); + }, + stop: function (e, ui) { + updateTabsSortOrder(); + } + }; + + scope.sortableOptionsProperty = { + axis: 'y', + distance: 10, + tolerance: "pointer", + connectWith: ".umb-group-builder__properties", + opacity: 0.7, + scroll: true, + cursor: "move", + placeholder: "umb-group-builder__property_sortable-placeholder", + zIndex: 6000, + handle: ".umb-group-builder__property-handle", + items: ".umb-group-builder__property-sortable", + start: function (e, ui) { + ui.placeholder.height(ui.item.height()); + }, + stop: function (e, ui) { + updatePropertiesSortOrder(); + } + }; + } - // store this tabs sort order as reference for the next - prevSortOrder = group.sortOrder; + function updateTabsSortOrder() { - } + var first = true; + var prevSortOrder = 0; - }); + scope.model.groups.map(function (group) { - } + var index = scope.model.groups.indexOf(group); - function filterAvailableCompositions(selectedContentType, selecting) { + if (group.tabState !== "init") { - //selecting = true if the user has check the item, false if the user has unchecked the item + // set the first not inherited tab to sort order 0 + if (!group.inherited && first) { + + // set the first tab sort order to 0 if prev is 0 + if (prevSortOrder === 0) { + group.sortOrder = 0; + // when the first tab is inherited and sort order is not 0 + } else { + group.sortOrder = prevSortOrder + 1; + } + + first = false; + + } else if (!group.inherited && !first) { + + // find next group + var nextGroup = scope.model.groups[index + 1]; + + // if a groups is dropped in the middle of to groups with + // same sort order. Give it the dropped group same sort order + if (prevSortOrder === nextGroup.sortOrder) { + group.sortOrder = prevSortOrder; + } else { + group.sortOrder = prevSortOrder + 1; + } + + } + + // store this tabs sort order as reference for the next + prevSortOrder = group.sortOrder; + + } - var selectedContentTypeAliases = selecting ? - //the user has selected the item so add to the current list - _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : - //the user has unselected the item so remove from the current list - _.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) { - return i === selectedContentType.alias; }); - //get the currently assigned property type aliases - ensure we pass these to the server side filer - var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })), function (f) { - return f !== null && f !== undefined; - }); + } - //use a different resource lookup depending on the content type type - var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + function filterAvailableCompositions(selectedContentType, selecting) { - return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { - _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { - //reset first - current.allowed = true; - //see if this list item is found in the response (allowed) list - var found = _.find(filteredAvailableCompositeTypes, function (f) { - return current.contentType.alias === f.contentType.alias; + //selecting = true if the user has check the item, false if the user has unchecked the item + + var selectedContentTypeAliases = selecting ? + //the user has selected the item so add to the current list + _.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) : + //the user has unselected the item so remove from the current list + _.reject(scope.compositionsDialogModel.compositeContentTypes, function (i) { + return i === selectedContentType.alias; }); - //allow if the item was found in the response (allowed) list - - // and ensure its set to allowed if it is currently checked, - // DO not allow if it's a locked content type. - current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && - (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false); - + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function (g) { + return _.map(g.properties, function (p) { + return p.alias; + }); + })), function (f) { + return f !== null && f !== undefined; }); - }); - } - function updatePropertiesSortOrder() { + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; - angular.forEach(scope.model.groups, function(group){ - if( group.tabState !== "init" ) { - group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties); - } - }); + return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) { + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) { + //reset first + current.allowed = true; + //see if this list item is found in the response (allowed) list + var found = _.find(filteredAvailableCompositeTypes, function (f) { + return current.contentType.alias === f.contentType.alias; + }); - } + //allow if the item was found in the response (allowed) list - + // and ensure its set to allowed if it is currently checked, + // DO not allow if it's a locked content type. + current.allowed = scope.model.lockedCompositeContentTypes.indexOf(current.contentType.alias) === -1 && + (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) || ((found !== null && found !== undefined) ? found.allowed : false); + + }); + }); + } + + function updatePropertiesSortOrder() { + + angular.forEach(scope.model.groups, function (group) { + if (group.tabState !== "init") { + group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties); + } + }); + + } + + function setupAvailableContentTypesModel(result) { + scope.compositionsDialogModel.availableCompositeContentTypes = result; + //iterate each one and set it up + _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { + //enable it if it's part of the selected model + if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) { + c.allowed = true; + } + + //set the inherited flags + c.inherited = false; + if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { + c.inherited = true; + } + // convert icons for composite content types + iconHelper.formatContentTypeIcons([c.contentType]); + }); + } + + /* ---------- DELETE PROMT ---------- */ + + scope.togglePrompt = function (object) { + object.deletePrompt = !object.deletePrompt; + }; + + scope.hidePrompt = function (object) { + object.deletePrompt = false; + }; + + /* ---------- TOOLBAR ---------- */ + + scope.toggleSortingMode = function (tool) { + + if (scope.sortingMode === true) { + + var sortOrderMissing = false; + + for (var i = 0; i < scope.model.groups.length; i++) { + var group = scope.model.groups[i]; + if (group.tabState !== "init" && group.sortOrder === undefined) { + sortOrderMissing = true; + group.showSortOrderMissing = true; + notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated); + } + } + + if (!sortOrderMissing) { + scope.sortingMode = false; + scope.sortingButtonKey = "general_reorder"; + } + + } else { + + scope.sortingMode = true; + scope.sortingButtonKey = "general_reorderDone"; - function setupAvailableContentTypesModel(result) { - scope.compositionsDialogModel.availableCompositeContentTypes = result; - //iterate each one and set it up - _.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (c) { - //enable it if it's part of the selected model - if (scope.compositionsDialogModel.compositeContentTypes.indexOf(c.contentType.alias) !== -1) { - c.allowed = true; } - //set the inherited flags - c.inherited = false; - if (scope.model.lockedCompositeContentTypes.indexOf(c.contentType.alias) > -1) { - c.inherited = true; - } - // convert icons for composite content types - iconHelper.formatContentTypeIcons([c.contentType]); - }); - } + }; - /* ---------- DELETE PROMT ---------- */ + scope.openCompositionsDialog = function () { - scope.togglePrompt = function (object) { - object.deletePrompt = !object.deletePrompt; - }; + scope.compositionsDialogModel = { + contentType: scope.model, + compositeContentTypes: scope.model.compositeContentTypes, + view: "views/common/infiniteeditors/compositions/compositions.html", + size: "small", + submit: function () { - scope.hidePrompt = function (object) { - object.deletePrompt = false; - }; + // make sure that all tabs has an init property + if (scope.model.groups.length !== 0) { + angular.forEach(scope.model.groups, function (group) { + addInitProperty(group); + }); + } - /* ---------- TOOLBAR ---------- */ + // remove overlay + editorService.close(); - scope.toggleSortingMode = function(tool) { + }, + close: function (oldModel) { - if (scope.sortingMode === true) { + // reset composition changes + scope.model.groups = oldModel.contentType.groups; + scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes; - var sortOrderMissing = false; + // remove overlay + editorService.close(); - for (var i = 0; i < scope.model.groups.length; i++) { - var group = scope.model.groups[i]; - if (group.tabState !== "init" && group.sortOrder === undefined) { - sortOrderMissing = true; - group.showSortOrderMissing = true; - notificationsService.error(validationTranslated + ": " + group.name + " " + tabNoSortOrderTranslated); - } - } + }, + selectCompositeContentType: function (selectedContentType) { - if (!sortOrderMissing) { - scope.sortingMode = false; - scope.sortingButtonKey = "general_reorder"; - } + //first check if this is a new selection - we need to store this value here before any further digests/async + // because after that the scope.model.compositeContentTypes will be populated with the selected value. + var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; - } else { + if (newSelection) { + //merge composition with content type - scope.sortingMode = true; - scope.sortingButtonKey = "general_reorderDone"; + //use a different resource lookup depending on the content type type + var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; - } + resourceLookup(selectedContentType.id).then(function (composition) { + //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and + // double check here. + var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); + if (overlappingAliases.length > 0) { + //this will create an invalid composition, need to uncheck it + scope.compositionsDialogModel.compositeContentTypes.splice( + scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); + //dissallow this until something else is unchecked + selectedContentType.allowed = false; + } + else { + contentTypeHelper.mergeCompositeContentType(scope.model, composition); + } - }; - - scope.openCompositionsDialog = function() { - - scope.compositionsDialogModel = { - contentType: scope.model, - compositeContentTypes: scope.model.compositeContentTypes, - view: "views/common/infiniteeditors/compositions/compositions.html", - size: "small", - submit: function() { - - // make sure that all tabs has an init property - if (scope.model.groups.length !== 0) { - angular.forEach(scope.model.groups, function(group) { - addInitProperty(group); - }); - } - - // remove overlay - editorService.close(); - - }, - close: function(oldModel) { - - // reset composition changes - scope.model.groups = oldModel.contentType.groups; - scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes; - - // remove overlay - editorService.close(); - - }, - selectCompositeContentType: function (selectedContentType) { - - //first check if this is a new selection - we need to store this value here before any further digests/async - // because after that the scope.model.compositeContentTypes will be populated with the selected value. - var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1; - - if (newSelection) { - //merge composition with content type - - //use a different resource lookup depending on the content type type - var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById; - - resourceLookup(selectedContentType.id).then(function (composition) { - //based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and - // double check here. - var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition); - if (overlappingAliases.length > 0) { - //this will create an invalid composition, need to uncheck it - scope.compositionsDialogModel.compositeContentTypes.splice( - scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1); - //dissallow this until something else is unchecked - selectedContentType.allowed = false; + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + // TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); + }); } else { - contentTypeHelper.mergeCompositeContentType(scope.model, composition); + // split composition from content type + contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); + + //based on the selection, we need to filter the available composite types list + filterAvailableCompositions(selectedContentType, newSelection).then(function () { + // TODO: Here we could probably re-enable selection if we previously showed a throbber or something + }); } - //based on the selection, we need to filter the available composite types list - filterAvailableCompositions(selectedContentType, newSelection).then(function () { - // TODO: Here we could probably re-enable selection if we previously showed a throbber or something - }); - }); - } - else { - // split composition from content type - contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType); + } + }; - //based on the selection, we need to filter the available composite types list - filterAvailableCompositions(selectedContentType, newSelection).then(function () { - // TODO: Here we could probably re-enable selection if we previously showed a throbber or something + //select which resource methods to use, eg document Type or Media Type versions + var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; + var whereUsedContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getWhereCompositionIsUsedInContentTypes : mediaTypeResource.getWhereCompositionIsUsedInContentTypes; + var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount; + + //get the currently assigned property type aliases - ensure we pass these to the server side filer + var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function (g) { + return _.map(g.properties, function (p) { + return p.alias; }); + })), function (f) { + return f !== null && f !== undefined; + }); + scope.compositionsButtonState = "busy"; + $q.all([ + //get available composite types + availableContentTypeResource(scope.model.id, [], propAliasesExisting, scope.model.isElement).then(function (result) { + setupAvailableContentTypesModel(result); + }), + //get where used document types + whereUsedContentTypeResource(scope.model.id).then(function (whereUsed) { + //pass to the dialog model the content type eg documentType or mediaType + scope.compositionsDialogModel.section = scope.contentType; + //pass the list of 'where used' document types + scope.compositionsDialogModel.whereCompositionUsed = whereUsed; + }), + //get content type count + countContentTypeResource().then(function (result) { + scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); + }) + ]).then(function () { + //resolves when both other promises are done, now show it + editorService.open(scope.compositionsDialogModel); + scope.compositionsButtonState = "init"; + }); + + }; + + + scope.openDocumentType = function (documentTypeId) { + const editor = { + id: documentTypeId, + submit: function (model) { + const args = { node: scope.model }; + eventsService.emit("editors.documentType.reload", args); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + + }; + + /* ---------- GROUPS ---------- */ + + scope.addGroup = function (group) { + + // set group sort order + var index = scope.model.groups.indexOf(group); + var prevGroup = scope.model.groups[index - 1]; + + if (index > 0) { + // set index to 1 higher than the previous groups sort order + group.sortOrder = prevGroup.sortOrder + 1; + + } else { + // first group - sort order will be 0 + group.sortOrder = 0; } + // activate group + scope.activateGroup(group); + + // push new init tab to the scope + addInitGroup(scope.model.groups); + }; + + scope.activateGroup = function (selectedGroup) { + + // set all other groups that are inactive to active + angular.forEach(scope.model.groups, function (group) { + // skip init tab + if (group.tabState !== "init") { + group.tabState = "inActive"; + } + }); + + selectedGroup.tabState = "active"; + + }; + + scope.canRemoveGroup = function (group) { + return group.inherited !== true && _.find(group.properties, function (property) { return property.locked === true; }) == null; } - }; - //select which resource methods to use, eg document Type or Media Type versions - var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes; - var whereUsedContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getWhereCompositionIsUsedInContentTypes : mediaTypeResource.getWhereCompositionIsUsedInContentTypes; - var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount; + scope.removeGroup = function (groupIndex) { + scope.model.groups.splice(groupIndex, 1); + }; - //get the currently assigned property type aliases - ensure we pass these to the server side filer - var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) { - return _.map(g.properties, function(p) { - return p.alias; - }); - })), function(f) { - return f !== null && f !== undefined; - }); - scope.compositionsButtonState = "busy"; - $q.all([ - //get available composite types - availableContentTypeResource(scope.model.id, [], propAliasesExisting, scope.model.isElement).then(function (result) { - setupAvailableContentTypesModel(result); - }), - //get where used document types - whereUsedContentTypeResource(scope.model.id).then(function (whereUsed) { - //pass to the dialog model the content type eg documentType or mediaType - scope.compositionsDialogModel.section = scope.contentType; - //pass the list of 'where used' document types - scope.compositionsDialogModel.whereCompositionUsed = whereUsed; - }), - //get content type count - countContentTypeResource().then(function(result) { - scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10); - }) - ]).then(function() { - //resolves when both other promises are done, now show it - editorService.open(scope.compositionsDialogModel); - scope.compositionsButtonState = "init"; - }); + scope.updateGroupTitle = function (group) { + if (group.properties.length === 0) { + addInitProperty(group); + } + }; - }; + scope.changeSortOrderValue = function (group) { + + if (group.sortOrder !== undefined) { + group.showSortOrderMissing = false; + } + scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); + }; + + function addInitGroup(groups) { + + // check i init tab already exists + var addGroup = true; + + angular.forEach(groups, function (group) { + if (group.tabState === "init") { + addGroup = false; + } + }); + + if (addGroup) { + groups.push({ + properties: [], + parentTabContentTypes: [], + parentTabContentTypeNames: [], + name: "", + tabState: "init" + }); + } + + return groups; + } + + function activateFirstGroup(groups) { + if (groups && groups.length > 0) { + var firstGroup = groups[0]; + if (!firstGroup.tabState || firstGroup.tabState === "inActive") { + firstGroup.tabState = "active"; + } + } + } + + /* ---------- PROPERTIES ---------- */ + + scope.addPropertyToActiveGroup = function () { + var group = _.find(scope.model.groups, group => group.tabState === "active"); + if (!group && scope.model.groups.length) { + group = scope.model.groups[0]; + } + + if (!group || !group.name) { + return; + } + + var property = _.find(group.properties, property => property.propertyState === "init"); + if (!property) { + return; + } + scope.addProperty(property, group); + } + + scope.addProperty = function (property, group) { + + // set property sort order + var index = group.properties.indexOf(property); + var prevProperty = group.properties[index - 1]; + + if (index > 0) { + // set index to 1 higher than the previous property sort order + property.sortOrder = prevProperty.sortOrder + 1; + + } else { + // first property - sort order will be 0 + property.sortOrder = 0; + } + + // open property settings dialog + scope.editPropertyTypeSettings(property, group); + + }; + + scope.editPropertyTypeSettings = function (property, group) { + + if (!property.inherited) { + + var oldPropertyModel = Utilities.copy(property); + if (oldPropertyModel.allowCultureVariant === undefined) { + // this is necessary for comparison when detecting changes to the property + oldPropertyModel.allowCultureVariant = scope.model.allowCultureVariant; + oldPropertyModel.alias = ""; + } + var propertyModel = Utilities.copy(property); + + var propertySettings = { + title: "Property settings", + property: propertyModel, + contentType: scope.contentType, + contentTypeName: scope.model.name, + contentTypeAllowCultureVariant: scope.model.allowCultureVariant, + contentTypeAllowSegmentVariant: scope.model.allowSegmentVariant, + view: "views/common/infiniteeditors/propertysettings/propertysettings.html", + size: "small", + submit: function (model) { + + property.inherited = false; + property.dialogIsOpen = false; + property.propertyState = "active"; + + // apply all property changes + property.label = propertyModel.label; + property.alias = propertyModel.alias; + property.description = propertyModel.description; + property.config = propertyModel.config; + property.editor = propertyModel.editor; + property.view = propertyModel.view; + property.dataTypeId = propertyModel.dataTypeId; + property.dataTypeIcon = propertyModel.dataTypeIcon; + property.dataTypeName = propertyModel.dataTypeName; + property.validation.mandatory = propertyModel.validation.mandatory; + property.validation.mandatoryMessage = propertyModel.validation.mandatoryMessage; + property.validation.pattern = propertyModel.validation.pattern; + property.validation.patternMessage = propertyModel.validation.patternMessage; + property.showOnMemberProfile = propertyModel.showOnMemberProfile; + property.memberCanEdit = propertyModel.memberCanEdit; + property.isSensitiveData = propertyModel.isSensitiveData; + property.isSensitiveValue = propertyModel.isSensitiveValue; + property.allowCultureVariant = propertyModel.allowCultureVariant; + property.allowSegmentVariant = propertyModel.allowSegmentVariant; + + // update existing data types + if (model.updateSameDataTypes) { + updateSameDataTypes(property); + } + + // close the editor + editorService.close(); + + // push new init property to group + addInitProperty(group); + + // set focus on init property + var numberOfProperties = group.properties.length; + group.properties[numberOfProperties - 1].focus = true; + + notifyChanged(); + }, + close: function () { + if (_.isEqual(oldPropertyModel, propertyModel) === false) { + localizationService.localizeMany(["general_confirm", "contentTypeEditor_propertyHasChanges", "general_cancel", "general_ok"]).then(function (data) { + const overlay = { + title: data[0], + content: data[1], + closeButtonLabel: data[2], + submitButtonLabel: data[3], + submitButtonStyle: "danger", + close: function () { + overlayService.close(); + }, + submit: function () { + // close the confirmation + overlayService.close(); + // close the editor + editorService.close(); + } + }; + + overlayService.open(overlay); + }); + } + else { + // remove the editor + editorService.close(); + } + } + }; + + // open property settings editor + editorService.open(propertySettings); + + // set property states + property.dialogIsOpen = true; + + } + }; + + scope.deleteProperty = function (tab, propertyIndex) { + + // remove property + tab.properties.splice(propertyIndex, 1); + + notifyChanged(); + }; + + function notifyChanged() { + eventsService.emit("editors.groupsBuilder.changed"); + } + + function addInitProperty(group) { + + var addInitPropertyBool = true; + var initProperty = { + label: null, + alias: null, + propertyState: "init", + validation: { + mandatory: false, + mandatoryMessage: null, + pattern: null, + patternMessage: null + } + }; + + // check if there already is an init property + angular.forEach(group.properties, function (property) { + if (property.propertyState === "init") { + addInitPropertyBool = false; + } + }); + + if (addInitPropertyBool) { + group.properties.push(initProperty); + } + + return group; + } + + function updateSameDataTypes(newProperty) { + + // find each property + angular.forEach(scope.model.groups, function (group) { + angular.forEach(group.properties, function (property) { + + if (property.dataTypeId === newProperty.dataTypeId) { + + // update property data + property.config = newProperty.config; + property.editor = newProperty.editor; + property.view = newProperty.view; + property.dataTypeId = newProperty.dataTypeId; + property.dataTypeIcon = newProperty.dataTypeIcon; + property.dataTypeName = newProperty.dataTypeName; + + } + + }); + }); + } + + function hasPropertyOfDataTypeId(dataTypeId) { + + // look at each property + var result = _.filter(scope.model.groups, function (group) { + return _.filter(group.properties, function (property) { + return (property.dataTypeId === dataTypeId); + }); + }); + + return (result.length > 0); + } - scope.openDocumentType = function (documentTypeId) { - const editor = { - id: documentTypeId, - submit: function (model) { - const args = { node: scope.model }; - eventsService.emit("editors.documentType.reload", args); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.documentTypeEditor(editor); + eventBindings.push(scope.$watch('model', function (newValue, oldValue) { + if (newValue !== undefined && newValue.groups !== undefined) { + activate(); + } + })); - }; + // clean up + eventBindings.push(eventsService.on("editors.dataTypeSettings.saved", function (name, args) { + if (hasPropertyOfDataTypeId(args.dataType.id)) { + scope.dataTypeHasChanged = true; + } + })); - /* ---------- GROUPS ---------- */ + // clean up + eventBindings.push(scope.$on('$destroy', function () { + for (var e in eventBindings) { + eventBindings[e](); + } + // if a dataType has changed, we want to notify which properties that are affected by this dataTypeSettings change + if (scope.dataTypeHasChanged === true) { + var args = { documentType: scope.model }; + eventsService.emit("editors.documentType.saved", args); + } + })); - scope.addGroup = function(group) { - - // set group sort order - var index = scope.model.groups.indexOf(group); - var prevGroup = scope.model.groups[index - 1]; - - if( index > 0) { - // set index to 1 higher than the previous groups sort order - group.sortOrder = prevGroup.sortOrder + 1; - - } else { - // first group - sort order will be 0 - group.sortOrder = 0; } - // activate group - scope.activateGroup(group); - - // push new init tab to the scope - addInitGroup(scope.model.groups); - }; - - scope.activateGroup = function(selectedGroup) { - - // set all other groups that are inactive to active - angular.forEach(scope.model.groups, function(group) { - // skip init tab - if (group.tabState !== "init") { - group.tabState = "inActive"; - } - }); - - selectedGroup.tabState = "active"; - - }; - - scope.canRemoveGroup = function(group){ - return group.inherited !== true && _.find(group.properties, function(property) { return property.locked === true; }) == null; - } - - scope.removeGroup = function(groupIndex) { - scope.model.groups.splice(groupIndex, 1); - }; - - scope.updateGroupTitle = function(group) { - if (group.properties.length === 0) { - addInitProperty(group); - } - }; - - scope.changeSortOrderValue = function(group) { - - if (group.sortOrder !== undefined) { - group.showSortOrderMissing = false; - } - scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder'); - }; - - function addInitGroup(groups) { - - // check i init tab already exists - var addGroup = true; - - angular.forEach(groups, function(group) { - if (group.tabState === "init") { - addGroup = false; - } - }); - - if (addGroup) { - groups.push({ - properties: [], - parentTabContentTypes: [], - parentTabContentTypeNames: [], - name: "", - tabState: "init" - }); - } - - return groups; - } - - function activateFirstGroup(groups) { - if (groups && groups.length > 0) { - var firstGroup = groups[0]; - if(!firstGroup.tabState || firstGroup.tabState === "inActive") { - firstGroup.tabState = "active"; - } - } - } - - /* ---------- PROPERTIES ---------- */ - - scope.addPropertyToActiveGroup = function () { - var group = _.find(scope.model.groups, group => group.tabState === "active"); - if (!group && scope.model.groups.length) { - group = scope.model.groups[0]; - } - - if (!group || !group.name) { - return; - } - - var property = _.find(group.properties, property => property.propertyState === "init"); - if (!property) { - return; - } - scope.addProperty(property, group); - } - - scope.addProperty = function(property, group) { - - // set property sort order - var index = group.properties.indexOf(property); - var prevProperty = group.properties[index - 1]; - - if( index > 0) { - // set index to 1 higher than the previous property sort order - property.sortOrder = prevProperty.sortOrder + 1; - - } else { - // first property - sort order will be 0 - property.sortOrder = 0; - } - - // open property settings dialog - scope.editPropertyTypeSettings(property, group); - - }; - - scope.editPropertyTypeSettings = function(property, group) { - - if (!property.inherited) { - - var oldPropertyModel = angular.copy(property); - if (oldPropertyModel.allowCultureVariant === undefined) { - // this is necessary for comparison when detecting changes to the property - oldPropertyModel.allowCultureVariant = scope.model.allowCultureVariant; - oldPropertyModel.alias = ""; - } - var propertyModel = angular.copy(property); - - var propertySettings = { - title: "Property settings", - property: propertyModel, - contentType: scope.contentType, - contentTypeName: scope.model.name, - contentTypeAllowCultureVariant: scope.model.allowCultureVariant, - view: "views/common/infiniteeditors/propertysettings/propertysettings.html", - size: "small", - submit: function(model) { - - property.inherited = false; - property.dialogIsOpen = false; - property.propertyState = "active"; - - // apply all property changes - property.label = propertyModel.label; - property.alias = propertyModel.alias; - property.description = propertyModel.description; - property.config = propertyModel.config; - property.editor = propertyModel.editor; - property.view = propertyModel.view; - property.dataTypeId = propertyModel.dataTypeId; - property.dataTypeIcon = propertyModel.dataTypeIcon; - property.dataTypeName = propertyModel.dataTypeName; - property.validation.mandatory = propertyModel.validation.mandatory; - property.validation.mandatoryMessage = propertyModel.validation.mandatoryMessage; - property.validation.pattern = propertyModel.validation.pattern; - property.validation.patternMessage = propertyModel.validation.patternMessage; - property.showOnMemberProfile = propertyModel.showOnMemberProfile; - property.memberCanEdit = propertyModel.memberCanEdit; - property.isSensitiveData = propertyModel.isSensitiveData; - property.isSensitiveValue = propertyModel.isSensitiveValue; - property.allowCultureVariant = propertyModel.allowCultureVariant; - - // update existing data types - if(model.updateSameDataTypes) { - updateSameDataTypes(property); - } - - // close the editor - editorService.close(); - - // push new init property to group - addInitProperty(group); - - // set focus on init property - var numberOfProperties = group.properties.length; - group.properties[numberOfProperties - 1].focus = true; - - notifyChanged(); + var directive = { + restrict: "E", + replace: true, + templateUrl: "views/components/umb-groups-builder.html", + scope: { + model: "=", + compositions: "=", + sorting: "=", + contentType: "@" }, - close: function() { - if(_.isEqual(oldPropertyModel, propertyModel) === false) { - localizationService.localizeMany(["general_confirm", "contentTypeEditor_propertyHasChanges", "general_cancel", "general_ok"]).then(function (data) { - const overlay = { - title: data[0], - content: data[1], - closeButtonLabel: data[2], - submitButtonLabel: data[3], - submitButtonStyle: "danger", - close: function () { - overlayService.close(); - }, - submit: function () { - // close the confirmation - overlayService.close(); - // close the editor - editorService.close(); - } - }; - - overlayService.open(overlay); - }); - } - else { - // remove the editor - editorService.close(); - } - } - }; - - // open property settings editor - editorService.open(propertySettings); - - // set property states - property.dialogIsOpen = true; - - } - }; - - scope.deleteProperty = function(tab, propertyIndex) { - - // remove property - tab.properties.splice(propertyIndex, 1); - - notifyChanged(); - }; - - function notifyChanged() { - eventsService.emit("editors.groupsBuilder.changed"); - } - - function addInitProperty(group) { - - var addInitPropertyBool = true; - var initProperty = { - label: null, - alias: null, - propertyState: "init", - validation: { - mandatory: false, - mandatoryMessage: null, - pattern: null, - patternMessage: null - } + link: link }; - // check if there already is an init property - angular.forEach(group.properties, function(property) { - if (property.propertyState === "init") { - addInitPropertyBool = false; - } - }); - - if (addInitPropertyBool) { - group.properties.push(initProperty); - } - - return group; - } - - function updateSameDataTypes(newProperty) { - - // find each property - angular.forEach(scope.model.groups, function(group){ - angular.forEach(group.properties, function(property){ - - if(property.dataTypeId === newProperty.dataTypeId) { - - // update property data - property.config = newProperty.config; - property.editor = newProperty.editor; - property.view = newProperty.view; - property.dataTypeId = newProperty.dataTypeId; - property.dataTypeIcon = newProperty.dataTypeIcon; - property.dataTypeName = newProperty.dataTypeName; - - } - - }); - }); - } - - function hasPropertyOfDataTypeId(dataTypeId) { - - // look at each property - var result = _.filter(scope.model.groups, function(group) { - return _.filter(group.properties, function(property) { - return (property.dataTypeId === dataTypeId); - }); - }); - - return (result.length > 0); - } - - - eventBindings.push(scope.$watch('model', function(newValue, oldValue) { - if (newValue !== undefined && newValue.groups !== undefined) { - activate(); - } - })); - - // clean up - eventBindings.push(eventsService.on("editors.dataTypeSettings.saved", function (name, args) { - if(hasPropertyOfDataTypeId(args.dataType.id)) { - scope.dataTypeHasChanged = true; - } - })); - - // clean up - eventBindings.push(scope.$on('$destroy', function() { - for(var e in eventBindings) { - eventBindings[e](); - } - // if a dataType has changed, we want to notify which properties that are affected by this dataTypeSettings change - if(scope.dataTypeHasChanged === true) { - var args = {documentType: scope.model}; - eventsService.emit("editors.documentType.saved", args); - } - })); - + return directive; } - var directive = { - restrict: "E", - replace: true, - templateUrl: "views/components/umb-groups-builder.html", - scope: { - model: "=", - compositions: "=", - sorting: "=", - contentType: "@" - }, - link: link - }; - - return directive; - } - - angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective); + angular.module('umbraco.directives').directive('umbGroupsBuilder', GroupsBuilderDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js index 9d76993fd3..988f8fab24 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbimagelazyload.directive.js @@ -31,7 +31,7 @@ Use this directive to lazy-load an image only when it is scrolled into view. **/ -(function() { +(function () { 'use strict'; function ImageLazyLoadDirective() { @@ -41,7 +41,7 @@ Use this directive to lazy-load an image only when it is scrolled into view. function link(scope, element, attrs) { const observer = new IntersectionObserver(loadImg); - const img = angular.element(element)[0]; + const img = element[0]; img.src = placeholder; observer.observe(img); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js index 953a28bd99..c499a8bfc6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbkeyboardshortcutsoverview.directive.js @@ -120,26 +120,26 @@ When this combination is hit an overview is opened with shortcuts based on the m scope.toggleShortcutsOverlay = function () { - if(overlay) { + if (overlay) { scope.close(); } else { scope.open(); } - if(scope.onToggle) { + if (scope.onToggle) { scope.onToggle(); } }; - scope.open = function() { - if(!overlay) { + scope.open = function () { + if (!overlay) { overlay = { title: "Keyboard shortcuts", view: "keyboardshortcuts", hideSubmitButton: true, shortcuts: scope.model, - close: function() { + close: function () { scope.close(); } }; @@ -147,20 +147,20 @@ When this combination is hit an overview is opened with shortcuts based on the m } }; - scope.close = function() { - if(overlay) { + scope.close = function () { + if (overlay) { overlayService.close(); overlay = null; - if(scope.onClose) { + if (scope.onClose) { scope.onClose(); } } }; function onInit() { - angular.forEach(scope.model, function (shortcutGroup) { - angular.forEach(shortcutGroup.shortcuts, function (shortcut) { + Utilities.forEach(scope.model, shortcutGroup => { + Utilities.forEach(shortcutGroup.shortcuts, shortcut => { shortcut.platformKeys = []; // get shortcut keys for mac @@ -173,30 +173,29 @@ When this combination is hit an overview is opened with shortcuts based on the m } else if (shortcut.keys && shortcut && shortcut.keys.length > 0) { shortcut.platformKeys = shortcut.keys; } - - }); + }) }); } onInit(); - eventBindings.push(scope.$watch('model', function(newValue, oldValue){ + eventBindings.push(scope.$watch('model', function (newValue, oldValue) { if (newValue !== oldValue) { onInit(); } })); - eventBindings.push(scope.$watch('showOverlay', function(newValue, oldValue){ + eventBindings.push(scope.$watch('showOverlay', function (newValue, oldValue) { - if(newValue === oldValue) { + if (newValue === oldValue) { return; } - if(newValue === true) { + if (newValue === true) { scope.open(); } - if(newValue === false) { + if (newValue === false) { scope.close(); } 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 1c4bf4d583..aa28d49c4a 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 @@ -314,7 +314,7 @@ Use this directive to generate a thumbnail grid of media items. }; var unbindItemsWatcher = scope.$watch('items', function(newValue, oldValue) { - if (angular.isArray(newValue)) { + if (Utilities.isArray(newValue)) { activate(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js index 471714d30b..72dba3ca2f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -3,12 +3,12 @@ function () { var link = function ($scope) { - + // Clone the model because some property editors // do weird things like updating and config values // so we want to ensure we start from a fresh every // time, we'll just sync the value back when we need to - $scope.model = angular.copy($scope.ngModel); + $scope.model = Utilities.copy($scope.ngModel); $scope.nodeContext = $scope.model; // Find the selected tab @@ -31,7 +31,7 @@ // Tell inner controls we are submitting $scope.$broadcast("formSubmitting", { scope: $scope }); - + // Sync the values back angular.forEach($scope.ngModel.variants[0].tabs, function (tab) { if (tab.alias.toLowerCase() === selectedTab.alias.toLowerCase()) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js deleted file mode 100644 index aac1b8dac1..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js +++ /dev/null @@ -1,37 +0,0 @@ -/** -@ngdoc directive -@name umbraco.directives.directive:umbPasswordToggle -@restrict E -@scope - -@description -Added in Umbraco v. 7.7.4: Use this directive to render a password toggle. - -**/ - -(function () { - 'use strict'; - - // comes from https://codepen.io/jakob-e/pen/eNBQaP - // works fine with Angular 1.6.5 - alas not with 1.1.5 - binding issue - - function PasswordToggleDirective($compile) { - - var directive = { - restrict: 'A', - scope: {}, - link: function(scope, elem, attrs) { - scope.tgl = function () { elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); } - var lnk = angular.element("Toggle"); - $compile(lnk)(scope); - elem.wrap("
").after(lnk); - } - }; - - return directive; - - } - - angular.module('umbraco.directives').directive('umbPasswordToggle', PasswordToggleDirective); - -})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js index 0003658600..21a1f181a6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js @@ -57,13 +57,13 @@ For extra details about options and events take a look here: https://refreshless **/ -(function() { - 'use strict'; +(function () { + 'use strict'; - var umbRangeSlider = { + var umbRangeSlider = { template: '
', - controller: UmbRangeSliderController, - bindings: { + controller: UmbRangeSliderController, + bindings: { ngModel: '<', options: '<', onSetup: '&?', @@ -73,15 +73,15 @@ For extra details about options and events take a look here: https://refreshless onChange: '&?', onStart: '&?', onEnd: '&?' - } + } }; - - function UmbRangeSliderController($element, $timeout, $scope, assetsService) { - + + function UmbRangeSliderController($element, $timeout, $scope, assetsService) { + const ctrl = this; let sliderInstance = null; - ctrl.$onInit = function() { + ctrl.$onInit = function () { // load css file for the date picker assetsService.loadCss('lib/nouislider/nouislider.min.css', $scope); @@ -94,13 +94,13 @@ For extra details about options and events take a look here: https://refreshless }; - function grabElementAndRun() { - $timeout(function() { + function grabElementAndRun() { + $timeout(function () { const element = $element.find('.umb-range-slider')[0]; - setSlider(element); - }, 0, true); + setSlider(element); + }, 0, true); } - + function setSlider(element) { sliderInstance = element; @@ -117,82 +117,82 @@ For extra details about options and events take a look here: https://refreshless // create new slider noUiSlider.create(sliderInstance, options); - - if (ctrl.onSetup) { - ctrl.onSetup({ - slider: sliderInstance - }); + + if (ctrl.onSetup) { + ctrl.onSetup({ + slider: sliderInstance + }); } // If has ngModel set the date - if (ctrl.ngModel) { + if (ctrl.ngModel) { sliderInstance.noUiSlider.set(ctrl.ngModel); } // destroy the slider instance when the dom element is removed - angular.element(element).on('$destroy', function() { + $(element).on('$destroy', function () { sliderInstance.noUiSlider.off(); }); setUpCallbacks(); - // Refresh the scope - $scope.$applyAsync(); + // Refresh the scope + $scope.$applyAsync(); } - + function setUpCallbacks() { - if(sliderInstance) { + if (sliderInstance) { // bind hook for update - if(ctrl.onUpdate) { - sliderInstance.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) { - $timeout(function() { - ctrl.onUpdate({values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions}); + if (ctrl.onUpdate) { + sliderInstance.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) { + $timeout(function () { + ctrl.onUpdate({ values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions }); }); }); } // bind hook for slide - if(ctrl.onSlide) { - sliderInstance.noUiSlider.on('slide', function (values, handle, unencoded, tap, positions) { - $timeout(function() { - ctrl.onSlide({values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions}); + if (ctrl.onSlide) { + sliderInstance.noUiSlider.on('slide', function (values, handle, unencoded, tap, positions) { + $timeout(function () { + ctrl.onSlide({ values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions }); }); }); } // bind hook for set - if(ctrl.onSet) { - sliderInstance.noUiSlider.on('set', function (values, handle, unencoded, tap, positions) { - $timeout(function() { - ctrl.onSet({values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions}); + if (ctrl.onSet) { + sliderInstance.noUiSlider.on('set', function (values, handle, unencoded, tap, positions) { + $timeout(function () { + ctrl.onSet({ values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions }); }); }); } // bind hook for change - if(ctrl.onChange) { - sliderInstance.noUiSlider.on('change', function (values, handle, unencoded, tap, positions) { - $timeout(function() { - ctrl.onChange({values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions}); + if (ctrl.onChange) { + sliderInstance.noUiSlider.on('change', function (values, handle, unencoded, tap, positions) { + $timeout(function () { + ctrl.onChange({ values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions }); }); }); } // bind hook for start - if(ctrl.onStart) { - sliderInstance.noUiSlider.on('start', function (values, handle, unencoded, tap, positions) { - $timeout(function() { - ctrl.onStart({values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions}); + if (ctrl.onStart) { + sliderInstance.noUiSlider.on('start', function (values, handle, unencoded, tap, positions) { + $timeout(function () { + ctrl.onStart({ values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions }); }); }); } // bind hook for end - if(ctrl.onEnd) { - sliderInstance.noUiSlider.on('end', function (values, handle, unencoded, tap, positions) { - $timeout(function() { - ctrl.onEnd({values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions}); + if (ctrl.onEnd) { + sliderInstance.noUiSlider.on('end', function (values, handle, unencoded, tap, positions) { + $timeout(function () { + ctrl.onEnd({ values: values, handle: handle, unencoded: unencoded, tap: tap, positions: positions }); }); }); } @@ -201,7 +201,7 @@ For extra details about options and events take a look here: https://refreshless } } - + angular.module('umbraco.directives').component('umbRangeSlider', umbRangeSlider); - + })(); 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 96a072330b..653b4f427c 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 @@ -26,6 +26,7 @@ fileManager.setFiles({ propertyAlias: vm.propertyAlias, culture: vm.culture, + segment: vm.segment, files: [] }); //clear the current files @@ -92,6 +93,11 @@ vm.culture = null; } + //normalize segment to null if it's not there + if (!vm.segment) { + vm.segment = null; + } + // TODO: need to figure out what we can do for things like Nested Content var existingClientFiles = checkPendingClientFiles(); @@ -134,11 +140,16 @@ vm.culture = null; } + //normalize segment to null if it's not there + if (!vm.segment) { + vm.segment = null; + } + //check the file manager to see if there's already local files pending for this editor var existingClientFiles = _.map( _.filter(fileManager.getFiles(), function (f) { - return f.alias === vm.propertyAlias && f.culture === vm.culture; + return f.alias === vm.propertyAlias && f.culture === vm.culture && f.segment === vm.segment; }), function (f) { return f.file; @@ -264,7 +275,8 @@ fileManager.setFiles({ propertyAlias: vm.propertyAlias, files: args.files, - culture: vm.culture + culture: vm.culture, + segment: vm.segment }); updateModelFromSelectedFiles(args.files).then(function(newVal) { @@ -287,6 +299,7 @@ templateUrl: 'views/components/upload/umb-property-file-upload.html', bindings: { culture: "@?", + segment: "@?", propertyAlias: "@", value: "<", hideSelection: "<", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index 97eb2bf708..8cbdabbf75 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -56,7 +56,7 @@ } //set the model defaults - if (!angular.isObject(vm.passwordValues)) { + if (!Utilities.isObject(vm.passwordValues)) { //if it's not an object then just create a new one vm.passwordValues = { newPassword: null, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js index 029a4e420f..023692be86 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/autoscale.directive.js @@ -21,7 +21,7 @@ angular.module("umbraco.directives") var totalOffset = 0; var offsety = parseInt(attrs.autoScale, 10); - var window = angular.element($window); + var window = $($window); if (offsety !== undefined) { totalOffset += offsety; } @@ -34,7 +34,7 @@ angular.module("umbraco.directives") el.height(window.height() - (el.offset().top + totalOffset)); } - var resizeCallback = function() { + var resizeCallback = function () { setElementSize(); }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/disabletabindex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/disabletabindex.directive.js index 759d05df71..d43282715e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/util/disabletabindex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/disabletabindex.directive.js @@ -1,46 +1,46 @@ angular.module("umbraco.directives") .directive('disableTabindex', function (tabbableService) { - return { - restrict: 'A', //Can only be used as an attribute, - scope: { - "disableTabindex": "<" - }, - link: function (scope, element, attrs) { + return { + restrict: 'A', //Can only be used as an attribute, + scope: { + "disableTabindex": "<" + }, + link: function (scope, element, attrs) { - if(scope.disableTabindex) { - //Select the node that will be observed for mutations (native DOM element not jQLite version) - var targetNode = element[0]; + if (scope.disableTabindex) { + //Select the node that will be observed for mutations (native DOM element not jQLite version) + var targetNode = element[0]; - //Watch for DOM changes - so when the property editor subview loads in - //We can be notified its updated the child elements inside the DIV we are watching - var observer = new MutationObserver(domChange); + //Watch for DOM changes - so when the property editor subview loads in + //We can be notified its updated the child elements inside the DIV we are watching + var observer = new MutationObserver(domChange); - // Options for the observer (which mutations to observe) - var config = { attributes: true, childList: true, subtree: true }; + // Options for the observer (which mutations to observe) + var config = { attributes: true, childList: true, subtree: true }; - function domChange(mutationsList, observer) { - for(var mutation of mutationsList) { + function domChange(mutationsList, observer) { + for (var mutation of mutationsList) { - //DOM items have been added or removed - if (mutation.type == 'childList') { + //DOM items have been added or removed + if (mutation.type == 'childList') { - //Check if any child items in mutation.target contain an input - var childInputs = tabbableService.tabbable(mutation.target); + //Check if any child items in mutation.target contain an input + var childInputs = tabbableService.tabbable(mutation.target); - //For each item in childInputs - override or set HTML attribute tabindex="-1" - angular.forEach(childInputs, function(element){ - angular.element(element).attr('tabindex', '-1'); - }); + //For each item in childInputs - override or set HTML attribute tabindex="-1" + angular.forEach(childInputs, function (element) { + $(element).attr('tabindex', '-1'); + }); + } } } + + // Start observing the target node for configured mutations + //GO GO GO + observer.observe(targetNode, config); } - // Start observing the target node for configured mutations - //GO GO GO - observer.observe(targetNode, config); } - - } - }; -}); + }; + }); 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 149c2b5087..30d3530efb 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 @@ -38,7 +38,8 @@ function valPropertyMsg(serverValidationManager, localizationService) { var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; - var currentCulture = currentProperty.culture; + var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; @@ -54,7 +55,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { var currentVariant = umbVariantCtrl.editor.content; // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -70,7 +71,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { //this can be null if no property was assigned if (scope.currentProperty) { //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, ""); + var err = serverValidationManager.getPropertyError(scope.currentProperty.alias, null, "", null); //if there's an error message use it if (err && err.errorMsg) { return err.errorMsg; @@ -101,7 +102,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { var errCount = 0; for (var e in formCtrl.$error) { - if (angular.isArray(formCtrl.$error[e])) { + if (Utilities.isArray(formCtrl.$error[e])) { errCount++; } } @@ -111,8 +112,8 @@ function valPropertyMsg(serverValidationManager, localizationService) { // is the only one, then we'll clear. if (errCount === 0 - || (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) - || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { + || (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg)) + || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { scope.errorMsg = ""; formCtrl.$setValidity('valPropertyMsg', true); } else if (showValidation && scope.errorMsg === "") { @@ -221,25 +222,31 @@ function valPropertyMsg(serverValidationManager, localizationService) { // the correct field validation in their property editors. if (scope.currentProperty) { //this can be null if no property was assigned + + function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + hasError = !isValid; + if (hasError) { + //set the error message to the server message + scope.errorMsg = propertyErrors[0].errorMsg; + //flag that the current validator is invalid + formCtrl.$setValidity('valPropertyMsg', false); + startWatch(); + } + else { + scope.errorMsg = ""; + //flag that the current validator is valid + formCtrl.$setValidity('valPropertyMsg', true); + stopWatch(); + } + } + unsubscribe.push(serverValidationManager.subscribe(scope.currentProperty.alias, currentCulture, "", - function(isValid, propertyErrors, allErrors) { - hasError = !isValid; - if (hasError) { - //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg; - //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); - startWatch(); - } - else { - scope.errorMsg = ""; - //flag that the current validator is valid - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); - } - })); + serverValidationManagerCallback, + currentSegment + ) + ); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index a0cc7e3033..3fa9220f7b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -24,6 +24,7 @@ function valServer(serverValidationManager) { var currentProperty = umbPropCtrl.property; var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive @@ -31,7 +32,7 @@ function valServer(serverValidationManager) { var currentVariant = umbVariantCtrl.editor.content; // Lets check if we have variants and we are on the default language then ... - if (umbVariantCtrl.content.variants.length > 1 && !currentVariant.language.isDefault && !currentCulture && !currentProperty.unlockInvariantValue) { + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { //This property is locked cause its a invariant property shown on a non-default language. //Therefor do not validate this field. return; @@ -75,7 +76,7 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); //clear the server validation entry - serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName); + serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName, currentSegment); stopWatch(); } }, true); @@ -90,23 +91,26 @@ function valServer(serverValidationManager) { } //subscribe to the server validation changes + function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + if (!isValid) { + modelCtrl.$setValidity('valServer', false); + //assign an error msg property to the current validator + modelCtrl.errorMsg = propertyErrors[0].errorMsg; + startWatch(); + } + else { + modelCtrl.$setValidity('valServer', true); + //reset the error message + modelCtrl.errorMsg = ""; + stopWatch(); + } + } unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias, currentCulture, fieldName, - function(isValid, propertyErrors, allErrors) { - if (!isValid) { - modelCtrl.$setValidity('valServer', false); - //assign an error msg property to the current validator - modelCtrl.errorMsg = propertyErrors[0].errorMsg; - startWatch(); - } - else { - modelCtrl.$setValidity('valServer', true); - //reset the error message - modelCtrl.errorMsg = ""; - stopWatch(); - } - })); + serverValidationManagerCallback, + currentSegment) + ); scope.$on('$destroy', function () { stopWatch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js index 524b5f7efe..1f5aaaa1c2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valsubview.directive.js @@ -40,7 +40,7 @@ function link(scope, el, attr, ctrl) { //if there are no containing form or valFormManager controllers, then we do nothing - if (!ctrl || !angular.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) { + if (!ctrl || !Utilities.isArray(ctrl) || ctrl.length !== 2 || !ctrl[0] || !ctrl[1]) { return; } diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsJoinArray.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsJoinArray.filter.js index de340bab27..a519f81054 100644 --- a/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsJoinArray.filter.js +++ b/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsJoinArray.filter.js @@ -1,6 +1,6 @@ /** * @ngdoc filter - * @name umbraco.filters.filter:CMS_joinArray + * @name umbraco.filters.filter:umbCmsJoinArray * @namespace umbCmsJoinArray * * param {array} array of string or objects, if an object use the third argument to specify which prop to list. diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsTitleCase.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsTitleCase.filter.js new file mode 100644 index 0000000000..8d2c233655 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/filters/umbCmsTitleCase.filter.js @@ -0,0 +1,19 @@ +/** + * @ngdoc filter + * @name umbraco.filters.filter:umbCmsTitleCase + * @namespace umbCmsTitleCase + * + * param {string} the text turned into title case. + * + * @description + * Transforms text to title case. Capitalizes the first letter of each word, and transforms the rest of the word to lower case. + * + */ +angular.module("umbraco.filters").filter('umbCmsTitleCase', function() { + return function (str) { + return str.replace( + /\w\S*/g, + txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase() + ); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/umbwordlimit.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/umbwordlimit.filter.js index c02624409f..93f0bddc96 100644 --- a/src/Umbraco.Web.UI.Client/src/common/filters/umbwordlimit.filter.js +++ b/src/Umbraco.Web.UI.Client/src/common/filters/umbwordlimit.filter.js @@ -13,7 +13,7 @@ function umbWordLimitFilter() { return function (collection, property) { - if (!angular.isString(collection)) { + if (!Utilities.isString(collection)) { return collection; } @@ -35,4 +35,4 @@ angular.module('umbraco.filters').filter('umbWordLimit', umbWordLimitFilter); -})(); \ No newline at end of file +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js index 186f3accf0..283a2a7ae9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/culturerequest.interceptor.js @@ -24,6 +24,7 @@ if ($routeParams) { // it's an API request, add the current client culture as a header value config.headers["X-UMB-CULTURE"] = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + config.headers["X-UMB-SEGMENT"] = $routeParams.csegment ? $routeParams.csegment : null; } return config; diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js index 03373089d7..f3dd60bdce 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/donotpostdollarvariablesrequest.interceptor.js @@ -1,40 +1,40 @@ -(function() { - 'use strict'; - +(function () { + 'use strict'; + function removeProperty(obj, propertyPrefix) { for (var property in obj) { if (obj.hasOwnProperty(property)) { - + if (property.startsWith(propertyPrefix) && obj[property] !== undefined) { obj[property] = undefined; } - + if (typeof obj[property] === "object") { removeProperty(obj[property], propertyPrefix); } } } - + } - - function transform(data){ + + function transform(data) { removeProperty(data, "$"); } - + function doNotPostDollarVariablesRequestInterceptor($q, urlHelper) { return { //dealing with requests: - 'request': function(config) { - if(config.method === "POST"){ - var clone = angular.copy(config); + 'request': function (config) { + if (config.method === "POST") { + var clone = Utilities.copy(config); transform(clone.data); return clone; } - + return config; } }; - } + } angular.module('umbraco.interceptors').factory('doNotPostDollarVariablesOnPostRequestInterceptor', doNotPostDollarVariablesRequestInterceptor); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js b/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js index 30daaf5837..bf748975a1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js +++ b/src/Umbraco.Web.UI.Client/src/common/interceptors/security.interceptor.js @@ -44,7 +44,7 @@ var headers = config.headers ? config.headers : {}; //Here we'll check if we should ignore the error (either based on the original header set or the request configuration) - if (headers["x-umb-ignore-error"] === "ignore" || config.umbIgnoreErrors === true || (angular.isArray(config.umbIgnoreStatus) && config.umbIgnoreStatus.indexOf(rejection.status) !== -1)) { + if (headers["x-umb-ignore-error"] === "ignore" || config.umbIgnoreErrors === true || (Utilities.isArray(config.umbIgnoreStatus) && config.umbIgnoreStatus.indexOf(rejection.status) !== -1)) { //exit/ignore return $q.reject(rejection); } diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index fc28567ea3..1eb259747e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -90,7 +90,7 @@ angular.module('umbraco.mocks'). name: "1 column layout", sections: [ { - grid: 12, + grid: 12 } ] }, @@ -98,7 +98,7 @@ angular.module('umbraco.mocks'). name: "2 column layout", sections: [ { - grid: 4, + grid: 4 }, { grid: 8 @@ -139,7 +139,7 @@ angular.module('umbraco.mocks'). } } - }, + } ] }, { diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/datatype.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/datatype.mocks.js index 054f0aa66d..e70f483bf4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/datatype.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/datatype.mocks.js @@ -1,113 +1,113 @@ angular.module('umbraco.mocks'). - factory('dataTypeMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { - 'use strict'; - - function returnById(status, data, headers) { + factory('dataTypeMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { + 'use strict'; - if (!mocksUtils.checkAuth()) { - return [401, null, null]; - } + function returnById(status, data, headers) { - var id = mocksUtils.getParameterByName(data, "id") || 1234; + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } - var selectedId = String.CreateGuid(); + var id = mocksUtils.getParameterByName(data, "id") || 1234; - var dataType = mocksUtils.getMockDataType(id, selectedId); - - return [200, dataType, null]; - } - - function returnEmpty(status, data, headers) { + var selectedId = String.CreateGuid(); - if (!mocksUtils.checkAuth()) { - return [401, null, null]; - } + var dataType = mocksUtils.getMockDataType(id, selectedId); - var response = returnById(200, "", null); - var node = response[1]; + return [200, dataType, null]; + } - node.name = ""; - node.selectedEditor = ""; - node.id = 0; - node.preValues = []; + function returnEmpty(status, data, headers) { - return response; - } - - function returnPreValues(status, data, headers) { + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } - if (!mocksUtils.checkAuth()) { - return [401, null, null]; - } + var response = returnById(200, "", null); + var node = response[1]; - var editorId = mocksUtils.getParameterByName(data, "editorId") || "83E9AD36-51A7-4440-8C07-8A5623AC6979"; + node.name = ""; + node.selectedEditor = ""; + node.id = 0; + node.preValues = []; - var preValues = [ - { - label: "Custom pre value 1 for editor " + editorId, - description: "Enter a value for this pre-value", - key: "myPreVal", - view: "requiredfield", - validation: [ - { - type: "Required" - } - ] - }, - { - label: "Custom pre value 2 for editor " + editorId, - description: "Enter a value for this pre-value", - key: "myPreVal", - view: "requiredfield", - validation: [ - { - type: "Required" - } - ] - } - ]; - return [200, preValues, null]; - } - - function returnSave(status, data, headers) { - if (!mocksUtils.checkAuth()) { - return [401, null, null]; - } + return response; + } - var postedData = angular.fromJson(headers); + function returnPreValues(status, data, headers) { - var dataType = mocksUtils.getMockDataType(postedData.id, postedData.selectedEditor); - dataType.notifications = [{ - header: "Saved", - message: "Data type saved", - type: 0 - }]; + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } - return [200, dataType, null]; - } + var editorId = mocksUtils.getParameterByName(data, "editorId") || "83E9AD36-51A7-4440-8C07-8A5623AC6979"; - return { - register: function() { - - $httpBackend - .whenPOST(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/PostSave')) - .respond(returnSave); - - $httpBackend - .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetById')) - .respond(returnById); - - $httpBackend - .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetEmpty')) - .respond(returnEmpty); - - $httpBackend - .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetPreValues')) - .respond(returnPreValues); - }, - expectGetById: function() { - $httpBackend - .expectGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetById')); - } - }; - }]); + var preValues = [ + { + label: "Custom pre value 1 for editor " + editorId, + description: "Enter a value for this pre-value", + key: "myPreVal", + view: "requiredfield", + validation: [ + { + type: "Required" + } + ] + }, + { + label: "Custom pre value 2 for editor " + editorId, + description: "Enter a value for this pre-value", + key: "myPreVal", + view: "requiredfield", + validation: [ + { + type: "Required" + } + ] + } + ]; + return [200, preValues, null]; + } + + function returnSave(status, data, headers) { + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } + + var postedData = JSON.parse(headers); + + var dataType = mocksUtils.getMockDataType(postedData.id, postedData.selectedEditor); + dataType.notifications = [{ + header: "Saved", + message: "Data type saved", + type: 0 + }]; + + return [200, dataType, null]; + } + + return { + register: function () { + + $httpBackend + .whenPOST(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/PostSave')) + .respond(returnSave); + + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetById')) + .respond(returnById); + + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetEmpty')) + .respond(returnEmpty); + + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetPreValues')) + .respond(returnPreValues); + }, + expectGetById: function () { + $httpBackend + .expectGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/DataType/GetById')); + } + }; + }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 678cffe42e..936f69e738 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -30,7 +30,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { umbRequestHelper.getApiUrl( "authenticationApiBaseUrl", "PostSend2FACode"), - angular.toJson(provider)), + Utilities.toJson(provider)), 'Could not send code'); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index d714ea4938..7bc2a9d2c8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -610,10 +610,10 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { //converts the value to a js bool function toBool(v) { - if (angular.isNumber(v)) { + if (Utilities.isNumber(v)) { return v > 0; } - if (angular.isString(v)) { + if (Utilities.isString(v)) { return v === "true"; } if (typeof v === "boolean") { @@ -1003,10 +1003,10 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { loginPageId: loginPageId, errorPageId: errorPageId }; - if (angular.isArray(groups) && groups.length) { + if (Utilities.isArray(groups) && groups.length) { publicAccess.groups = groups; } - else if (angular.isArray(usernames) && usernames.length) { + else if (Utilities.isArray(usernames) && usernames.length) { publicAccess.usernames = usernames; } else { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js index e10837ceca..60b87e919f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js @@ -87,7 +87,7 @@ function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { umbRequestHelper.getApiUrl( "currentUserApiBaseUrl", "PostSetInvitedUserPassword"), - angular.toJson(newPassword)), + Utilities.toJson(newPassword)), 'Failed to change password'); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index e4e3cc6f3f..e24f4786eb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -348,10 +348,10 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { //converts the value to a js bool function toBool(v) { - if (angular.isNumber(v)) { + if (Utilities.isNumber(v)) { return v > 0; } - if (angular.isString(v)) { + if (Utilities.isString(v)) { return v === "true"; } if (typeof v === "boolean") { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js index d21edbbab8..c45e173a98 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js @@ -53,10 +53,10 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) { //converts the value to a js bool function toBool(v) { - if (angular.isNumber(v)) { + if (Utilities.isNumber(v)) { return v > 0; } - if (angular.isString(v)) { + if (Utilities.isString(v)) { return v === "true"; } if (typeof v === "boolean") { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index 8cba83328f..f2ff711ac9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -33,7 +33,7 @@ function angularHelper($q) { //this is sequential promise chaining, it's not pretty but we need to do it this way. //$q.all doesn't execute promises in sequence but that's what we want to do here. - if (!angular.isArray(promises)) { + if (!Utilities.isArray(promises)) { throw "promises must be an array"; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js index 6673002981..30e59e9a88 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js @@ -256,7 +256,7 @@ angular.module('umbraco.services') load: function (pathArray, scope, defaultAssetType) { var promise; - if (!angular.isArray(pathArray)) { + if (!Utilities.isArray(pathArray)) { throw "pathArray must be an array"; } 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 20a47ae32c..bfcc0d536e 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 @@ -10,7 +10,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt function isValidIdentifier(id) { //empty id <= 0 - if (angular.isNumber(id)) { + if (Utilities.isNumber(id)) { if (id === 0) { return false; } @@ -39,7 +39,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt /** Used by the content editor and mini content editor to perform saving operations */ contentEditorPerformSave: function (args) { - if (!angular.isObject(args)) { + if (!Utilities.isObject(args)) { throw "args must be an object"; } if (!args.scope) { @@ -152,7 +152,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt here we'll build the buttons according to the chars of the user. */ configureContentEditorButtons: function (args) { - if (!angular.isObject(args)) { + if (!Utilities.isObject(args)) { throw "args must be an object"; } if (!args.content) { @@ -328,9 +328,11 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt * * @description * Returns a id for the variant that is unique between all variants on the content + * Note "invariant" is used for the invariant culture, + * "null" is used for the NULL segment */ buildCompositeVariantId: function (variant) { - return (variant.language ? variant.language.culture : "invariant") + "_" + (variant.segment ? variant.segment : ""); + return (variant.language ? variant.language.culture : "invariant") + "_" + (variant.segment ? variant.segment : "null"); }, @@ -698,7 +700,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt // /belle/#/content/edit/9876 (where 9876 is the new id) //clear the query strings - navigationService.clearSearch(["cculture"]); + navigationService.clearSearch(["cculture", "csegment"]); if (softRedirect) { navigationService.setSoftRedirect(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index 305e4a694d..1be66cc68f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -7,21 +7,21 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var contentTypeHelperService = { - createIdArray: function(array) { + createIdArray: function (array) { - var newArray = []; + var newArray = []; - angular.forEach(array, function(arrayItem){ + angular.forEach(array, function (arrayItem) { - if(angular.isObject(arrayItem)) { - newArray.push(arrayItem.id); - } else { - newArray.push(arrayItem); - } + if (Utilities.isObject(arrayItem)) { + newArray.push(arrayItem.id); + } else { + newArray.push(arrayItem); + } - }); + }); - return newArray; + return newArray; }, @@ -30,18 +30,18 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null; var modelsBuilderEnabled = Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled; if (modelsBuilderEnabled && modelsResource) { - modelsResource.buildModels().then(function(result) { + modelsResource.buildModels().then(function (result) { deferred.resolve(result); //just calling this to get the servar back to life modelsResource.getModelsOutOfDateStatus(); - }, function(e) { + }, function (e) { deferred.reject(e); }); } - else { - deferred.resolve(false); + else { + deferred.resolve(false); } return deferred.promise; }, @@ -49,10 +49,10 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje checkModelsBuilderStatus: function () { var deferred = $q.defer(); var modelsResource = $injector.has("modelsBuilderManagementResource") ? $injector.get("modelsBuilderManagementResource") : null; - var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); - + var modelsBuilderEnabled = (Umbraco && Umbraco.Sys && Umbraco.Sys.ServerVariables && Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder && Umbraco.Sys.ServerVariables.umbracoPlugins.modelsBuilder.enabled === true); + if (modelsBuilderEnabled && modelsResource) { - modelsResource.getModelsOutOfDateStatus().then(function(result) { + modelsResource.getModelsOutOfDateStatus().then(function (result) { //Generate models buttons should be enabled if it is 0 deferred.resolve(result.status === 0); }); @@ -64,37 +64,37 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje }, makeObjectArrayFromId: function (idArray, objectArray) { - var newArray = []; + var newArray = []; - for (var idIndex = 0; idArray.length > idIndex; idIndex++) { - var id = idArray[idIndex]; + for (var idIndex = 0; idArray.length > idIndex; idIndex++) { + var id = idArray[idIndex]; - for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) { - var object = objectArray[objectIndex]; - if (id === object.id) { - newArray.push(object); - } - } + for (var objectIndex = 0; objectArray.length > objectIndex; objectIndex++) { + var object = objectArray[objectIndex]; + if (id === object.id) { + newArray.push(object); + } + } - } + } - return newArray; + return newArray; }, - validateAddingComposition: function(contentType, compositeContentType) { + validateAddingComposition: function (contentType, compositeContentType) { //Validate that by adding this group that we are not adding duplicate property type aliases - var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) { - return _.map(g.properties, function(p) { + var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function (g) { + return _.map(g.properties, function (p) { return p.alias; }); })); - var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) { - return _.map(g.properties, function(p) { + var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function (g) { + return _.map(g.properties, function (p) { return p.alias; }); - })), function(f) { + })), function (f) { return f !== null && f !== undefined; }); @@ -108,7 +108,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje return []; }, - mergeCompositeContentType: function(contentType, compositeContentType) { + mergeCompositeContentType: function (contentType, compositeContentType) { //Validate that there are no overlapping aliases var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType); @@ -116,107 +116,107 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join()); } - angular.forEach(compositeContentType.groups, function(compositionGroup) { + angular.forEach(compositeContentType.groups, function (compositionGroup) { - // order composition groups based on sort order - compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); + // order composition groups based on sort order + compositionGroup.properties = $filter('orderBy')(compositionGroup.properties, 'sortOrder'); - // get data type details - angular.forEach(compositionGroup.properties, function(property) { - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - }); + // get data type details + angular.forEach(compositionGroup.properties, function (property) { + dataTypeResource.getById(property.dataTypeId) + .then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; + }); + }); - // set inherited state on tab - compositionGroup.inherited = true; + // set inherited state on tab + compositionGroup.inherited = true; - // set inherited state on properties - angular.forEach(compositionGroup.properties, function(compositionProperty) { - compositionProperty.inherited = true; - }); + // set inherited state on properties + angular.forEach(compositionGroup.properties, function (compositionProperty) { + compositionProperty.inherited = true; + }); - // set tab state - compositionGroup.tabState = "inActive"; + // set tab state + compositionGroup.tabState = "inActive"; - // if groups are named the same - merge the groups - angular.forEach(contentType.groups, function(contentTypeGroup) { + // if groups are named the same - merge the groups + angular.forEach(contentType.groups, function (contentTypeGroup) { - if (contentTypeGroup.name === compositionGroup.name) { + if (contentTypeGroup.name === compositionGroup.name) { - // set flag to show if properties has been merged into a tab - compositionGroup.groupIsMerged = true; + // set flag to show if properties has been merged into a tab + compositionGroup.groupIsMerged = true; - // make group inherited - contentTypeGroup.inherited = true; + // make group inherited + contentTypeGroup.inherited = true; - // add properties to the top of the array - contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties); + // add properties to the top of the array + contentTypeGroup.properties = compositionGroup.properties.concat(contentTypeGroup.properties); - // update sort order on all properties in merged group - contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + // update sort order on all properties in merged group + contentTypeGroup.properties = contentTypeHelperService.updatePropertiesSortOrder(contentTypeGroup.properties); + + // make parentTabContentTypeNames to an array so we can push values + if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) { + contentTypeGroup.parentTabContentTypeNames = []; + } + + // push name to array of merged composite content types + contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name); + + // make parentTabContentTypes to an array so we can push values + if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) { + contentTypeGroup.parentTabContentTypes = []; + } + + // push id to array of merged composite content types + contentTypeGroup.parentTabContentTypes.push(compositeContentType.id); + + // get sort order from composition + contentTypeGroup.sortOrder = compositionGroup.sortOrder; + + // splice group to the top of the array + var contentTypeGroupCopy = Utilities.copy(contentTypeGroup); + var index = contentType.groups.indexOf(contentTypeGroup); + contentType.groups.splice(index, 1); + contentType.groups.unshift(contentTypeGroupCopy); + + } + + }); + + // if group is not merged - push it to the end of the array - before init tab + if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) { // make parentTabContentTypeNames to an array so we can push values - if (contentTypeGroup.parentTabContentTypeNames === null || contentTypeGroup.parentTabContentTypeNames === undefined) { - contentTypeGroup.parentTabContentTypeNames = []; + if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) { + compositionGroup.parentTabContentTypeNames = []; } // push name to array of merged composite content types - contentTypeGroup.parentTabContentTypeNames.push(compositeContentType.name); + compositionGroup.parentTabContentTypeNames.push(compositeContentType.name); // make parentTabContentTypes to an array so we can push values - if (contentTypeGroup.parentTabContentTypes === null || contentTypeGroup.parentTabContentTypes === undefined) { - contentTypeGroup.parentTabContentTypes = []; + if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) { + compositionGroup.parentTabContentTypes = []; } // push id to array of merged composite content types - contentTypeGroup.parentTabContentTypes.push(compositeContentType.id); + compositionGroup.parentTabContentTypes.push(compositeContentType.id); - // get sort order from composition - contentTypeGroup.sortOrder = compositionGroup.sortOrder; + // push group before placeholder tab + contentType.groups.unshift(compositionGroup); - // splice group to the top of the array - var contentTypeGroupCopy = angular.copy(contentTypeGroup); - var index = contentType.groups.indexOf(contentTypeGroup); - contentType.groups.splice(index, 1); - contentType.groups.unshift(contentTypeGroupCopy); + } - } + }); - }); + // sort all groups by sortOrder property + contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder'); - // if group is not merged - push it to the end of the array - before init tab - if (compositionGroup.groupIsMerged === false || compositionGroup.groupIsMerged === undefined) { - - // make parentTabContentTypeNames to an array so we can push values - if (compositionGroup.parentTabContentTypeNames === null || compositionGroup.parentTabContentTypeNames === undefined) { - compositionGroup.parentTabContentTypeNames = []; - } - - // push name to array of merged composite content types - compositionGroup.parentTabContentTypeNames.push(compositeContentType.name); - - // make parentTabContentTypes to an array so we can push values - if (compositionGroup.parentTabContentTypes === null || compositionGroup.parentTabContentTypes === undefined) { - compositionGroup.parentTabContentTypes = []; - } - - // push id to array of merged composite content types - compositionGroup.parentTabContentTypes.push(compositeContentType.id); - - // push group before placeholder tab - contentType.groups.unshift(compositionGroup); - - } - - }); - - // sort all groups by sortOrder property - contentType.groups = $filter('orderBy')(contentType.groups, 'sortOrder'); - - return contentType; + return contentType; }, @@ -224,22 +224,22 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var groups = []; - angular.forEach(contentType.groups, function(contentTypeGroup){ + angular.forEach(contentType.groups, function (contentTypeGroup) { - if( contentTypeGroup.tabState !== "init" ) { + if (contentTypeGroup.tabState !== "init") { var idIndex = contentTypeGroup.parentTabContentTypes.indexOf(compositeContentType.id); var nameIndex = contentTypeGroup.parentTabContentTypeNames.indexOf(compositeContentType.name); var groupIndex = contentType.groups.indexOf(contentTypeGroup); - if( idIndex !== -1 ) { + if (idIndex !== -1) { var properties = []; // remove all properties from composite content type - angular.forEach(contentTypeGroup.properties, function(property){ - if(property.contentTypeId !== compositeContentType.id) { + angular.forEach(contentTypeGroup.properties, function (property) { + if (property.contentTypeId !== compositeContentType.id) { properties.push(property); } }); @@ -252,22 +252,22 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje contentTypeGroup.parentTabContentTypeNames.splice(nameIndex, 1); // remove inherited state if there are no inherited properties - if(contentTypeGroup.parentTabContentTypes.length === 0) { + if (contentTypeGroup.parentTabContentTypes.length === 0) { contentTypeGroup.inherited = false; } // remove group if there are no properties left - if(contentTypeGroup.properties.length > 1) { + if (contentTypeGroup.properties.length > 1) { //contentType.groups.splice(groupIndex, 1); groups.push(contentTypeGroup); } } else { - groups.push(contentTypeGroup); + groups.push(contentTypeGroup); } } else { - groups.push(contentTypeGroup); + groups.push(contentTypeGroup); } // update sort order on properties @@ -281,67 +281,67 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje updatePropertiesSortOrder: function (properties) { - var sortOrder = 0; + var sortOrder = 0; - angular.forEach(properties, function(property) { - if( !property.inherited && property.propertyState !== "init") { - property.sortOrder = sortOrder; - } - sortOrder++; - }); + angular.forEach(properties, function (property) { + if (!property.inherited && property.propertyState !== "init") { + property.sortOrder = sortOrder; + } + sortOrder++; + }); - return properties; + return properties; }, - getTemplatePlaceholder: function() { + getTemplatePlaceholder: function () { - var templatePlaceholder = { - "name": "", - "icon": "icon-layout", - "alias": "templatePlaceholder", - "placeholder": true - }; + var templatePlaceholder = { + "name": "", + "icon": "icon-layout", + "alias": "templatePlaceholder", + "placeholder": true + }; - return templatePlaceholder; + return templatePlaceholder; }, - insertDefaultTemplatePlaceholder: function(defaultTemplate) { + insertDefaultTemplatePlaceholder: function (defaultTemplate) { - // get template placeholder - var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); - // add as default template - defaultTemplate = templatePlaceholder; + // add as default template + defaultTemplate = templatePlaceholder; - return defaultTemplate; + return defaultTemplate; }, - insertTemplatePlaceholder: function(array) { + insertTemplatePlaceholder: function (array) { - // get template placeholder - var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); + // get template placeholder + var templatePlaceholder = contentTypeHelperService.getTemplatePlaceholder(); - // add as selected item - array.push(templatePlaceholder); + // add as selected item + array.push(templatePlaceholder); - return array; + return array; - }, + }, - insertChildNodePlaceholder: function (array, name, icon, id) { + insertChildNodePlaceholder: function (array, name, icon, id) { - var placeholder = { - "name": name, - "icon": icon, - "id": id - }; + var placeholder = { + "name": name, + "icon": icon, + "id": id + }; - array.push(placeholder); + array.push(placeholder); - } + } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 9c935086a0..538bd41ce0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -164,14 +164,14 @@ When building a custom infinite editor view you can use the same components as a "use strict"; function editorService(eventsService, keyboardService, $timeout) { - - + + let editorsKeyboardShorcuts = []; var editors = []; var isEnabled = true; var lastElementInFocus = null; - - + + // events for backdrop eventsService.on("appState.backdrop", function (name, args) { if (args.show === true) { @@ -180,7 +180,7 @@ When building a custom infinite editor view you can use the same components as a focus(); } }); - + /** * @ngdoc method @@ -205,7 +205,7 @@ When building a custom infinite editor view you can use the same components as a function getNumberOfEditors() { return editors.length; }; - + /** * @ngdoc method * @name umbraco.services.editorService#blur @@ -232,7 +232,7 @@ When building a custom infinite editor view you can use the same components as a * Method to tell editors that they are gaining focus again. */ function focus() { - if(isEnabled === false) { + if (isEnabled === false) { /* keyboard shortcuts will be overwritten by the new infinite editor so we need to store the shortcuts for the current editor so they can be rebound when the infinite editor closes @@ -241,7 +241,7 @@ When building a custom infinite editor view you can use the same components as a isEnabled = true; } } - + /** * @ngdoc method * @name umbraco.services.editorService#open @@ -305,7 +305,7 @@ When building a custom infinite editor view you can use the same components as a // delay required to map the properties to the correct editor due // to another delay in the closing animation of the editor - $timeout(function() { + $timeout(function () { // rebind keyboard shortcuts for the new editor in focus rebindKeyboardShortcuts(); @@ -651,7 +651,7 @@ When building a custom infinite editor view you can use the same components as a editor.view = "views/mediatypes/edit.html"; open(editor); } - + /** * @ngdoc method * @name umbraco.services.editorService#memberTypeEditor @@ -928,21 +928,21 @@ When building a custom infinite editor view you can use the same components as a open(editor); } - /** - * @ngdoc method - * @name umbraco.services.editorService#memberPicker - * @methodOf umbraco.services.editorService - * - * @description - * Opens a member picker in infinite editing, the submit callback returns an array of selected items - * - * @param {Object} editor rendering options - * @param {Boolean} editor.multiPicker Pick one or multiple items - * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object - * @param {Function} editor.close Callback function when the close button is clicked. - * - * @returns {Object} editor object - */ + /** + * @ngdoc method + * @name umbraco.services.editorService#memberPicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens a member picker in infinite editing, the submit callback returns an array of selected items + * + * @param {Object} editor rendering options + * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * + * @returns {Object} editor object + */ function memberPicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; if (!editor.size) editor.size = "small"; @@ -985,7 +985,7 @@ When building a custom infinite editor view you can use the same components as a * */ function unbindKeyboardShortcuts() { - const shortcuts = angular.copy(keyboardService.keyboardEvent); + const shortcuts = Utilities.copy(keyboardService.keyboardEvent); editorsKeyboardShorcuts.push(shortcuts); // unbind the current shortcuts because we only want to diff --git a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js index 51f63e6787..965ac3d635 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js @@ -1,7 +1,7 @@ /** Used to broadcast and listen for global events and allow the ability to add async listeners to the callbacks */ /* - Core app events: + Core app events: app.ready app.authenticated @@ -12,9 +12,9 @@ */ function eventsService($q, $rootScope) { - + return { - + /** raise an event with a given name */ emit: function (name, args) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js index 41614a3bee..38aee3fc4a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/filemanager.service.js @@ -27,10 +27,10 @@ function fileManager($rootScope) { setFiles: function (args) { //propertyAlias, files - if (!angular.isString(args.propertyAlias)) { + if (!Utilities.isString(args.propertyAlias)) { throw "args.propertyAlias must be a non empty string"; } - if (!angular.isObject(args.files)) { + if (!Utilities.isObject(args.files)) { throw "args.files must be an object"; } @@ -39,18 +39,22 @@ function fileManager($rootScope) { args.culture = null; } + if (!args.segment) { + args.segment = null; + } + var metaData = []; - if (angular.isArray(args.metaData)) { + if (Utilities.isArray(args.metaData)) { metaData = args.metaData; } - //this will clear the files for the current property/culture and then add the new ones for the current property + //this will clear the files for the current property/culture/segment and then add the new ones for the current property fileCollection = _.reject(fileCollection, function (item) { - return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture); + return item.alias === args.propertyAlias && (!args.culture || args.culture === item.culture) && (!args.segment || args.segment === item.segment); }); for (var i = 0; i < args.files.length; i++) { //save the file object to the files collection - fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, metaData: metaData }); + fileCollection.push({ alias: args.propertyAlias, file: args.files[i], culture: args.culture, segment: args.segment, metaData: metaData }); } }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 0555318bae..90fbd76ec9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -83,7 +83,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService if (!args || !args.notifications) { return false; } - if (angular.isArray(args.notifications)) { + if (Utilities.isArray(args.notifications)) { for (var i = 0; i < args.notifications.length; i++) { notificationsService.showNotification(args.notifications[i]); } @@ -159,9 +159,16 @@ function formHelper(angularHelper, serverValidationManager, notificationsService //the alias in model state can be in dot notation which indicates // * the first part is the content property alias // * the second part is the field to which the valiation msg is associated with - //There will always be at least 3 parts for content properties since all model errors for properties are prefixed with "_Properties" + //There will always be at least 4 parts for content properties since all model errors for properties are prefixed with "_Properties" //If it is not prefixed with "_Properties" that means the error is for a field of the object directly. + // Example: "_Properties.headerImage.en-US.mySegment.myField" + // * it's for a property since it has a _Properties prefix + // * it's for the headerImage property type + // * it's for the en-US culture + // * it's for the mySegment segment + // * it's for the myField html field (optional) + var parts = e.split("."); //Check if this is for content properties - specific to content/media/member editors because those are special @@ -179,16 +186,23 @@ function formHelper(angularHelper, serverValidationManager, notificationsService } } - //if it contains 3 '.' then we will wire it up to a property's html field + var segment = null; if (parts.length > 3) { - //add an error with a reference to the field for which the validation belongs too - serverValidationManager.addPropertyError(propertyAlias, culture, parts[3], modelState[e][0]); + segment = parts[3]; + //special check in case the string is formatted this way + if (segment === "null") { + segment = null; + } } - else { - //add a generic error for the property, no reference to a specific html field - serverValidationManager.addPropertyError(propertyAlias, culture, "", modelState[e][0]); + + var htmlFieldReference = ""; + if (parts.length > 4) { + htmlFieldReference = parts[4] || ""; } + // add a generic error for the property + serverValidationManager.addPropertyError(propertyAlias, culture, htmlFieldReference, modelState[e][0], segment); + } else { //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 20d014ab0f..28156e70c3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -332,14 +332,14 @@ selection.length = 0; - if (angular.isArray(items)) { + if (Utilities.isArray(items)) { for (i = 0; items.length > i; i++) { var item = items[i]; item.selected = false; } } - if(angular.isArray(folders)) { + if(Utilities.isArray(folders)) { for (i = 0; folders.length > i; i++) { var folder = folders[i]; folder.selected = false; @@ -366,7 +366,7 @@ var checkbox = $event.target; var clearSelection = false; - if (!angular.isArray(items)) { + if (!Utilities.isArray(items)) { return; } @@ -413,7 +413,7 @@ function selectAllItemsToggle(items, selection) { - if (!angular.isArray(items)) { + if (!Utilities.isArray(items)) { return; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js index a91f9d51e4..5b79b9c327 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/macro.service.js @@ -62,12 +62,12 @@ function macroService() { val = val ? val : ""; //need to detect if the val is a string or an object var keyVal; - if (angular.isString(val)) { + if (Utilities.isString(val)) { keyVal = key + "=\"" + (val ? val : "") + "\" "; } else { //if it's not a string we'll send it through the json serializer - var json = angular.toJson(val); + var json = Utilities.toJson(val); //then we need to url encode it so that it's safe var encoded = encodeURIComponent(json); keyVal = key + "=\"" + encoded + "\" "; @@ -142,7 +142,7 @@ function macroService() { if (item.value !== null && item.value !== undefined && !_.isString(item.value)) { try { - val = angular.toJson(val); + val = Utilities.toJson(val); } catch (e) { // not json diff --git a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js index 2271f891ce..ce2a18c3c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/mediahelper.service.js @@ -45,7 +45,7 @@ function mediaHelper(umbRequestHelper, $log) { //this performs a simple check to see if we have a media file as value //it doesnt catch everything, but better then nothing - if (angular.isString(item.value) && item.value.indexOf(mediaRoot) === 0) { + if (Utilities.isString(item.value) && item.value.indexOf(mediaRoot) === 0) { return true; } @@ -143,7 +143,7 @@ function mediaHelper(umbRequestHelper, $log) { */ resolveFileFromEntity: function (mediaEntity, thumbnail) { - var mediaPath = angular.isObject(mediaEntity.metaData) ? mediaEntity.metaData.MediaPath : null; + var mediaPath = Utilities.isObject(mediaEntity.metaData) ? mediaEntity.metaData.MediaPath : null; if (!mediaPath) { //don't throw since this image legitimately might not contain a media path, but output a warning diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index ab9cfb63d2..da784a1f9e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -26,60 +26,60 @@ function navigationService($routeParams, $location, $q, $injector, eventsService navReadyPromise.resolve(mainTreeApi); }); - + //A list of query strings defined that when changed will not cause a reload of the route - var nonRoutingQueryStrings = ["mculture", "cculture", "lq", "sr"]; + var nonRoutingQueryStrings = ["mculture", "cculture", "csegment", "lq", "sr"]; var retainedQueryStrings = ["mculture"]; - + function setMode(mode) { switch (mode) { - case 'tree': - appState.setGlobalState("navMode", "tree"); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", false); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showTray", false); - break; - case 'menu': - appState.setGlobalState("navMode", "menu"); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", true); - appState.setMenuState("showMenuDialog", false); - appState.setGlobalState("stickyNavigation", true); - break; - case 'dialog': - appState.setGlobalState("navMode", "dialog"); - appState.setGlobalState("stickyNavigation", true); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", true); - appState.setMenuState("allowHideMenuDialog", true); - break; - case 'search': - appState.setGlobalState("navMode", "search"); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showNavigation", true); - appState.setMenuState("showMenu", false); - appState.setSectionState("showSearchResults", true); - appState.setMenuState("showMenuDialog", false); - break; - default: - appState.setGlobalState("navMode", "default"); - appState.setMenuState("showMenu", false); - appState.setMenuState("showMenuDialog", false); - appState.setMenuState("allowHideMenuDialog", true); - appState.setSectionState("showSearchResults", false); - appState.setGlobalState("stickyNavigation", false); - appState.setGlobalState("showTray", false); - appState.setMenuState("currentNode", null); + case 'tree': + appState.setGlobalState("navMode", "tree"); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", false); + appState.setMenuState("showMenuDialog", false); + appState.setGlobalState("stickyNavigation", false); + appState.setGlobalState("showTray", false); + break; + case 'menu': + appState.setGlobalState("navMode", "menu"); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", true); + appState.setMenuState("showMenuDialog", false); + appState.setGlobalState("stickyNavigation", true); + break; + case 'dialog': + appState.setGlobalState("navMode", "dialog"); + appState.setGlobalState("stickyNavigation", true); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", false); + appState.setMenuState("showMenuDialog", true); + appState.setMenuState("allowHideMenuDialog", true); + break; + case 'search': + appState.setGlobalState("navMode", "search"); + appState.setGlobalState("stickyNavigation", false); + appState.setGlobalState("showNavigation", true); + appState.setMenuState("showMenu", false); + appState.setSectionState("showSearchResults", true); + appState.setMenuState("showMenuDialog", false); + break; + default: + appState.setGlobalState("navMode", "default"); + appState.setMenuState("showMenu", false); + appState.setMenuState("showMenuDialog", false); + appState.setMenuState("allowHideMenuDialog", true); + appState.setSectionState("showSearchResults", false); + appState.setGlobalState("stickyNavigation", false); + appState.setGlobalState("showTray", false); + appState.setMenuState("currentNode", null); - if (appState.getGlobalState("isTablet") === true) { - appState.setGlobalState("showNavigation", false); - } + if (appState.getGlobalState("isTablet") === true) { + appState.setGlobalState("showNavigation", false); + } - break; + break; } } @@ -88,7 +88,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @param {any} requestPath */ function pathToRouteParts(requestPath) { - if (!angular.isString(requestPath)) { + if (!Utilities.isString(requestPath)) { throw "The value for requestPath is not a string"; } var pathAndQuery = requestPath.split("#")[1]; @@ -114,7 +114,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService } var service = { - + /** * @ngdoc method * @name umbraco.services.navigationService#isRouteChangingNavigation @@ -130,11 +130,11 @@ function navigationService($routeParams, $location, $q, $injector, eventsService */ isRouteChangingNavigation: function (currUrlParams, nextUrlParams) { - if (angular.isString(currUrlParams)) { + if (Utilities.isString(currUrlParams)) { currUrlParams = pathToRouteParts(currUrlParams); } - if (angular.isString(nextUrlParams)) { + if (Utilities.isString(nextUrlParams)) { nextUrlParams = pathToRouteParts(nextUrlParams); } @@ -151,7 +151,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService var nextRoutingKeys = _.difference(_.keys(nextUrlParams), nonRoutingQueryStrings); var diff1 = _.difference(currRoutingKeys, nextRoutingKeys); var diff2 = _.difference(nextRoutingKeys, currRoutingKeys); - + //if the routing parameter keys are the same, we'll compare their values to see if any have changed and if so then the routing will be allowed. if (diff1.length === 0 && diff2.length === 0) { var partsChanged = 0; @@ -223,7 +223,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @param {Object} nextRouteParams The next route parameters */ retainQueryStrings: function (currRouteParams, nextRouteParams) { - var toRetain = angular.copy(nextRouteParams); + var toRetain = Utilities.copy(nextRouteParams); var updated = false; _.each(retainedQueryStrings, function (r) { @@ -260,7 +260,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * and load the dashboard related to the section * @param {string} sectionAlias The alias of the section */ - changeSection: function(sectionAlias, force) { + changeSection: function (sectionAlias, force) { setMode("default-opensection"); if (force && appState.getSectionState("currentSection") === sectionAlias) { @@ -360,19 +360,19 @@ function navigationService($routeParams, $location, $q, $injector, eventsService TODO: Delete this if not required */ - _syncPath: function(path, forceReload) { + _syncPath: function (path, forceReload) { return navReadyPromise.promise.then(function () { return mainTreeApi.syncTree({ path: path, forceReload: forceReload }); }); }, - - reloadNode: function(node) { + + reloadNode: function (node) { return navReadyPromise.promise.then(function () { return mainTreeApi.reloadNode(node); }); }, - - reloadSection: function(sectionAlias) { + + reloadSection: function (sectionAlias) { return navReadyPromise.promise.then(function () { treeService.clearCache({ section: sectionAlias }); return mainTreeApi.load(sectionAlias); @@ -387,11 +387,11 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @description * Hides the tree by hiding the containing dom element */ - hideTree: function() { + hideTree: function () { if (appState.getGlobalState("isTablet") === true && !appState.getGlobalState("stickyNavigation")) { //reset it to whatever is in the url - appState.setSectionState("currentSection", $routeParams.section); + appState.setSectionState("currentSection", $routeParams.section); setMode("default-hidesectiontree"); } @@ -409,19 +409,19 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * * @param {Event} event the click event triggering the method, passed from the DOM element */ - showMenu: function(args) { - + showMenu: function (args) { + var self = this; return treeService.getMenu({ treeNode: args.node }) - .then(function(data) { + .then(function (data) { //check for a default //NOTE: event will be undefined when a call to hideDialog is made so it won't re-load the default again. // but perhaps there's a better way to deal with with an additional parameter in the args ? it works though. if (data.defaultAlias && !args.skipDefault) { - var found = _.find(data.menuItems, function(item) { + var found = _.find(data.menuItems, function (item) { return item.alias = data.defaultAlias; }); @@ -450,7 +450,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService return $q.resolve(); }); - + }, /** @@ -461,7 +461,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @description * Hides the menu by hiding the containing dom element */ - hideMenu: function() { + hideMenu: function () { //SD: Would we ever want to access the last action'd node instead of clearing it here? appState.setMenuState("currentNode", null); appState.setMenuState("menuActions", []); @@ -483,14 +483,14 @@ function navigationService($routeParams, $location, $q, $injector, eventsService appState.setMenuState("currentNode", node); - if (action.metaData && action.metaData["actionRoute"] && angular.isString(action.metaData["actionRoute"])) { + if (action.metaData && action.metaData["actionRoute"] && Utilities.isString(action.metaData["actionRoute"])) { //first check if the menu item simply navigates to a route var parts = action.metaData["actionRoute"].split("?"); $location.path(parts[0]).search(parts.length > 1 ? parts[1] : ""); this.hideNavigation(); return; } - else if (action.metaData && action.metaData["jsAction"] && angular.isString(action.metaData["jsAction"])) { + else if (action.metaData && action.metaData["jsAction"] && Utilities.isString(action.metaData["jsAction"])) { //we'll try to get the jsAction from the injector var menuAction = action.metaData["jsAction"].split('.'); @@ -532,7 +532,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService }); } }, - + /** * @ngdoc method @@ -553,7 +553,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @param {Scope} args.scope current scope passed to the dialog * @param {Object} args.action the clicked action containing `name` and `alias` */ - showDialog: function(args) { + showDialog: function (args) { if (!args) { throw "showDialog is missing the args parameter"; @@ -578,20 +578,20 @@ function navigationService($routeParams, $location, $q, $injector, eventsService if (args.action.metaData["actionView"]) { templateUrl = args.action.metaData["actionView"]; } - else { + else { var treeAlias = treeService.getTreeAlias(args.node); if (!treeAlias) { throw "Could not get tree alias for node " + args.node.id; - } + } templateUrl = this.getTreeTemplateUrl(treeAlias, args.action.alias); } setMode("dialog"); - if(templateUrl) { + if (templateUrl) { appState.setMenuState("dialogTemplateUrl", templateUrl); } - + }, /** * @ngdoc method @@ -607,7 +607,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * we will also check for a 'packageName' for the current tree, if it exists then the convention will be: * for example: /App_Plugins/{mypackage}/backoffice/{treetype}/create.html */ - getTreeTemplateUrl: function(treeAlias, action) { + getTreeTemplateUrl: function (treeAlias, action) { var packageTreeFolder = treeService.getTreePackageFolder(treeAlias); if (packageTreeFolder) { return Umbraco.Sys.ServerVariables.umbracoSettings.appPluginsPath + @@ -661,7 +661,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @description * shows the search pane */ - showSearch: function() { + showSearch: function () { setMode("search"); }, /** @@ -672,7 +672,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @description * hides the search pane */ - hideSearch: function() { + hideSearch: function () { setMode("default-hidesearch"); }, /** @@ -683,7 +683,7 @@ function navigationService($routeParams, $location, $q, $injector, eventsService * @description * hides any open navigation panes and resets the tree, actions and the currently selected node */ - hideNavigation: function() { + hideNavigation: function () { appState.setMenuState("menuActions", []); setMode("default"); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js index e5701b9de0..196e0e3baa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js @@ -245,7 +245,7 @@ angular.module('umbraco.services') * @param {Int} index index where the notication should be removed from */ remove: function (index) { - if(angular.isObject(index)){ + if (Utilities.isObject(index)){ var i = nArray.indexOf(index); angularHelper.safeApply($rootScope, function() { nArray.splice(i, 1); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index b9bfa51122..718e44d66e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -13,19 +13,20 @@ function serverValidationManager($timeout) { var callbacks = []; /** calls the callback specified with the errors specified, used internally */ - function executeCallback(self, errorsForCallback, callback, culture) { + function executeCallback(self, errorsForCallback, callback, culture, segment) { callback.apply(self, [ false, // pass in a value indicating it is invalid errorsForCallback, // pass in the errors for this item self.items, // pass in all errors in total - culture // pass the culture that we are listing for. + culture, // pass the culture that we are listing for. + segment // pass the segment that we are listing for. ] ); } function getFieldErrors(self, fieldName) { - if (!angular.isString(fieldName)) { + if (!Utilities.isString(fieldName)) { throw "fieldName must be a string"; } @@ -35,33 +36,40 @@ function serverValidationManager($timeout) { }); } - function getPropertyErrors(self, propertyAlias, culture, fieldName) { - if (!angular.isString(propertyAlias)) { + + function getPropertyErrors(self, propertyAlias, culture, segment, fieldName) { + if (!Utilities.isString(propertyAlias)) { throw "propertyAlias must be a string"; } - if (fieldName && !angular.isString(fieldName)) { + if (fieldName && !Utilities.isString(fieldName)) { throw "fieldName must be a string"; } if (!culture) { culture = "invariant"; } + if (!segment) { + segment = null; + } //find all errors for this property return _.filter(self.items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } - function getCultureErrors(self, culture) { + function getVariantErrors(self, culture, segment) { if (!culture) { culture = "invariant"; } + if (!segment) { + segment = null; + } //find all errors for this property return _.filter(self.items, function (item) { - return (item.culture === culture); + return (item.culture === culture && item.segment === segment); }); } @@ -71,21 +79,21 @@ function serverValidationManager($timeout) { //its a field error callback var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName); if (fieldErrors.length > 0) { - executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture); + executeCallback(self, fieldErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } else if (callbacks[cb].propertyAlias != null) { //its a property error - var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].fieldName); + var propErrors = getPropertyErrors(self, callbacks[cb].propertyAlias, callbacks[cb].culture, callbacks[cb].segment, callbacks[cb].fieldName); if (propErrors.length > 0) { - executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture); + executeCallback(self, propErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } else { - //its a culture error - var cultureErrors = getCultureErrors(self, callbacks[cb].culture); - if (cultureErrors.length > 0) { - executeCallback(self, cultureErrors, callbacks[cb].callback, callbacks[cb].culture); + //its a variant error + var variantErrors = getVariantErrors(self, callbacks[cb].culture, callbacks[cb].segment); + if (variantErrors.length > 0) { + executeCallback(self, variantErrors, callbacks[cb].callback, callbacks[cb].culture, callbacks[cb].segment); } } } @@ -150,20 +158,27 @@ function serverValidationManager($timeout) { * field alias to listen for. * If propertyAlias is null, then this subscription is for a field property (not a user defined property). */ - subscribe: function (propertyAlias, culture, fieldName, callback) { + subscribe: function (propertyAlias, culture, fieldName, callback, segment) { if (!callback) { return; } var id = String.CreateGuid(); + + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } if (propertyAlias === null) { callbacks.push({ propertyAlias: null, culture: culture, + segment: segment, fieldName: fieldName, callback: callback, id: id @@ -175,6 +190,7 @@ function serverValidationManager($timeout) { callbacks.push({ propertyAlias: propertyAlias, culture: culture, + segment: segment, fieldName: fieldName, callback: callback, id: id @@ -199,25 +215,29 @@ function serverValidationManager($timeout) { * @param {} fieldName * @returns {} */ - unsubscribe: function (propertyAlias, culture, fieldName) { + unsubscribe: function (propertyAlias, culture, fieldName, segment) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } if (propertyAlias === null) { //remove all callbacks for the content field callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === null && item.culture === culture && item.fieldName === fieldName; + return item.propertyAlias === null && item.culture === culture && item.segment === segment && item.fieldName === fieldName; }); } else if (propertyAlias !== undefined) { //remove all callbacks for the content property callbacks = _.reject(callbacks, function (item) { - return item.propertyAlias === propertyAlias && item.culture === culture && + return item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || ((item.fieldName === undefined || item.fieldName === "") && (fieldName === undefined || fieldName === ""))); }); @@ -236,16 +256,20 @@ function serverValidationManager($timeout) { * This will always return any callbacks registered for just the property (i.e. field name is empty) and for ones with an * explicit field name set. */ - getPropertyCallbacks: function (propertyAlias, culture, fieldName) { + getPropertyCallbacks: function (propertyAlias, culture, fieldName, segment) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly against the field and for only the property - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); }); return found; }, @@ -262,7 +286,7 @@ function serverValidationManager($timeout) { getFieldCallbacks: function (fieldName) { var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly against the field - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return found; }, @@ -274,12 +298,29 @@ function serverValidationManager($timeout) { * @function * * @description - * Gets all callbacks that has been registered using the subscribe method for the culture. + * Gets all callbacks that has been registered using the subscribe method for the culture. Not including segments. */ getCultureCallbacks: function (culture) { var found = _.filter(callbacks, function (item) { //returns any callback that have been registered directly/ONLY against the culture - return (item.culture === culture && item.propertyAlias === null && item.fieldName === null); + return (item.culture === culture && item.segment === null && item.propertyAlias === null && item.fieldName === null); + }); + return found; + }, + + /** + * @ngdoc function + * @name getVariantCallbacks + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Gets all callbacks that has been registered using the subscribe method for the culture and segment. + */ + getVariantCallbacks: function (culture, segment) { + var found = _.filter(callbacks, function (item) { + //returns any callback that have been registered directly against the given culture and given segment. + return (item.culture === culture && item.segment === segment && item.propertyAlias === null && item.fieldName === null); }); return found; }, @@ -303,6 +344,7 @@ function serverValidationManager($timeout) { this.items.push({ propertyAlias: null, culture: "invariant", + segment: null, fieldName: fieldName, errorMsg: errorMsg }); @@ -314,7 +356,7 @@ function serverValidationManager($timeout) { var cbs = this.getFieldCallbacks(fieldName); //call each callback for this error for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, null); + executeCallback(this, errorsForCallback, cbs[cb].callback, null, null); } }, @@ -327,7 +369,7 @@ function serverValidationManager($timeout) { * @description * Adds an error message for the content property */ - addPropertyError: function (propertyAlias, culture, fieldName, errorMsg) { + addPropertyError: function (propertyAlias, culture, fieldName, errorMsg, segment) { if (!propertyAlias) { return; } @@ -336,31 +378,36 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } //only add the item if it doesn't exist - if (!this.hasPropertyError(propertyAlias, culture, fieldName)) { + if (!this.hasPropertyError(propertyAlias, culture, fieldName, segment)) { this.items.push({ propertyAlias: propertyAlias, culture: culture, + segment: segment, fieldName: fieldName, errorMsg: errorMsg }); } //find all errors for this item - var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, fieldName); + var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, segment, fieldName); //we should now call all of the call backs registered for this error - var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName); + var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName, segment); //call each callback for this error for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, culture); + executeCallback(this, errorsForCallback, cbs[cb].callback, culture, segment); } - //execute culture specific callbacks here too when a propery error is added - var cultureCbs = this.getCultureCallbacks(culture); + //execute variant specific callbacks here too when a propery error is added + var variantCbs = this.getVariantCallbacks(culture, segment); //call each callback for this error - for (var cb in cultureCbs) { - executeCallback(this, errorsForCallback, cultureCbs[cb].callback, culture); + for (var cb in variantCbs) { + executeCallback(this, errorsForCallback, variantCbs[cb].callback, culture, segment); } }, @@ -373,7 +420,7 @@ function serverValidationManager($timeout) { * @description * Removes an error message for the content property */ - removePropertyError: function (propertyAlias, culture, fieldName) { + removePropertyError: function (propertyAlias, culture, fieldName, segment) { if (!propertyAlias) { return; @@ -383,10 +430,14 @@ function serverValidationManager($timeout) { if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } //remove the item this.items = _.reject(this.items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); }, @@ -405,7 +456,10 @@ function serverValidationManager($timeout) { callbacks[cb].callback.apply(this, [ true, //pass in a value indicating it is VALID [], //pass in empty collection - []]); //pass in empty collection + [], + null, + null] + ); } }, @@ -431,16 +485,20 @@ function serverValidationManager($timeout) { * @description * Gets the error message for the content property */ - getPropertyError: function (propertyAlias, culture, fieldName) { + getPropertyError: function (propertyAlias, culture, fieldName, segment) { - //normalize culture to null + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); return err; }, @@ -457,7 +515,7 @@ function serverValidationManager($timeout) { getFieldError: function (fieldName) { var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return err; }, @@ -471,16 +529,20 @@ function serverValidationManager($timeout) { * @description * Checks if the content property + culture + field name combo has an error */ - hasPropertyError: function (propertyAlias, culture, fieldName) { + hasPropertyError: function (propertyAlias, culture, fieldName, segment) { //normalize culture to null if (!culture) { culture = "invariant"; } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); return err ? true : false; }, @@ -497,12 +559,11 @@ function serverValidationManager($timeout) { hasFieldError: function (fieldName) { var err = _.find(this.items, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); + return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); return err ? true : false; }, - /** * @ngdoc function * @name hasCultureError @@ -513,14 +574,40 @@ function serverValidationManager($timeout) { * Checks if the given culture has an error */ hasCultureError: function (culture) { - - //normalize culture to null + + //normalize culture to "invariant" if (!culture) { culture = "invariant"; } + + var err = _.find(this.items, function (item) { + return (item.culture === culture && item.segment === null); + }); + return err ? true : false; + }, + + /** + * @ngdoc function + * @name hasVariantError + * @methodOf umbraco.services.serverValidationManager + * @function + * + * @description + * Checks if the given culture has an error + */ + hasVariantError: function (culture, segment) { + + //normalize culture to "invariant" + if (!culture) { + culture = "invariant"; + } + //normalize segment to null + if (!segment) { + segment = null; + } var err = _.find(this.items, function (item) { - return item.culture === culture; + return (item.culture === culture && item.segment === segment); }); return err ? true : false; }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index d2b91a3707..66f0b110bf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -480,7 +480,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s //now we need to check if this custom config key is defined in our baseline, if it is we don't want to //overwrite the baseline config item if it is an array, we want to concat the items in the array, otherwise //if it's an object it will overwrite the baseline - if (angular.isArray(config[i]) && angular.isArray(tinyMceConfig.customConfig[i])) { + if (Utilities.isArray(config[i]) && Utilities.isArray(tinyMceConfig.customConfig[i])) { //concat it and below this concat'd array will overwrite the baseline in angular.extend tinyMceConfig.customConfig[i] = config[i].concat(tinyMceConfig.customConfig[i]); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js index 8fcab445b3..4166725c39 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -9,7 +9,7 @@ 'use strict'; function tourService(eventsService, currentUserResource, $q, tourResource) { - + var tours = []; var currentTour = null; @@ -18,14 +18,16 @@ */ function registerAllTours() { tours = []; - return tourResource.getTours().then(function(tourFiles) { - angular.forEach(tourFiles, function (tourFile) { - angular.forEach(tourFile.tours, function(newTour) { + return tourResource.getTours().then(function (tourFiles) { + Utilities.forEach(tourFiles, tourFile => { + + Utilities.forEach(tourFile.tours, newTour => { validateTour(newTour); validateTourRegistration(newTour); - tours.push(newTour); + tours.push(newTour); }); }); + eventsService.emit("appState.tour.updatedTours", tours); }); } @@ -74,7 +76,7 @@ tour.disabled = true; currentUserResource .saveTourStatus({ alias: tour.alias, disabled: tour.disabled, completed: tour.completed }).then( - function() { + function () { eventsService.emit("appState.tour.end", tour); currentTour = null; deferred.resolve(tour); @@ -96,7 +98,7 @@ tour.completed = true; currentUserResource .saveTourStatus({ alias: tour.alias, disabled: tour.disabled, completed: tour.completed }).then( - function() { + function () { eventsService.emit("appState.tour.complete", tour); currentTour = null; deferred.resolve(tour); @@ -130,10 +132,10 @@ function getGroupedTours() { var deferred = $q.defer(); var tours = getTours(); - setTourStatuses(tours).then(function() { + setTourStatuses(tours).then(function () { var groupedTours = []; tours.forEach(function (item) { - + if (item.contentType === null || item.contentType === '') { var groupExists = false; var newGroup = { @@ -149,9 +151,9 @@ } groupExists = true; - if(item.hidden === false){ - group.tours.push(item); - } + if (item.hidden === false) { + group.tours.push(item); + } } }); @@ -162,7 +164,7 @@ newGroup.groupOrder = item.groupOrder; } - if(item.hidden === false){ + if (item.hidden === false) { newGroup.tours.push(item); groupedTours.push(newGroup); } @@ -242,14 +244,14 @@ throw "Tour " + tour.alias + " is missing the required sections"; } } - + /** * Validates a tour before it gets registered in the service * @param {any} tour */ function validateTourRegistration(tour) { // check for existing tours with the same alias - angular.forEach(tours, function (existingTour) { + Utilities.forEach(tours, existingTour => { if (existingTour.alias === tour.alias) { throw "A tour with the alias " + tour.alias + " is already registered"; } @@ -265,16 +267,17 @@ var deferred = $q.defer(); currentUserResource.getTours().then(function (storedTours) { - angular.forEach(storedTours, function (storedTour) { + Utilities.forEach(storedTours, storedTour => { + if (storedTour.completed === true) { - angular.forEach(tours, function (tour) { + Utilities.forEach(tours, tour => { if (storedTour.alias === tour.alias) { tour.completed = true; } }); } if (storedTour.disabled === true) { - angular.forEach(tours, function (tour) { + Utilities.forEach(tours, tour => { if (storedTour.alias === tour.alias) { tour.disabled = true; } @@ -296,7 +299,7 @@ getCurrentTour: getCurrentTour, getGroupedTours: getGroupedTours, getTourByAlias: getTourByAlias, - getToursForDoctype : getToursForDoctype + getToursForDoctype: getToursForDoctype }; return service; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index 3c9846fc43..0d6216f7cc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -47,7 +47,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS /** Internal method to track expanded paths on a tree */ _trackExpandedPaths: function (node, expandedPaths) { - if (!node.children || !angular.isArray(node.children) || node.children.length == 0) { + if (!node.children || !Utilities.isArray(node.children) || node.children.length == 0) { return; } @@ -174,7 +174,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS //we determine this based on the server variables if (Umbraco.Sys.ServerVariables.umbracoPlugins && Umbraco.Sys.ServerVariables.umbracoPlugins.trees && - angular.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { + Utilities.isArray(Umbraco.Sys.ServerVariables.umbracoPlugins.trees)) { var found = _.find(Umbraco.Sys.ServerVariables.umbracoPlugins.trees, function (item) { return invariantEquals(item.alias, treeAlias); @@ -473,7 +473,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS for (var i = 0; i < treeNode.children.length; i++) { var child = treeNode.children[i]; - if (child.children && angular.isArray(child.children) && child.children.length > 0) { + if (child.children && Utilities.isArray(child.children) && child.children.length > 0) { //recurse found = this.getDescendantNode(child, id); if (found) { @@ -773,7 +773,7 @@ function treeService($q, treeResource, iconHelper, notificationsService, eventsS if (!args.path) { throw "No path defined on args object for syncTree"; } - if (!angular.isArray(args.path)) { + if (!Utilities.isArray(args.path)) { throw "Path must be an array"; } if (args.path.length < 1) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index d9aef1ddba..109fff0919 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -64,7 +64,7 @@ var saveModel = _.pick(displayModel, 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', - 'key', 'parentId', 'alias', 'path', 'allowCultureVariant', 'isElement'); + 'key', 'parentId', 'alias', 'path', 'allowCultureVariant', 'allowSegmentVariant', 'isElement'); // TODO: Map these saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); @@ -83,7 +83,7 @@ }); var saveProperties = _.map(realProperties, function (p) { - var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant'); + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant'); return saveProperty; }); @@ -158,7 +158,7 @@ var currGroups = saveModel.userGroups; var formattedGroups = []; for (var i = 0; i < currGroups.length; i++) { - if (!angular.isString(currGroups[i])) { + if (!Utilities.isString(currGroups[i])) { formattedGroups.push(currGroups[i].alias); } else { @@ -229,7 +229,7 @@ var currSections = saveModel.sections; var formattedSections = []; for (var i = 0; i < currSections.length; i++) { - if (!angular.isString(currSections[i])) { + if (!Utilities.isString(currSections[i])) { formattedSections.push(currSections[i].alias); } else { @@ -242,7 +242,7 @@ var currUsers = saveModel.users; var formattedUsers = []; for (var j = 0; j < currUsers.length; j++) { - if (!angular.isNumber(currUsers[j])) { + if (!Utilities.isNumber(currUsers[j])) { formattedUsers.push(currUsers[j].id); } else { @@ -367,6 +367,7 @@ name: v.name || "", //if its null/empty,we must pass up an empty string else we get json converter errors properties: getContentProperties(v.tabs), culture: v.language ? v.language.culture : null, + segment: v.segment, publish: v.publish, save: v.save, releaseDate: v.releaseDate, @@ -393,38 +394,59 @@ */ formatContentGetData: function(displayModel) { - //We need to check for invariant properties among the variant variants. - //When we detect this, we want to make sure that the property object instance is the - //same reference object between all variants instead of a copy (which it will be when - //return from the JSON structure). + // We need to check for invariant properties among the variant variants, + // as the value of an invariant property is shared between different variants. + // A property can be culture invariant, segment invariant, or both. + // When we detect this, we want to make sure that the property object instance is the + // same reference object between all variants instead of a copy (which it will be when + // return from the JSON structure). if (displayModel.variants && displayModel.variants.length > 1) { + // Collect all invariant properties from the variants that are either the + // default language variant or the default segment variant. + var defaultVariants = _.filter(displayModel.variants, function (variant) { + var isDefaultLanguage = variant.language && variant.language.isDefault; + var isDefaultSegment = variant.segment == null; - var invariantProperties = []; - - //collect all invariant properties on the first first variant - var firstVariant = displayModel.variants[0]; - _.each(firstVariant.tabs, function(tab, tabIndex) { - _.each(tab.properties, function (property, propIndex) { - //in theory if there's more than 1 variant, that means they would all have a language - //but we'll do our safety checks anyways here - if (firstVariant.language && !property.culture) { - invariantProperties.push({ - tabIndex: tabIndex, - propIndex: propIndex, - property: property - }); - } - }); + return isDefaultLanguage || isDefaultSegment; }); + if (defaultVariants.length > 0) { + _.each(defaultVariants, function (defaultVariant) { + var invariantProps = []; - //now assign this same invariant property instance to the same index of the other variants property array - for (var j = 1; j < displayModel.variants.length; j++) { - var variant = displayModel.variants[j]; + _.each(defaultVariant.tabs, function (tab, tabIndex) { + _.each(tab.properties, function (property, propIndex) { + // culture == null -> property is culture invariant + // segment == null -> property is *possibly* segment invariant + if (!property.culture || !property.segment) { + invariantProps.push({ + tabIndex: tabIndex, + propIndex: propIndex, + property: property + }); + } + }); + }); - _.each(invariantProperties, function (invProp) { - variant.tabs[invProp.tabIndex].properties[invProp.propIndex] = invProp.property; + var otherVariants = _.filter(displayModel.variants, function (variant) { + return variant !== defaultVariant; + }); + + // now assign this same invariant property instance to the same index of the other variants property array + _.each(otherVariants, function (variant) { + _.each(invariantProps, function (invProp) { + var tab = variant.tabs[invProp.tabIndex]; + var prop = tab.properties[invProp.propIndex]; + + var inheritsCulture = prop.culture === invProp.property.culture && prop.segment == null && invProp.property.segment == null; + var inheritsSegment = prop.segment === invProp.property.segment && !prop.culture; + + if (inheritsCulture || inheritsSegment) { + tab.properties[invProp.propIndex] = invProp.property; + } + }); + }); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 0d766dc7d8..4cbc5e567a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -44,7 +44,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe */ dictionaryToQueryString: function (queryStrings) { - if (angular.isArray(queryStrings)) { + if (Utilities.isArray(queryStrings)) { return _.map(queryStrings, function (item) { var key = null; var val = null; @@ -59,7 +59,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe return encodeURIComponent(key) + "=" + encodeURIComponent(val); }).join("&"); } - else if (angular.isObject(queryStrings)) { + else if (Utilities.isObject(queryStrings)) { //this allows for a normal object to be passed in (ie. a dictionary) return decodeURIComponent($.param(queryStrings)); @@ -91,7 +91,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe } return Umbraco.Sys.ServerVariables["umbracoUrls"][apiName] + actionName + - (!queryStrings ? "" : "?" + (angular.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings))); + (!queryStrings ? "" : "?" + (Utilities.isString(queryStrings) ? queryStrings : this.dictionaryToQueryString(queryStrings))); }, @@ -129,7 +129,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe var err = { //NOTE: the default error message here should never be used based on the above docs! - errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'), + errorMsg: (Utilities.isString(opts) ? opts : 'An error occurred!'), data: data, status: status }; @@ -252,12 +252,13 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe for (var f in args.files) { //each item has a property alias and the file object, we'll ensure that the alias is suffixed to the key // so we know which property it belongs to on the server side - var fileKey = "file_" + args.files[f].alias + "_" + (args.files[f].culture ? args.files[f].culture : ""); + var file = args.files[f]; + var fileKey = "file_" + file.alias + "_" + (file.culture ? file.culture : "") + "_" + (file.segment ? file.segment : ""); - if (angular.isArray(args.files[f].metaData) && args.files[f].metaData.length > 0) { - fileKey += ("_" + args.files[f].metaData.join("_")); + if (Utilities.isArray(file.metaData) && file.metaData.length > 0) { + fileKey += ("_" + file.metaData.join("_")); } - formData.append(fileKey, args.files[f].file); + formData.append(fileKey, file.file); } }).then(function (response) { //success callback @@ -322,7 +323,7 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe //validate input, jsonData can be an array of key/value pairs or just one key/value pair. if (!jsonData) { throw "jsonData cannot be null"; } - if (angular.isArray(jsonData)) { + if (Utilities.isArray(jsonData)) { _.each(jsonData, function (item) { if (!item.key || !item.value) { throw "jsonData array item must have both a key and a value property"; } }); @@ -340,13 +341,13 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe transformRequest: function(data) { var formData = new FormData(); //add the json data - if (angular.isArray(data)) { + if (Utilities.isArray(data)) { _.each(data, function(item) { - formData.append(item.key, !angular.isString(item.value) ? angular.toJson(item.value) : item.value); + formData.append(item.key, !Utilities.isString(item.value) ? Utilities.toJson(item.value) : item.value); }); } else { - formData.append(data.key, !angular.isString(data.value) ? angular.toJson(data.value) : data.value); + formData.append(data.key, !Utilities.isString(data.value) ? Utilities.toJson(data.value) : data.value); } //call the callback diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index afd7b606e7..de6fbaf782 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -128,7 +128,7 @@ angular.module('umbraco.services') function setUserTimeoutInternal(newTimeout) { var asNumber = parseFloat(newTimeout); - if (!isNaN(asNumber) && currentUser && angular.isNumber(asNumber)) { + if (!isNaN(asNumber) && currentUser && Utilities.isNumber(asNumber)) { currentUser.remainingAuthSeconds = newTimeout; lastServerTimeoutSet = new Date(); } @@ -185,7 +185,19 @@ angular.module('umbraco.services') authenticate: function (login, password) { return authResource.performLogin(login, password) - .then(this.setAuthenticationSuccessful); + .then(function(data) { + + // Check if user has a start node set. + if(data.startContentIds.length === 0 && data.startMediaIds.length === 0){ + var errorMsg = "User has no start-nodes"; + var result = { errorMsg: errorMsg, user: data, authenticated: false, lastUserId: lastUserId, loginType: "credentials" }; + eventsService.emit("app.notAuthenticated", result); + throw result; + } + + return data; + + }).then(this.setAuthenticationSuccessful); }, setAuthenticationSuccessful: function (data) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index a7ff9def21..82353df744 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -206,11 +206,11 @@ function umbSessionStorage($window) { return { get: function (key) { - return angular.fromJson(storage["umb_" + key]); + return Utilities.fromJson(storage["umb_" + key]); }, set: function (key, value) { - storage["umb_" + key] = angular.toJson(value); + storage["umb_" + key] = Utilities.toJson(value); } }; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index d5c5166d21..ce824f63c0 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -34,7 +34,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', else { const introTourShown = localStorageService.get("introTourShown"); - if(!introTourShown){ + if (!introTourShown) { // Go & show email marketing tour (ONLY when intro tour is completed or been dismissed) tourService.getTourByAlias("umbEmailMarketing").then(function (emailMarketingTour) { // Only show the email marketing tour one time - dismissing it or saying no will make sure it never appears again @@ -45,7 +45,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', // Only show the email tour once per logged in session // The localstorage key is removed on logout or user session timeout const emailMarketingTourShown = localStorageService.get("emailMarketingTourShown"); - if(!emailMarketingTourShown){ + if (!emailMarketingTourShown) { tourService.startTour(emailMarketingTour); localStorageService.set("emailMarketingTourShown", true); } @@ -89,7 +89,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', currentRouteParams = toRetain; } else { - currentRouteParams = angular.copy(current.params); + currentRouteParams = Utilities.copy(current.params); } @@ -183,7 +183,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', currentRouteParams = toRetain; } else { - currentRouteParams = angular.copy(next.params); + currentRouteParams = Utilities.copy(next.params); } //always clear the 'sr' query string (soft redirect) if it exists @@ -191,7 +191,7 @@ app.run(['$rootScope', '$route', '$location', 'urlHelper', 'navigationService', currentRouteParams.sr = null; $route.updateParams(currentRouteParams); } - + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 6c021203a5..0a2ac07ab1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -107,6 +107,7 @@ @import "components/overlays.less"; @import "components/card.less"; @import "components/editor/umb-editor.less"; +@import "components/editor/umb-variant-switcher.less"; @import "components/umb-sub-views.less"; @import "components/umb-editor-navigation.less"; @import "components/umb-editor-navigation-item.less"; @@ -137,6 +138,7 @@ @import "components/tooltip/umb-tooltip-list.less"; @import "components/overlays/umb-overlay-backdrop.less"; @import "components/overlays/umb-itempicker.less"; +@import "components/overlays/umb-variant-selector-overlay"; @import "components/umb-grid.less"; @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less index 52ff2c2b01..03153973ff 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less @@ -31,6 +31,6 @@ border: none; } -.umb-dashboard__header .umb-tabs-nav .umb-tab > a { +.umb-dashboard__header .umb-tabs-nav .umb-tab > .umb-tab-button { padding-bottom: 25px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index 112182fa88..017468fa0c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -2,20 +2,28 @@ Library of card related compoents, like the right-hand icon list on the grid "cards" */ -.umb-card{ +.umb-card { position: relative; padding: 5px 10px 5px 10px; background: @white; width: 100%; - .title{padding: 12px; color: @gray-3; border-bottom: 1px solid @gray-8; font-weight: 400; font-size: 16px; text-transform: none; margin: 0 -10px 10px -10px;} + .title { + padding: 12px; + color: @gray-3; + border-bottom: 1px solid @gray-8; + font-weight: 400; + font-size: 16px; + text-transform: none; + margin: 0 -10px 10px -10px; + } } -.umb-card-thumb{ +.umb-card-thumb { text-align: center; - i{ + i { text-align: center; font-size: 20px; line-height: 40px; @@ -25,18 +33,28 @@ } } -.umb-card-content{ - .item-title{color: @blackLight; font-weight: 400; border: none; font-size: 16px; text-transform: none; margin-bottom: 3px;} - p{color: @gray-3; margin-bottom: 1px;} +.umb-card-content { + .item-title { + color: @blackLight; + font-weight: 400; + border: none; + font-size: 16px; + text-transform: none; + margin-bottom: 3px; + } + p { + color: @gray-3; + margin-bottom: 1px; + } } -.umb-card-actions{ +.umb-card-actions { padding-top: 10px; border-top: @gray-10 1px solid; clear: both; } -.umb-card-icons{ +.umb-card-icons { text-align: center; vertical-align: middle; display: block; @@ -45,7 +63,7 @@ padding: 0; } -.umb-card-icons.vertical{ +.umb-card-icons.vertical { position: absolute; top: 7px; right: 7px; @@ -53,19 +71,19 @@ width: 1px; } -.umb-card-icons li{ +.umb-card-icons li { display: inline-block; margin: 0 2px 0 2px; } -.umb-card-icons.vertical li{ +.umb-card-icons.vertical li { float: right; display: block; margin-bottom: 3px; } //card iocn list -.umb-card-list{ +.umb-card-list { display: block; padding: 0; margin: 0; @@ -81,7 +99,7 @@ //Card icon grid for picking items off a card -.umb-card-grid{ +.umb-card-grid { padding: 0; margin: 0 auto; list-style: none; @@ -101,14 +119,24 @@ width: 100px; } +.umb-card-grid.-six-in-row li { + flex: 0 0 25%; + max-width: 117px; +} + .umb-card-grid.-four-in-row li { flex: 0 0 25%; max-width: 25%; } .umb-card-grid.-three-in-row li { - flex: 0 0 33.33%; - max-width:33.33%; + flex: 0 0 33.333%; + max-width:33.333%; + + i { + font-size: 36px; + line-height: 28px; + } } .umb-card-grid .umb-card-grid-item { @@ -117,7 +145,7 @@ width: 100%; //height: 100%; padding-top: 100%; - border-radius: 3px; + border-radius: @baseBorderRadius * 2; transition: background-color 120ms; > span { @@ -153,6 +181,41 @@ } } +.umb-card-grid .umb-card-grid-item-slot { + position: relative; + display: block; + width: 100%; + padding-top: 100%; + border-radius: @baseBorderRadius * 2; + + box-sizing: border-box; + transition: background-color 120ms; + + &:hover, &:focus { + background-color: @ui-option-hover; + > span { + color:@ui-action-discreet-type-hover; + border-color:@ui-action-discreet-border-hover; + } + } + + > span { + position: absolute; + top: 10px; + bottom: 10px; + left: 10px; + right: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: transparent; + border:1.5px dashed @ui-action-discreet-border; + border-radius: @baseBorderRadius * 2; + } +} + + .umb-card-grid a { color: @ui-option-type; text-decoration: none; @@ -161,6 +224,7 @@ .umb-card-grid i { font-size: 30px; line-height: 20px; + margin-top: 6px; margin-bottom: 10px; display: block; } @@ -176,7 +240,7 @@ //Round icon-like button - this should be somewhere else -.umb-btn-round{ +.umb-btn-round { padding: 4px 6px 4px 6px; display: inline-block; cursor: pointer; @@ -186,7 +250,8 @@ margin: 2px; } -.umb-btn-round:hover, .umb-btn-round:hover *{ +.umb-btn-round:hover, +.umb-btn-round:hover * { background: @blueDark !important; color: @white !important; border-color: @blueDark !important; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index bc84b0d35e..ac55c6ffb1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -162,161 +162,6 @@ a.umb-editor-header__close-split-view:hover { } } -/* variant switcher */ -.umb-variant-switcher__toggle { - position: relative; - display: flex; - align-items: center; - padding: 0 10px; - margin: 1px 1px; - right: 0; - height: 30px; - text-decoration: none !important; - font-size: 13px; - color: @ui-action-discreet-type; - background: transparent; - border: none; - - max-width: 50%; - white-space: nowrap; - - user-select: none; - - span { - text-overflow: ellipsis; - overflow: hidden; - } -} - -button.umb-variant-switcher__toggle { - transition: color 0.2s ease-in-out; - &:hover { - //background-color: @gray-10; - color: @ui-action-discreet-type-hover; - .umb-variant-switcher__expand { - color: @ui-action-discreet-type-hover; - } - } - - &.--error { - &::before { - content: '!'; - position: absolute; - top: -8px; - right: -10px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 10px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - } - } -} - -.umb-variant-switcher__expand { - color: @ui-action-discreet-type; - margin-top: 3px; - margin-left: 5px; - margin-right: -5px; - transition: color 0.2s ease-in-out; -} - -.umb-variant-switcher__item { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid @gray-9; - position: relative; -} - -.umb-variant-switcher__item:last-child { - border-bottom: none; -} - -.umb-variant-switcher__item.--current { - color: @ui-light-active-type; -} -.umb-variant-switcher__item.--current .umb-variant-switcher__name-wrapper { - border-left: 4px solid @ui-active; -} - -.umb-variant-switcher__item:hover { - outline: none; -} - -.umb-variant-switcher__item.--not-allowed:not(.--current) .umb-variant-switcher__name-wrapper:hover { - //background-color: @white !important; - cursor: default; -} - -.umb-variant-switcher__item:hover .umb-variant-switcher__split-view { - display: block; - cursor: pointer; -} - -.umb-variant-switcher__item.--error { - .umb-variant-switcher__name { - color: @red; - &::after { - content: '!'; - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 5px; - top: -3px; - width: 14px; - height: 14px; - border-radius: 7px; - font-size: 8px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - } - } -} - -.umb-variant-switcher__name-wrapper { - font-size: 14px; - flex: 1; - cursor: pointer; - padding-top: 6px !important; - padding-bottom: 6px !important; - background-color: transparent; - border: none; - border-left: 2px solid transparent; -} - -.umb-variant-switcher__name { - display: block; -} - -.umb-variant-switcher__state { - font-size: 13px; - color: @gray-4; -} - -.umb-variant-switcher__split-view { - font-size: 13px; - display: none; - padding: 16px 20px; - position: absolute; - right: 0; - top: 0; - bottom: 0; - background-color: @white; - - &:hover { - background-color: @ui-option-hover; - color: @ui-option-type-hover; - } -} // container diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 4ebfa94b6f..3c4a037b0b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -5,7 +5,7 @@ border-right: 5px solid @brownGrayLight; display: flex; justify-content: space-between; - margin: -10px -5px 10px; + margin: -10px -1px 10px; position: relative; top: 0; box-sizing: border-box; @@ -34,6 +34,7 @@ transition: box-shadow 240ms; position:sticky; z-index: 30; + width: calc(100% + 2px); &.umb-sticky-bar--active { box-shadow: 0 6px 3px -3px rgba(0,0,0,.16); diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less index d2a3bdedb1..e81df77772 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less @@ -1,6 +1,7 @@ .umb-editors { .absolute(); overflow: hidden; + z-index: 7500; .umb-editor { box-shadow: 0px 0 30px 0 rgba(0,0,0,.3); @@ -104,4 +105,4 @@ i { margin-right:5px; } -} \ No newline at end of file +} 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 new file mode 100644 index 0000000000..8dbc070856 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -0,0 +1,332 @@ +/* variant switcher */ +.umb-variant-switcher__toggle { + position: relative; + display: flex; + align-items: center; + padding: 0 10px; + margin: 1px 1px; + right: 0; + height: 30px; + text-decoration: none !important; + font-size: 13px; + color: @ui-action-discreet-type; + background: transparent; + border: none; + + max-width: 50%; + white-space: nowrap; + + user-select: none; + + span { + text-overflow: ellipsis; + overflow: hidden; + } +} + +button.umb-variant-switcher__toggle { + transition: color 0.2s ease-in-out; + &:hover { + //background-color: @gray-10; + color: @ui-action-discreet-type-hover; + .umb-variant-switcher__expand { + color: @ui-action-discreet-type-hover; + } + } + + &.--error { + &::before { + content: '!'; + position: absolute; + top: -8px; + right: -10px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 10px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__expand { + color: @ui-action-discreet-type; + margin-top: 3px; + margin-left: 5px; + margin-right: -5px; + transition: color 0.2s ease-in-out; +} + + +.umb-variant-switcher { + min-width: 100%; + max-height: 80vh; + overflow-y: auto; + margin-top: 5px; + user-select: none; +} + +.umb-variant-switcher__item { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid @gray-9; + position: relative; + .umb-variant-switcher__name-wrapper:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +.umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::before { + content: "+"; + display: block; + float: left; + font-size: 15px; + font-weight: 900; + padding: 8px 16px 8px 6px; + color: @gray-5; + } + .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper::before { + padding: 8px 16px 8px 20px; + } + .umb-variant-switcher__name { + color: @gray-5; + } + .umb-variant-switcher__state { + color: @gray-6; + } + .umb-variant-switcher__name-wrapper::after { + content: ""; + position: absolute; + z-index: 1; + border: 1px dashed @gray-9; + top: 7px; + bottom: 7px; + left: 7px; + right: 7px; + border-radius: 3px; + pointer-events: none; + } + + .umb-variant-switcher__name-wrapper:hover { + &::before { + color: @blueMid; + } + .umb-variant-switcher__name { + color: @blueMid; + } + .umb-variant-switcher__state { + color: @blueMid; + } + } +} +/* +.umb-variant-switcher__item.--state-draft { + .umb-variant-switcher__name { + color: @gray-5; + } + &:hover { + .umb-variant-switcher__name { + color: @blueMid; + } + } +} +*/ + +.umb-variant-switcher.--has-sub-variants { + .umb-variant-switcher__item { + + } +} + +.umb-variant-switcher__item-expand-button { + text-decoration: none; + display: inline-block; + flex: 0; + align-self: stretch; + + padding-left: 22px !important; + padding-right: 14px !important; + + font-size: 12px; + + * { + pointer-events: none; + } +} + +.umb-variant-switcher__item:last-child { + border-bottom: none; +} + +.umb-variant-switcher__item.--current { + //color: @ui-light-active-type; + //background-color: @pinkExtraLight; + .umb-variant-switcher__name { + //color: @ui-light-active-type; + font-weight: 700; + } + &::before { + content: ''; + position: absolute; + border-radius: 0 4px 4px 0; + background-color: @ui-active-border; + width: 4px; + top:8px; + bottom: 8px; + left:0; + z-index:1; + pointer-events: none; + } +} + +.umb-variant-switcher__item:hover { + outline: none; +} + +.umb-variant-switcher__item.--active:not(.--current) .umb-variant-switcher__name-wrapper:hover { + //background-color: @white !important; + cursor: default; +} + +.umb-variant-switcher__item:focus .umb-variant-switcher__split-view, +.umb-variant-switcher__item:focus-within .umb-variant-switcher__split-view, +.umb-variant-switcher__item:hover .umb-variant-switcher__split-view, +.umb-variant-switcher__split-view:focus { + display: block; + cursor: pointer; +} + +.umb-variant-switcher__item.--error { + .umb-variant-switcher__name { + color: @red; + &::after { + content: '!'; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + margin-left: 5px; + top: -3px; + width: 14px; + height: 14px; + border-radius: 7px; + font-size: 8px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + } + } +} + +.umb-variant-switcher__name-wrapper { + font-size: 14px; + text-align: left; + flex: 1; + cursor: pointer; + background-color: transparent; + border: none; +} +.dropdown-menu>li { + > .umb-variant-switcher__name-wrapper { + padding-top: 10px; + padding-bottom: 10px; + } + + > .umb-variant-switcher__item-expand-button + .umb-variant-switcher__name-wrapper { + padding-left: 5px; + } +} + + +.umb-variant-switcher__name { + display: block; + font-weight: 600; + margin-bottom: -2px; +} + +.umb-variant-switcher__state { + font-size: 12px; + color: @gray-4; +} + +.umb-variant-switcher__split-view { + font-size: 12px; + display: none; + padding: 20px 20px; + position: absolute; + right: 0; + top: 0; + bottom: 0; + background-color: @white; + + &:hover { + background-color: @ui-option-hover; + color: @ui-option-type-hover; + } +} + + +.umb-variant-switcher__sub-variants { + + position: relative; + border-bottom: 1px solid @gray-9; + background-color: @gray-13; + /* + &::before { + content: ""; + position: absolute; + z-index: 1; + top: 0px; + left: 20px; + width: 4px; + bottom: 14px; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + background-color: @gray-8; + } + */ + .umb-variant-switcher__item { + border-bottom-color: @gray-10; + } + + .umb-variant-switcher__item.--state-notCreated:not(.--active) { + .umb-variant-switcher__name-wrapper::after { + left: 55px;// overwrite left to achieve same indentation on the dashed border as language. + } + } + + .umb-variant-switcher__name-wrapper { + + margin-left: 48px; + padding-left: 20px; + + padding-top: 10px; + padding-bottom: 10px; + + &:hover { + color: @ui-option-type-hover; + background-color: @ui-option-hover; + } + + .umb-variant-switcher__name { + //margin-right: 20px; + } + .umb-variant-switcher__state { + //flex: 0 0 200px; + } + + + } +} 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 bbd866a5fd..26a4abc146 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -2,7 +2,7 @@ position: fixed; overflow: hidden; background: @white; - z-index: @zindexUmbOverlay; + z-index: 7501; animation: fadeIn 0.2s; box-shadow: 0 10px 50px rgba(0,0,0,0.1), 0 6px 20px rgba(0,0,0,0.16); text-align: left; @@ -24,16 +24,19 @@ margin-top: 0; flex-grow: 0; flex-shrink: 0; - padding: 20px 20px 0; + padding: 30px 30px 0; } .umb-overlay__section-header { width: 100%; margin-top:30px; - margin-bottom: 10px; + margin-bottom: 20px; h5 { display: inline; + font-size: 16px; + line-height: 16px; + font-weight: bold; } button { @@ -66,7 +69,7 @@ flex-shrink: 1; flex-basis: auto; position: relative; - padding: 30px; + padding: 20px 30px; background: @white; max-height: calc(100vh - 170px); overflow-y: auto; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less index 3727c92251..010ff4636d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-itempicker.less @@ -1,6 +1,3 @@ .umb-itempicker .form-search { margin-top:10px; } -.umb-card-grid { - margin-top: 10px; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less new file mode 100644 index 0000000000..b50a622f98 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-variant-selector-overlay.less @@ -0,0 +1,26 @@ +.umb-variant-selector-overlay { + + + .umb-variant-selector-entry { + .umb-form-check { + .umb-form-check__symbol { + margin-top: 2px; + } + } + } + .umb-variant-selector-entry__title { + font-weight: 600; + font-size: 14px; + .__secondarytitle { + font-weight: normal; + color: @gray-5; + } + } + .umb-variant-selector-entry__description { + display: block; + font-size: 12px; + color: @gray-4; + } + + +} 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 2cca776614..f3c41dbc33 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 @@ -178,11 +178,11 @@ } // Validation -.umb-sub-views-nav-item__action.-has-error, +.show-validation .umb-sub-views-nav-item__action.-has-error, .show-validation .umb-sub-views-nav-item > a.-has-error { color: @red; - &::after { + &::before { background-color: @red; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index f9d8772d45..ac7a2c63ce 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -23,6 +23,7 @@ } .umb-group-builder__group.-placeholder { + width:100%; min-height: 86px; display: flex; justify-content: center; @@ -136,9 +137,10 @@ input.umb-group-builder__group-title-input:disabled:hover { } .umb-group-builder__group-add-property { - min-height: 46px; - margin-right: 45px; + + width: calc(100% - 315px); margin-left: 270px; + min-height: 46px; border-radius: 3px; display: flex; @@ -344,8 +346,9 @@ input.umb-group-builder__group-title-input:disabled:hover { .umb-group-builder__property-actions { flex: 0 0 44px; - text-align: right; - margin-top: 7px; + display: flex; + align-items: center; + justify-content: flex-end; } .umb-group-builder__property-action { @@ -473,7 +476,7 @@ input.umb-group-builder__group-sort-value { font-weight: bold; resize: none; line-height: 1.5em; - padding-left: 0; + padding: 0; border: none; &:focus { @@ -498,45 +501,82 @@ input.umb-group-builder__group-sort-value { text-decoration: none; color: @ui-action-type-hover; border-color: @ui-action-border-hover; + background-color: @ui-action-discreet-hover; } } - .editor { + .editor-wrapper { margin-bottom: 10px; + } - .editor-icon-wrapper { - border: 1px solid @gray-8; - width: 60px; - height: 60px; - text-align: center; - line-height: 60px; - border-radius: 5px; - float: left; - margin-right: 20px; + .editor { + display: flex; + align-items: center; + align-content: stretch; - .icon { - font-size: 26px; - } + min-height: 80px; + + border: 1px solid @gray-9; + color: @ui-action-discreet-type; + border-radius: @baseBorderRadius; + + } + + .editor-info { + flex: 1 0 auto; + text-align: left; + display: flex; + align-items: center; + max-width: calc(100% - 48px); + min-height: 80px; + color: @ui-action-discreet-type; + + &:hover { + color: @ui-action-discreet-type-hover; + background-color: @ui-action-discreet-hover; + } + } + + .editor-icon-wrapper { + width: 60px; + height: 60px; + text-align: center; + line-height: 60px; + flex: 0 0 60px; + padding-left: 10px; + + .icon { + font-size: 32px; + } + } + + .editor-details { + flex: 1 1 auto; + margin-top: 10px; + margin-bottom: 10px; + + .editor-name { + display: block; + font-weight: bold; } - .editor-details { - float: left; - margin-top: 10px; - - .editor-name { - display: block; - font-weight: bold; - } - - .editor-editor { - display: block; - font-size: 12px; - } + .editor-editor { + display: block; + font-size: 12px; } + } - .editor-settings-icon { - font-size: 18px; - margin-top: 8px; + .editor-remove-icon { + flex: 0 0 48px; + width: 48px; + height: 48px; + font-size: 18px; + + min-height: 80px; + color: @ui-action-discreet-type; + &:hover { + color: @ui-action-discreet-type-hover; + background-color: @ui-action-discreet-hover; } } @@ -544,6 +584,11 @@ input.umb-group-builder__group-sort-value { margin-bottom: 20px; } + .editor-description { + margin-top: 20px; + padding: 0; + } + .editor-description, .editor-validation-pattern { min-width: 100%; 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 94cfa6f62c..44955e8f8e 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 @@ -1,15 +1,18 @@ .umb-list--condensed { .umb-list-item { - padding-top: 5px; - padding-bottom: 5px; + padding-top: 7px; + padding-bottom: 7px; } } .umb-list-item { - border-bottom: 1px solid @gray-9; + border-bottom: 1px solid @gray-11; padding-top: 15px; padding-bottom: 15px; display: flex; + &:last-of-type { + border-bottom: none; + } } a.umb-list-item:hover, 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 ee784787fa..15b317aa45 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 @@ -13,25 +13,31 @@ top: 1px; } -.umb-tab > a { - +.umb-tab-button { display: flex; justify-content: center; align-items: center; position: relative; - + cursor: pointer; //border-bottom: 4px solid transparent; color: @ui-light-type; padding: 5px 20px 15px 20px; transition: color 150ms ease-in-out; + &:focus { color: @ui-light-type-hover; + + body:not(.tabbing-active) &{ + outline: none; + } } + &:hover { color: @ui-light-type-hover; text-decoration: none; } + &::after { content: ""; height: 0px; @@ -42,12 +48,21 @@ bottom: 0; border-radius: 3px 3px 0 0; opacity: 0; - transition: all .2s linear; + transition: all 0.2s linear; + } + + &--expand > i { + height: 5px; + width: 5px; + border-radius: 50%; + background: @black; + display: inline-block; + margin: 0 5px 0 0; + opacity: 0.6; } } - -.umb-tab--active > a { +.umb-tab--active > .umb-tab-button { color: @ui-light-active-type; //border-bottom-color: @ui-active; /* @@ -64,19 +79,19 @@ } } -.show-validation .umb-tab--error > a, -.show-validation .umb-tab--error > a:hover, -.show-validation .umb-tab--error > a:focus { - color: @white !important; - background-color: @red !important; - border-color: @errorBorder; +.show-validation .umb-tab--error > .umb-tab-button, +.show-validation .umb-tab--error > .umb-tab-button:hover, +.show-validation .umb-tab--error > .umb-tab-button:focus { + color: @white !important; + background-color: @red !important; + border-color: @errorBorder; } -.show-validation .umb-tab--error a:before { - content: "\e25d"; - font-family: 'icomoon'; - margin-right: 5px; - vertical-align: top; +.show-validation .umb-tab--error .umb-tab-button:before { + content: "\e25d"; + font-family: "icomoon"; + margin-right: 5px; + vertical-align: top; } // tabs tray @@ -86,20 +101,10 @@ left: auto; } -.umb-tabs-tray > a { +.umb-tabs-tray > .umb-tab-button { cursor: pointer; } .umb-tabs-tray-item--active { border-left: 2px solid @ui-active; } - -.umb-tab--expand > a > i { - height: 5px; - width: 5px; - border-radius: 50%; - background: @black; - display: inline-block; - margin: 0 5px 0 0; - opacity: .6; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less index 9b4bac723b..e5b84fc6ca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less @@ -1,13 +1,17 @@ .umb-validation-label { position: absolute; - top: 27px; - min-width: 100px; - max-width: 200px; - padding: 1px 5px; + z-index: 1; + top: 28px; + min-width: 80px; + max-width: 260px; + padding: 2px 6px; background: @red; color: @white; - font-size: 11px; + font-size: 12px; line-height: 1.5em; + border-radius: @baseBorderRadius; + pointer-events: none; + user-select: none; } .umb-validation-label:after { diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index e36acdc273..818b1d84d1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -123,6 +123,7 @@ position: relative; text-align: right; user-select: none; + margin-left: auto; a { opacity: .5; @@ -134,8 +135,8 @@ .password-text { background-repeat: no-repeat; background-size: 18px; - background-position: left center; - padding-left: 26px; + background-position: 0px 1px; + padding-left: 24px; &.show { background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M16 6C9 6 3 10 0 16c3 6 9 10 16 10s13-4 16-10c-3-6-9-10-16-10zm8 5.3c1.8 1.2 3.4 2.8 4.6 4.7-1.2 2-2.8 3.5-4.7 4.7-3 1.5-6 2.3-8 2.3s-6-.8-8-2.3C6 19.5 4 18 3 16c1.5-2 3-3.5 5-4.7l.6-.2C8 12 8 13 8 14c0 4.5 3.5 8 8 8s8-3.5 8-8c0-1-.3-2-.6-2.6l.4.3zM16 13c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3 3 1.3 3 3z'/%3E%3C/svg%3E"); diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index 8523fe9300..9e951feb1a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -49,7 +49,7 @@ } .date-wrapper-mini--checkbox{ - margin: 0 0 0 26px; + margin: 0 0 0 28px; } .date-wrapper-mini__date { @@ -62,6 +62,10 @@ &:first-of-type { margin-left: 0; } + .flatpickr-input > button:first-of-type { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } } .date-wrapper-mini__date .flatpickr-input > a { diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index c320a31807..26ec1d795a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -83,6 +83,8 @@ @sand-5: #F3ECE8;// added 2019 @sand-6: #F6F1EF;// added 2019 @sand-7: #F9F7F5;// added 2019 +@sand-8: #fbfaf9;// added 2019 +@sand-9: #fdfcfc;// added 2019 // Additional Icon Colours diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 55eb92ec04..d21331f106 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -66,12 +66,14 @@ function MainController($scope, $location, appState, treeService, notificationsS appState.setSearchState("show", false); }; - $scope.showLoginScreen = function(isTimedOut) { + $scope.showLoginScreen = function (isTimedOut) { + $scope.login.pageTitle = $scope.$root.locationTitle; $scope.login.isTimedOut = isTimedOut; $scope.login.show = true; }; - $scope.hideLoginScreen = function() { + $scope.hideLoginScreen = function () { + $scope.$root.locationTitle = $scope.login.pageTitle; $scope.login.show = false; }; @@ -82,6 +84,7 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.authenticated = null; $scope.user = null; const isTimedOut = data && data.isTimedOut ? true : false; + $scope.showLoginScreen(isTimedOut); // Remove the localstorage items for tours shown diff --git a/src/Umbraco.Web.UI.Client/src/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/navigation.controller.js index 281be2d331..a383c2d44a 100644 --- a/src/Umbraco.Web.UI.Client/src/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/navigation.controller.js @@ -68,7 +68,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar args.event.stopPropagation(); args.event.preventDefault(); - if (n.metaData && n.metaData["jsClickCallback"] && angular.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { + if (n.metaData && n.metaData["jsClickCallback"] && Utilities.isString(n.metaData["jsClickCallback"]) && n.metaData["jsClickCallback"] !== "") { //this is a legacy tree node! var jsPrefix = "javascript:"; var js; @@ -142,7 +142,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar var isInit = false; var evts = []; - + //Listen for global state changes evts.push(eventsService.on("appState.globalState.changed", function (e, args) { if (args.key === "showNavigation") { @@ -200,7 +200,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $scope.treeApi.load({ section: $scope.currentSection, customTreeParams: $scope.customTreeParams, cacheKey: $scope.treeCacheKey }); }); } - + //show/hide search results if (args.key === "showSearchResults") { $scope.showSearchResults = args.value; @@ -222,7 +222,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar } else { $location.search("mculture", null); } - + var currentEditorState = editorState.getCurrent(); if (currentEditorState && currentEditorState.path) { $scope.treeApi.syncTree({ path: currentEditorState.path, activate: true }); @@ -233,13 +233,13 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //Emitted when a language is created or an existing one saved/edited evts.push(eventsService.on("editors.languages.languageSaved", function (e, args) { - if(args.isNew){ + if (args.isNew) { //A new language has been created - reload languages for tree loadLanguages().then(function (languages) { $scope.languages = languages; }); } - else if(args.language.isDefault){ + else if (args.language.isDefault) { //A language was saved and was set to be the new default (refresh the tree, so its at the top) loadLanguages().then(function (languages) { $scope.languages = languages; @@ -282,7 +282,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar /** * For multi language sites, this ensures that mculture is set to either the last selected language or the default one - */ + */ function ensureMainCulture() { if ($location.search().mculture) { return; @@ -295,7 +295,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar $timeout(function () { $scope.selectLanguage(language); }); - } + } /** * Based on the current state of the application, this configures the scope variables that control the main tree and language drop down @@ -432,7 +432,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //the nav is ready, let the app know eventsService.emit("app.navigationReady", { treeApi: $scope.treeApi }); - + } }); }); @@ -469,7 +469,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar // add the selected culture to a cookie so the user will log back into the same culture later on (cookie lifetime = one year) var expireDate = new Date(); expireDate.setDate(expireDate.getDate() + 365); - $cookies.put("UMB_MCULTURE", language.culture, {path: "/", expires: expireDate}); + $cookies.put("UMB_MCULTURE", language.culture, { path: "/", expires: expireDate }); // close the language selector $scope.page.languageSelectorIsOpen = false; @@ -495,9 +495,10 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //execute them sequentially // set selected language to active - angular.forEach($scope.languages, function(language){ + Utilities.forEach($scope.languages, language => { language.active = false; }); + language.active = true; angularHelper.executeSequentialPromises(promises); @@ -538,7 +539,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar closeTree(); }; - $scope.onOutsideClick = function() { + $scope.onOutsideClick = function () { closeTree(); }; diff --git a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js index dc40338d01..5ff8dd3633 100644 --- a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js @@ -6,8 +6,8 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.services']) .controller("previewController", function ($scope, $window, $location) { - - $scope.currentCulture = {iso:'', title:'...', icon:'icon-loading'} + + $scope.currentCulture = { iso: '', title: '...', icon: 'icon-loading' } var cultures = []; $scope.tabbingActive = false; @@ -21,7 +21,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi window.addEventListener('mousedown', disableTabbingActive); } } - + function disableTabbingActive(evt) { $scope.tabbingActive = false; $scope.$digest(); @@ -113,10 +113,10 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.sizeOpen = false; $scope.cultureOpen = false; - $scope.toggleSizeOpen = function() { + $scope.toggleSizeOpen = function () { $scope.sizeOpen = toggleMenu($scope.sizeOpen); } - $scope.toggleCultureOpen = function() { + $scope.toggleCultureOpen = function () { $scope.cultureOpen = toggleMenu($scope.cultureOpen); } @@ -132,8 +132,8 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.sizeOpen = false; $scope.cultureOpen = false; } - - $scope.windowClickHandler = function() { + + $scope.windowClickHandler = function () { closeOthers(); } function windowBlurHandler() { @@ -141,16 +141,16 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi $scope.$digest(); } - var win = angular.element($window); + var win = $($window); win.on("blur", windowBlurHandler); - + $scope.$on("$destroy", function () { - win.off("blur", handleBlwindowBlurHandlerur ); + win.off("blur", handleBlwindowBlurHandlerur); }); - - function setPageUrl(){ + + function setPageUrl() { $scope.pageId = $location.search().id || getParameterByName("id"); var culture = $location.search().culture || getParameterByName("culture"); @@ -204,27 +204,27 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi /* Change culture */ /*****************************************************************************/ $scope.changeCulture = function (iso) { - if($location.search().culture !== iso) { + if ($location.search().culture !== iso) { $scope.frameLoaded = false; $scope.currentCultureIso = iso; $location.search("culture", iso); setPageUrl(); } }; - $scope.registerCulture = function(iso, title, isDefault) { - var cultureObject = {iso: iso, title: title, isDefault: isDefault}; + $scope.registerCulture = function (iso, title, isDefault) { + var cultureObject = { iso: iso, title: title, isDefault: isDefault }; cultures.push(cultureObject); } - $scope.$watch("currentCultureIso", function(oldIso, newIso) { + $scope.$watch("currentCultureIso", function (oldIso, newIso) { // if no culture is selected, we will pick the default one: if ($scope.currentCultureIso === null) { - $scope.currentCulture = cultures.find(function(culture) { + $scope.currentCulture = cultures.find(function (culture) { return culture.isDefault === true; }) return; } - $scope.currentCulture = cultures.find(function(culture) { + $scope.currentCulture = cultures.find(function (culture) { return culture.iso === $scope.currentCultureIso; }) }); @@ -252,7 +252,7 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi }); } - function hideUmbracoPreviewBadge (iframe) { + function hideUmbracoPreviewBadge(iframe) { if (iframe && iframe.contentDocument && iframe.contentDocument.getElementById("umbracoPreviewBadge")) { iframe.contentDocument.getElementById("umbracoPreviewBadge").style.display = "none"; } diff --git a/src/Umbraco.Web.UI.Client/src/utilities.js b/src/Umbraco.Web.UI.Client/src/utilities.js index bd12506358..9121ba0e25 100644 --- a/src/Umbraco.Web.UI.Client/src/utilities.js +++ b/src/Umbraco.Web.UI.Client/src/utilities.js @@ -66,6 +66,54 @@ */ const isObject = val => val !== null && typeof val === 'object'; + const isWindow = obj => obj && obj.window === obj; + + const isScope = obj => obj && obj.$evalAsync && obj.$watch; + + const toJsonReplacer = (key, value) => { + var val = value; + if (typeof key === 'string' && key.charAt(0) === '$' && key.charAt(1) === '$') { + val = undefined; + } else if (isWindow(value)) { + val = '$WINDOW'; + } else if (value && window.document === value) { + val = '$DOCUMENT'; + } else if (isScope(value)) { + val = '$SCOPE'; + } + return val; + } + /** + * Equivalent to angular.toJson + */ + const toJson = (obj, pretty) => { + if (isUndefined(obj)) return undefined; + if (!isNumber(pretty)) { + pretty = pretty ? 2 : null; + } + return JSON.stringify(obj, toJsonReplacer, pretty); + } + + /** + * Equivalent to angular.fromJson + */ + const fromJson = (val) => { + if (!isString(val)) { + return val; + } + return JSON.parse(val); + } + + /** + * Not equivalent to angular.forEach. But like the angularJS method this does not fail on null or undefined. + */ + const forEach = (obj, iterator) => { + if (obj) { + return obj.forEach(iterator); + } + return obj; + } + let _utilities = { noop: noop, copy: copy, @@ -77,10 +125,13 @@ isDefined: isDefined, isString: isString, isNumber: isNumber, - isObject: isObject + isObject: isObject, + fromJson: fromJson, + toJson: toJson, + forEach: forEach }; if (typeof (window.Utilities) === 'undefined') { window.Utilities = _utilities; } -})(window); \ No newline at end of file +})(window); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js index 7cfa02f95a..aa0cd54dff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js @@ -17,7 +17,7 @@ /* make a copy of the init model so it is possible to roll back the changes on cancel */ - oldModel = angular.copy($scope.model); + oldModel = Utilities.copy($scope.model); if (!$scope.model.title) { $scope.model.title = "Compositions"; @@ -39,7 +39,7 @@ }); } - + function isSelected(alias) { if ($scope.model.contentType.compositeContentTypes.indexOf(alias) !== -1) { @@ -68,7 +68,7 @@ or the confirm checkbox has been checked */ if (compositionRemoved) { vm.allowSubmit = false; - localizationService.localize("general_remove").then(function(value) { + localizationService.localize("general_remove").then(function (value) { const dialog = { view: "views/common/infiniteeditors/compositions/overlays/confirmremove.html", title: value, diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.controller.js new file mode 100644 index 0000000000..9ee80488af --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.controller.js @@ -0,0 +1,106 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.DataTypeConfigurationPickerController + * @function + * + * @description + * The controller for the content type editor data type configuration picker dialog + */ + +(function() { + "use strict"; + + function DataTypeConfigurationPicker($scope, $filter, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService, editorService) { + + var vm = this; + + vm.configs = []; + + vm.loading = true; + + vm.newDataType = newDataType; + vm.pickDataType = pickDataType; + vm.close = close; + + function activate() { + setTitle(); + load(); + } + + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectEditorConfiguration") + .then(function(data){ + $scope.model.title = data; + }); + } + } + + function load() { + + dataTypeResource.getGroupedDataTypes().then(function(configs) { + + var filteredConfigs = []; + + _.each(configs, function(configGroup) { + for(var i = 0; i + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js index 167e74c25d..d622ccea52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js @@ -13,30 +13,23 @@ function DataTypePicker($scope, $filter, dataTypeResource, contentTypeResource, localizationService, editorService) { var vm = this; - + + vm.showDataTypes = true; + vm.dataTypes = []; + vm.loading = true; + vm.loadingConfigs = false; vm.searchTerm = ""; - vm.showTabs = false; - vm.tabsLoaded = 0; - vm.typesAndEditors = []; - vm.userConfigured = []; - vm.loading = false; - vm.tabs = []; - vm.labels = {}; - - vm.onTabChange = onTabChange; - vm.filterItems = filterItems; - vm.showDetailsOverlay = showDetailsOverlay; - vm.hideDetailsOverlay = hideDetailsOverlay; - vm.pickEditor = pickEditor; + vm.searchResult = null; + + vm.viewOptionsForEditor = viewOptionsForEditor; vm.pickDataType = pickDataType; + vm.pickEditor = pickEditor; vm.close = close; + vm.searchTermChanged = searchTermChanged; function activate() { setTitle(); - loadTabs(); - getGroupedDataTypes(); - getGroupedPropertyEditors(); - + loadTypes(); } function setTitle() { @@ -44,128 +37,104 @@ localizationService.localize("defaultdialogs_selectEditor") .then(function(data){ $scope.model.title = data; - }); + } + ); } } - - function loadTabs() { - - var labels = ["contentTypeEditor_availableEditors", "contentTypeEditor_reuse"]; - - localizationService.localizeMany(labels) - .then(function(data){ - vm.labels.availableDataTypes = data[0]; - vm.labels.reuse = data[1]; - - vm.tabs = [{ - active: true, - id: 1, - label: vm.labels.availableDataTypes, - alias: "Default", - typesAndEditors: [] - }, { - active: false, - id: 2, - label: vm.labels.reuse, - alias: "Reuse", - userConfigured: [] - }]; - - }); - } - - function getGroupedPropertyEditors() { - - vm.loading = true; - - dataTypeResource.getGroupedPropertyEditors().then(function(data) { - vm.tabs[0].typesAndEditors = data; - vm.typesAndEditors = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); - }); - - } - - function getGroupedDataTypes() { - - vm.loading = true; - - dataTypeResource.getGroupedDataTypes().then(function(data) { - vm.tabs[1].userConfigured = data; - vm.userConfigured = data; - vm.tabsLoaded = vm.tabsLoaded + 1; - checkIfTabContentIsLoaded(); - }); - - } - - function checkIfTabContentIsLoaded() { - if (vm.tabsLoaded === 2) { + + function loadTypes() { + + dataTypeResource.getGroupedPropertyEditors().then(function(dataTypes) { + vm.dataTypes = dataTypes; vm.loading = false; - vm.showTabs = true; - } - } - - function onTabChange(selectedTab) { - vm.tabs.forEach(function(tab) { - tab.active = false; }); - selectedTab.active = true; + + } + + function loadConfigurations() { + + vm.loading = true; + vm.loadingConfigs = true; + + dataTypeResource.getGroupedDataTypes().then(function(configs) { + vm.configs = configs; + vm.loading = false; + performeSearch(); + }); + } - function filterItems() { - // clear item details - $scope.model.itemDetails = null; - - if (vm.searchTerm) { - vm.showTabs = false; - - var regex = new RegExp(vm.searchTerm, "i"); - - var userConfigured = filterCollection(vm.userConfigured, regex), - typesAndEditors = filterCollection(vm.typesAndEditors, regex); - - var totalResults = _.reduce(_.pluck(_.union(userConfigured, typesAndEditors), 'count'), (m, n) => m + n, 0); - - vm.filterResult = { - userConfigured: userConfigured, - typesAndEditors: typesAndEditors, - totalResults: totalResults - }; - + + function searchTermChanged() { + + vm.showDataTypes = (vm.searchTerm === ""); + + if(vm.loadingConfigs !== true) { + loadConfigurations() } else { - vm.filterResult = null; - vm.showTabs = true; + performeSearch(); } + } - + + function performeSearch() { + + if (vm.searchTerm) { + if (vm.configs) { + + var regex = new RegExp(vm.searchTerm, "i"); + vm.searchResult = { + configs: filterCollection(vm.configs, regex), + dataTypes: filterCollection(vm.dataTypes, regex) + }; + } + } else { + vm.searchResult = null; + } + + } + function filterCollection(collection, regex) { return _.map(_.keys(collection), function (key) { - - var filteredDataTypes = $filter('filter')(collection[key], function (dataType) { - return regex.test(dataType.name) || regex.test(dataType.alias); - }); - return { group: key, - count: filteredDataTypes.length, - dataTypes: filteredDataTypes + entries: $filter('filter')(collection[key], function (dataType) { + return regex.test(dataType.name) || regex.test(dataType.alias); + }) } }); } - function showDetailsOverlay(property) { + + function viewOptionsForEditor(editor) { + + var dataTypeConfigurationPicker = { + editor: editor, + property: $scope.model.property, + contentTypeName: $scope.model.contentTypeName, + view: "views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html", + size: "small", + submit: function(dataType, propertyType, isNew) { + submit(dataType, propertyType, isNew); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; - var propertyDetails = {}; - propertyDetails.icon = property.icon; - propertyDetails.title = property.name; + editorService.open(dataTypeConfigurationPicker); - $scope.model.itemDetails = propertyDetails; } - function hideDetailsOverlay() { - $scope.model.itemDetails = null; + function pickDataType(selectedDataType) { + selectedDataType.loading = true; + dataTypeResource.getById(selectedDataType.id).then(function(dataType) { + contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { + selectedDataType.loading = false; + submit(dataType, propertyType, false); + }); + }); } function pickEditor(propertyEditor) { @@ -188,16 +157,7 @@ }; editorService.open(dataTypeSettings); - } - function pickDataType(selectedDataType) { - selectedDataType.loading = true; - dataTypeResource.getById(selectedDataType.id).then(function(dataType) { - contentTypeResource.getPropertyTypeScaffold(dataType.id).then(function(propertyType) { - selectedDataType.loading = false; - submit(dataType, propertyType, false); - }); - }); } function submit(dataType, propertyType, isNew) { @@ -213,9 +173,9 @@ $scope.model.submit($scope.model); } - + function close() { - if ($scope.model.close) { + if($scope.model.close) { $scope.model.close(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html index 534fdc5648..768f8a8c24 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html @@ -15,127 +15,78 @@ -
- + - -
- - - -
-
-
{{key}}
-
    -
  • - - - - {{ systemDataType.name }} - - -
  • -
-
-
-
-
-
{{key}}
-
    -
  • -
    -
    -
    - - - - {{ dataType.name }} - - -
  • -
-
-
-
+
+
+
{{key | umbCmsTitleCase}}
+ +
- -
-
-
-
-
-
{{result.group}}
-
-
-
-
-
-
{{result.group}}
-
- - - -
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index 8acaa544c1..428905020b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -21,7 +21,7 @@ -
+
using this editor will get updated with the new settings.
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 47607b7f0b..a9803b70f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", function ($scope, eventsService, entityResource, mediaResource, mediaHelper, udiParser, userService, localizationService, editorService) { - + var vm = this; var dialogOptions = $scope.model; @@ -16,7 +16,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (!$scope.model.title) { localizationService.localize("defaultdialogs_selectLink") - .then(function(value) { + .then(function (value) { $scope.model.title = value; }); } @@ -59,7 +59,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", if (dialogOptions.currentTarget) { // clone the current target so we don't accidentally update the caller's model while manipulating $scope.model.target - $scope.model.target = angular.copy(dialogOptions.currentTarget); + $scope.model.target = Utilities.copy(dialogOptions.currentTarget); // if we have a node ID, we fetch the current node to build the form data if ($scope.model.target.id || $scope.model.target.udi) { @@ -194,7 +194,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", tree: "content" }); }, - close: function() { + close: function () { editorService.close(); } }; @@ -251,13 +251,13 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", } function close() { - if($scope.model && $scope.model.close) { + if ($scope.model && $scope.model.close) { $scope.model.close(); } } function submit() { - if($scope.model && $scope.model.submit) { + if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js index dfc77f786c..ba577045aa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.controller.js @@ -8,9 +8,9 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi $scope.noMacroParams = false; function onInit() { - if(!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMacro").then(function(value){ - $scope.model.title = value; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMacro").then(function (value) { + $scope.model.title = value; }); } } @@ -26,8 +26,8 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi } }; - $scope.close = function() { - if($scope.model.close) { + $scope.close = function () { + if ($scope.model.close) { $scope.model.close(); } } @@ -42,7 +42,7 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi .then(function (data) { //go to next page if there are params otherwise we can just exit - if (!angular.isArray(data) || data.length === 0) { + if (!Utilities.isArray(data) || data.length === 0) { if (insertIfNoParameters) { $scope.model.submit($scope.model); @@ -51,7 +51,7 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi } } else { - + $scope.wizardStep = "paramSelect"; $scope.model.macroParams = data; @@ -70,8 +70,8 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi //detect if it is a json string if (val.detectIsJson()) { try { - //Parse it to json - prop.value = angular.fromJson(val); + //Parse it from json + prop.value = Utilities.fromJson(val); } catch (e) { // not json @@ -104,13 +104,13 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi entityResource.getAll("Macro", ($scope.model.dialogData && $scope.model.dialogData.richTextEditor && $scope.model.dialogData.richTextEditor === true) ? "UseInEditor=true" : null) .then(function (data) { - if (angular.isArray(data) && data.length == 0) { + if (Utilities.isArray(data) && data.length == 0) { $scope.nomacros = true; } //if 'allowedMacros' is specified, we need to filter - if (angular.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { - $scope.macros = _.filter(data, function(d) { + if (Utilities.isArray($scope.model.dialogData.allowedMacros) && $scope.model.dialogData.allowedMacros.length > 0) { + $scope.macros = _.filter(data, function (d) { return _.contains($scope.model.dialogData.allowedMacros, d.alias); }); } 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 6728486a0d..7a4db7cbc5 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,13 +1,13 @@ //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) { + function ($scope, $timeout, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService, umbSessionStorage) { var vm = this; - + vm.submit = submit; vm.close = close; - + vm.toggle = toggle; vm.upload = upload; vm.dragLeave = dragLeave; @@ -26,7 +26,7 @@ angular.module("umbraco") vm.shouldShowUrl = shouldShowUrl; var dialogOptions = $scope.model; - + $scope.disableFolderSelect = (dialogOptions.disableFolderSelect && dialogOptions.disableFolderSelect !== "0") ? true : false; $scope.disableFocalPoint = (dialogOptions.disableFocalPoint && dialogOptions.disableFocalPoint !== "0") ? true : false; $scope.onlyImages = (dialogOptions.onlyImages && dialogOptions.onlyImages !== "0") ? true : false; @@ -133,21 +133,21 @@ angular.module("umbraco") // media object so we need to look it up var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; var altText = $scope.target.altText; - + // ID of a UDI or legacy int ID still could be null/undefinied here // As user may dragged in an image that has not been saved to media section yet if (id) { entityResource.getById(id, "Media") - .then(function (node) { - $scope.target = node; - if (ensureWithinStartNode(node)) { - selectMedia(node); - $scope.target.url = mediaHelper.resolveFileFromEntity(node); - $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); - $scope.target.altText = altText; - openDetailsDialog(); - } - }, gotoStartNode); + .then(function (node) { + $scope.target = node; + if (ensureWithinStartNode(node)) { + selectMedia(node); + $scope.target.url = mediaHelper.resolveFileFromEntity(node); + $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); + $scope.target.altText = altText; + openDetailsDialog(); + } + }, gotoStartNode); } else { // No ID set - then this is going to be a tmpimg that has not been uploaded // User editing this will want to be changing the ALT text @@ -156,15 +156,15 @@ angular.module("umbraco") } } - function upload(v) { - angular.element(".umb-file-dropzone .file-select").trigger("click"); + function upload() { + $(".umb-file-dropzone .file-select").trigger("click"); } - function dragLeave(el, event) { + function dragLeave() { $scope.activeDrag = false; } - function dragEnter(el, event) { + function dragEnter() { $scope.activeDrag = true; } @@ -240,16 +240,16 @@ angular.module("umbraco") } } else { if ($scope.showDetails) { - + $scope.target = media; - + // handle both entity and full media object if (media.image) { $scope.target.url = media.image; } else { $scope.target.url = mediaHelper.resolveFile(media); } - + openDetailsDialog(); } else { selectMedia(media); @@ -301,7 +301,7 @@ angular.module("umbraco") $timeout(function () { if ($scope.multiPicker) { var images = _.rest($scope.images, $scope.images.length - files.length); - _.each(images, function(image) { + _.each(images, function (image) { selectMedia(image); }); } else { @@ -341,7 +341,7 @@ angular.module("umbraco") return false; } - function gotoStartNode(err) { + function gotoStartNode() { gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); } @@ -373,7 +373,7 @@ angular.module("umbraco") if (vm.searchOptions.filter) { searchMedia(); } else { - + // reset pagination vm.searchOptions = { pageNumber: 1, @@ -383,7 +383,7 @@ angular.module("umbraco") filter: '', dataTypeKey: dataTypeKey }; - + getChildren($scope.currentFolder.id); } }); @@ -411,9 +411,9 @@ angular.module("umbraco") entityResource.getPagedDescendants($scope.filterOptions.excludeSubFolders ? $scope.currentFolder.id : $scope.startNodeId, "Media", vm.searchOptions) .then(function (data) { // update image data to work with image grid - angular.forEach(data.items, function (mediaItem) { - setMediaMetaData(mediaItem); - }); + if (data.items) { + data.items.forEach(mediaItem => setMediaMetaData(mediaItem)); + } // update images $scope.images = data.items ? data.items : []; @@ -497,7 +497,7 @@ angular.module("umbraco") var folderImage = $scope.images[folderIndex]; var imageIsSelected = false; - if ($scope.model && angular.isArray($scope.model.selection)) { + if ($scope.model && Utilities.isArray($scope.model.selection)) { for (var selectedIndex = 0; selectedIndex < $scope.model.selection.length; selectedIndex++) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js index 9640f2eba2..a6d1383640 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.controller.js @@ -30,6 +30,7 @@ vm.close = close; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.toggleValidation = toggleValidation; vm.toggleShowOnMemberProfile = toggleShowOnMemberProfile; vm.toggleMemberCanEdit = toggleMemberCanEdit; @@ -114,7 +115,7 @@ property: $scope.model.property, contentTypeName: $scope.model.contentTypeName, view: "views/common/infiniteeditors/datatypepicker/datatypepicker.html", - size: "small", + size: "medium", submit: function(model) { $scope.model.updateSameDataTypes = model.updateSameDataTypes; @@ -248,6 +249,10 @@ $scope.model.property.allowCultureVariant = toggleValue($scope.model.property.allowCultureVariant); } + function toggleAllowSegmentVariants() { + $scope.model.property.allowSegmentVariant = toggleValue($scope.model.property.allowSegmentVariant); + } + function toggleValidation() { $scope.model.property.validation.mandatory = toggleValue($scope.model.property.validation.mandatory); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 77ee276e3e..1b28e84607 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -18,8 +18,8 @@
-
-
+
+
- +
- + - - - -
- - - - - -
- {{ model.property.dataTypeName }} - {{ model.property.editor }} + +
+ Required editor
- - - - - + + +
+ + + +
- +
- +
@@ -132,19 +139,29 @@ ng-keypress="vm.submitOnEnter($event)" />
-
- -
- - - +
+ +
+ +
- -
+
+ +
+ + + + +
+ +
@@ -177,7 +194,7 @@ checked="model.property.isSensitiveData" on-click="vm.toggleIsSensitiveData()"> - +
@@ -207,7 +224,7 @@ label-key="general_submit" action="vm.submit(model)"> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html index 9b230410b0..e292a94606 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.html @@ -9,9 +9,9 @@ hide-icon="true" hide-description="true"> - + - + @@ -21,21 +21,21 @@
-
- +
- +

{{vm.currentVersion.name}} (Created: {{vm.currentVersion.createDate}})

- +
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html index e1eb5e454a..0772dea148 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/edit.html @@ -10,6 +10,7 @@ page="page" content="content" culture="culture" + segment="segment" on-select-app="appChanged(app)" on-select-app-anchor="appAnchorChanged(app, anchor)" on-back="onBack()" @@ -61,9 +62,9 @@ shortcut="ctrl+s" add-ellipsis="{{page.saveButtonEllipsis}}"> - - - +
- @@ -79,7 +79,7 @@
- {{ item.comment }} - +
@@ -126,7 +126,7 @@
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html index 9cdafd82b1..d534cd77ed 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-tabbed-content.html @@ -11,13 +11,13 @@ data-element="property-{{property.alias}}" ng-repeat="property in group.properties track by property.alias" property="property" - show-inherit="content.variants.length > 1 && !property.culture && !activeVariant.language.isDefault" - inherits-from="defaultVariant.language.name"> + show-inherit="propertyEditorDisabled(property)" + inherits-from="defaultVariant.displayName"> -
+
+ preview="propertyEditorDisabled(property)">
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html index 0e9e7d8eab..247453c604 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-variant-content-editors.html @@ -1,7 +1,6 @@ 
+ ng-repeat="editor in vm.editors track by editor.compositeId"> - + - +
@@ -34,14 +34,14 @@
-
- +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 8496aab80c..7e2cb4f012 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -36,26 +36,52 @@ required aria-required="true" aria-invalid="{{contentForm.headerNameForm.headerName.$invalid ? true : false}}" - autocomplete="off" maxlength="255" /> + autocomplete="off" + maxlength="255" /> - - - {{vm.currentVariant.language.name}} + + - - - -
Open in split view
+ +
Open in split view
+
+ + +
Open in split view
+
+
@@ -70,10 +96,10 @@
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html index 5735f8462b..d19992cb5a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html @@ -1,6 +1,6 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js index 2a3f67a7e3..66747a8697 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.controller.js @@ -7,35 +7,62 @@ //if we make the viewModel the variant itself, we end up with a circular reference in the models which isn't ideal // (i.e. variant.apps[contentApp].viewModel = variant) //so instead since we already have access to the content, we can just get the variant directly by the index. + + var unbindLanguageWatcher = function() {}; + var unbindSegmentWatcher = function() {}; + var timeout = null; var vm = this; vm.loading = true; function onInit() { - //get the variant by index (see notes above) - vm.content = $scope.content.variants[$scope.model.viewModel]; + serverValidationManager.notify(); vm.loading = false; + timeout = null;// ensure timeout is set to null, so we know that its not running anymore. //if this variant has a culture/language assigned, then we need to watch it since it will change //if the language drop down changes and we need to re-init - if (vm.content.language) { - $scope.$watch(function () { - return vm.content.language.culture; + if ($scope.variantContent) { + if ($scope.variantContent.language) { + unbindLanguageWatcher = $scope.$watch(function () { + return $scope.variantContent.language.culture; + }, function (newVal, oldVal) { + if (newVal !== oldVal) { + requestUpdate(); + } + }); + } + + unbindSegmentWatcher = $scope.$watch(function () { + return $scope.variantContent.segment; }, function (newVal, oldVal) { if (newVal !== oldVal) { - vm.loading = true; - - // TODO: Can we minimize the flicker? - $timeout(function () { - onInit(); - }, 100); + requestUpdate(); } }); } + + } + + function requestUpdate() { + if (timeout === null) { + vm.loading = true; + + // TODO: Can we minimize the flicker? + timeout = $timeout(function () { + onInit(); + }, 100); + } } onInit(); + + $scope.$on("$destroy", function() { + unbindLanguageWatcher(); + unbindSegmentWatcher(); + $timeout.cancel(timeout); + }); } angular.module("umbraco").controller("Umbraco.Editors.Content.Apps.ContentController", ContentAppContentController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html index d391f2cd95..7023a4187a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/apps/content/content.html @@ -2,7 +2,8 @@ + content-node-model="content" + content="variantContent"> - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 1a1254fcf1..fc7d38fd37 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -64,6 +64,9 @@ function contentCreateController($scope, language as what is selected in the tree */ .search("cculture", mainCulture) /* when we create a new node we must make sure that any previously + opened segments is reset */ + .search("csegment", null) + /* when we create a new node we must make sure that any previously used blueprint is reset */ .search("blueprintId", null); close(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index ad79bf01d1..2c7c53e31f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -28,6 +28,7 @@ function ContentEditController($scope, $routeParams, contentResource) { $scope.isNew = infiniteMode ? $scope.model.create : $routeParams.create; //load the default culture selected in the main tree if any $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + $scope.segment = $routeParams.csegment ? $routeParams.csegment : null; //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change @@ -36,6 +37,7 @@ function ContentEditController($scope, $routeParams, contentResource) { //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.segment = next.params.csegment ? next.params.csegment : null; $scope.isNew = next.params.create === "true"; $scope.contentId = infiniteMode ? $scope.model.id : $routeParams.id; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index ddd9dd35ec..94e8c25b89 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -8,6 +8,7 @@ tree-alias="content" is-new="isNew" culture="culture" + segment="segment" infinite-model="model">
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js index 8caa38ea17..decf05be5f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.controller.js @@ -5,193 +5,171 @@ var vm = this; vm.loading = true; - vm.hasPristineVariants = false; vm.isNew = true; vm.changeSelection = changeSelection; - vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; - /** Returns true if publishing is possible based on if there are un-published mandatory languages */ + /** Returns true if publish meets the requirements of mandatory languages */ function canPublish() { - var possible = false; + var hasSomethingToPublish = false; + for (var i = 0; i < vm.variants.length; i++) { var variant = vm.variants[i]; - var state = canVariantPublish(variant); - if (state === true) { - possible = true; - } - if (state === false) { + + // if varaint is mandatory and not already published: + if (variant.publish === false && notPublishedMandatoryFilter(variant)) { return false; } + if (variant.publish === true) { + hasSomethingToPublish = true; + } + } - return possible; - } - - /** Returns true if publishing is possible based on if the variant is a un-published mandatory language */ - function canVariantPublish(variant) { - - //if this variant will show up in the publish-able list - var publishable = dirtyVariantFilter(variant); - var published = !(variant.state === "NotCreated" || variant.state === "Draft"); - - // is this variant mandatory: - if (variant.language.isMandatory && !published && !variant.publish) { - //if a mandatory variant isn't published or set to be published - //then we cannot continue - - return false; - } - - // is this variant selected for publish: - if (variant.publish === true) { - return publishable; - } - - return null; + return hasSomethingToPublish; } function changeSelection(variant) { - + // update submit button state: $scope.model.disableSubmitButton = !canPublish(); - //need to set the Save state to true if publish is true + //need to set the Save state to same as publish. variant.save = variant.publish; - - variant.willPublish = canVariantPublish(variant); } - function dirtyVariantFilter(variant) { - //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's - // * the active one - // * it's editor is in a $dirty state - // * it has pending saves - // * it is unpublished - // * it is in NotCreated state - return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges" || variant.state === "NotCreated"); - } - function hasAnyData(variant) { + function hasAnyDataFilter(variant) { if (variant.name == null || variant.name.length === 0) { return false; } - var result = variant.isDirty != null; - - if(result) return true; + if(variant.isDirty === true) { + return true; + } for (var t=0; t < variant.tabs.length; t++){ for (var p=0; p < variant.tabs[t].properties.length; p++){ - var property = variant.tabs[t].properties[p]; - - if(property.culture == null) continue; - - result = result || (property.value != null && property.value.length > 0); - - if(result) return true; + if (property.value != null && property.value.length > 0) { + return true; + } } } - return result; + return false; } - function pristineVariantFilter(variant) { - return !(dirtyVariantFilter(variant)); + function dirtyVariantFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * it's editor is in a $dirty state + // * it has pending saves + // * it is unpublished + return (variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); + } + + function publishableVariantFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * variant is active + // * it's editor is in a $dirty state + // * it has pending saves + // * it is unpublished + return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); + } + + function notPublishedMandatoryFilter(variant) { + return variant.state !== "Published" && isMandatoryFilter(variant); + } + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); + } + function notPublishableButMandatoryFilter(variant) { + //determine a variant is needed, but not already a choice. + // * publishable — aka. displayed as a publish option. + // * published — its already published and everything is then fine. + // * mandatory — this is needed, and thats why we highlight it. + return !publishableVariantFilter(variant) && variant.state !== "Published" && variant.isMandatory === true; } function onInit() { - - vm.variants = $scope.model.variants; - if (!$scope.model.title) { - localizationService.localize("content_readyToPublish").then(function (value) { - $scope.model.title = value; - }); - } + _.each(vm.variants, (variant) => { + + // reset to not be published + variant.publish = false; + variant.save = false; + + variant.isMandatory = isMandatoryFilter(variant); - vm.hasPristineVariants = false; - - _.each(vm.variants, - function (variant) { - if(variant.state === "NotCreated") { - vm.isNew = true; - } + // If we have a variant thats not in the state of NotCreated, then we know we have adata and its not a new content node. + if(variant.state !== "NotCreated") { + vm.isNew = false; } - ); - - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - - // reset to not be published - variant.publish = false; - variant.save = false; - - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); - } - - // If the variant havent been created jet. - if(variant.state === "NotCreated") { - // If the variant is mandatory, then set the variant to be published. - if (variant.language.isMandatory === true) { - variant.publish = true; - variant.save = true; - } - } - - variant.canPublish = dirtyVariantFilter(variant); - - // if we have data on this variant. - if(variant.canPublish && hasAnyData(variant)) { - // and if some varaints havent been saved before, or they dont have a publishing date set, then we set it for publishing. - if(vm.isNew || variant.publishDate == null){ - variant.publish = true; - variant.save = true; - } - } - - variant.willPublish = canVariantPublish(variant); - } - ); - - if (vm.variants.length !== 0) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; - }); - - var active = _.find(vm.variants, function (v) { - return v.active; - }); - - if (active) { - //ensure that the current one is selected - active.publish = true; - active.save = true; - } - - $scope.model.disableSubmitButton = !canPublish(); - - } else { - //disable Publish button if we have nothing to publish, should not happen - $scope.model.disableSubmitButton = true; - } - - var labelKey = vm.isNew ? "content_languagesToPublishForFirstTime" : "content_languagesToPublish"; - - localizationService.localize(labelKey).then(function (value) { - vm.headline = value; - vm.loading = false; }); + _.each(vm.variants, (variant) => { + + // if this is a new node and we have data on this variant. + if(vm.isNew === true && hasAnyDataFilter(variant)) { + variant.save = true; + } + + }); + vm.availableVariants = vm.variants.filter(publishableVariantFilter); + vm.missingMandatoryVariants = vm.variants.filter(notPublishableButMandatoryFilter); + + // if any active varaiant that is available for publish, we set it to be published: + _.each(vm.availableVariants, (v) => { + if(v.active) { + v.save = v.publish = true; + } + }); + + if (vm.availableVariants.length !== 0) { + vm.availableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; + }); + } + + + $scope.model.disableSubmitButton = !canPublish(); + + if (vm.missingMandatoryVariants.length > 0) { + localizationService.localize("content_notReadyToPublish").then(function (value) { + $scope.model.title = value; + vm.loading = false; + }); + } else { + if (!$scope.model.title) { + localizationService.localize("content_readyToPublish").then(function (value) { + $scope.model.title = value; + vm.loading = false; + }); + } else { + vm.loading = false; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html index 80c8cfd485..4cf4444cfe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publish.html @@ -1,67 +1,83 @@ -
- -
-

{{vm.headline}}

-
+
-
+
+ +
+

+
+ ng-repeat="variant in vm.availableVariants track by variant.compositeId"> -
+
-
- - - - - - + server-validation-field="{{variant.htmlId}}" + > + + + * + + + + — {{variant.language.name}} + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + - - {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} - + - -
-
+
+
+

+
-
-
-

+
+
+

-
-
-
- {{variant.language.name}} - * -
+
+
+ + + * + + + + — {{variant.language.name}} + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + -
- -
- -
-
{{notification.message}}
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js index 0ca2fe65c9..095c4f3fe1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.controller.js @@ -5,12 +5,16 @@ var vm = this; + vm.includeUnpublished = false; + vm.changeSelection = changeSelection; + vm.toggleIncludeUnpublished = toggleIncludeUnpublished; + function onInit() { - vm.includeUnpublished = false; vm.variants = $scope.model.variants; + vm.displayVariants = vm.variants.slice(0);// shallow copy, we dont want to share the array-object(because we will be performing a sort method) but each entry should be shared (because we need validation and notifications). vm.labels = {}; if (!$scope.model.title) { @@ -19,17 +23,30 @@ }); } - _.each(vm.variants, - function (variant) { - variant.compositeId = (variant.language ? variant.language.culture : "inv") + "_" + (variant.segment ? variant.segment : ""); - variant.htmlId = "_content_variant_" + variant.compositeId; - }); + _.each(vm.variants, function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + }); if (vm.variants.length > 1) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + vm.displayVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); var active = _.find(vm.variants, function (v) { @@ -48,14 +65,17 @@ // localize help text for invariant content vm.labels.help = { "key": "content_publishDescendantsHelp", - "tokens": [] + "tokens": [vm.variants[0].name] }; - // add the node name as a token so it will show up in the translated text - vm.labels.help.tokens.push(vm.variants[0].name); } } + function toggleIncludeUnpublished() { + console.log("toggleIncludeUnpublished") + vm.includeUnpublished = !vm.includeUnpublished; + } + /** Returns true if publishing is possible based on if there are un-published mandatory languages */ function canPublish() { var selected = []; @@ -64,7 +84,7 @@ var published = !(variant.state === "NotCreated" || variant.state === "Draft"); - if (variant.language.isMandatory && !published && !variant.publish) { + if (variant.segment == null && variant.language && variant.language.isMandatory && !published && !variant.publish) { //if a mandatory variant isn't published //and not flagged for saving //then we cannot continue @@ -86,6 +106,14 @@ variant.save = variant.publish; } + + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); + } + //when this dialog is closed, reset all 'publish' flags $scope.$on('$destroy', function () { for (var i = 0; i < vm.variants.length; i++) { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html index 0bf434f9a4..92373d00ff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html @@ -1,67 +1,69 @@ -
+
-
+

- +
-
+

- -
- -
- +
-
+
-
+
- - -
- - - - - - - - - {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} - - - + > + + + * -
-
+ + + — {{variant.language.name}} + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js index aacea9fe0e..2d40005618 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/save.controller.js @@ -9,8 +9,6 @@ vm.isNew = true; vm.changeSelection = changeSelection; - vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; function changeSelection(variant) { var firstSelected = _.find(vm.variants, function (v) { @@ -19,16 +17,18 @@ $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } - function dirtyVariantFilter(variant) { + function saveableVariantFilter(variant) { //determine a variant is 'dirty' (meaning it will show up as save-able) if it's // * the active one // * it's editor is in a $dirty state - // * it is in NotCreated state return (variant.active || variant.isDirty); } - function pristineVariantFilter(variant) { - return !(dirtyVariantFilter(variant)); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } function hasAnyData(variant) { @@ -57,6 +57,7 @@ function onInit() { vm.variants = $scope.model.variants; + vm.availableVariants = vm.variants.filter(saveableVariantFilter); if(!$scope.model.title) { localizationService.localize("content_readyToSave").then(function(value){ @@ -64,10 +65,15 @@ }); } - vm.hasPristineVariants = false; - _.each(vm.variants, function (variant) { + + //reset state: + variant.save = false; + variant.publish = false; + + variant.isMandatory = isMandatoryFilter(variant); + if(variant.state !== "NotCreated"){ vm.isNew = false; } @@ -75,34 +81,40 @@ _.each(vm.variants, function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); - } - if(vm.isNew && hasAnyData(variant)){ variant.save = true; } }); if (vm.variants.length !== 0) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + + _.find(vm.variants, function (v) { + if(v.active) { + //ensure that the current one is selected + v.save = true; + } }); - var active = _.find(vm.variants, function (v) { - return v.active; + vm.availableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); - if (active) { - //ensure that the current one is selected - active.save = true; - } - } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; 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 552f6003b0..dd6e96df95 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 @@ -1,4 +1,4 @@ -
+
@@ -7,17 +7,16 @@

- - +

+ ng-repeat="variant in vm.availableVariants track by variant.compositeId"> -
+
+ > + + + * + + + + — {{variant.language.name}} + * + + + + - + + + + {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} + + -
- - - - - + - - {{saveVariantSelectorForm.saveVariantSelector.errorMsg}} - - - -

- -
-
-

- - -

-
- -
-
-
- {{ variant.language.name }} - * -
- -
- -
- -
-
{{notification.message}}
-
-
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js index 5aa7eff1ef..e7f31ac707 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.controller.js @@ -12,7 +12,6 @@ vm.clearPublishDate = clearPublishDate; vm.clearUnpublishDate = clearUnpublishDate; vm.dirtyVariantFilter = dirtyVariantFilter; - vm.pristineVariantFilter = pristineVariantFilter; vm.changeSelection = changeSelection; vm.firstSelectedDates = {}; @@ -24,7 +23,7 @@ function onInit() { vm.variants = $scope.model.variants; - vm.hasPristineVariants = false; + vm.displayVariants = vm.variants.slice(0);// shallow copy, we dont want to share the array-object(because we will be performing a sort method) but each entry should be shared (because we need validation and notifications). for (let i = 0; i < vm.variants.length; i++) { origDates.push({ @@ -38,36 +37,42 @@ $scope.model.title = value; }); } + + _.each(vm.variants, function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + + }); // Check for variants: if a node is invariant it will still have the default language in variants // so we have to check for length > 1 if (vm.variants.length > 1) { - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - - //check for pristine variants - if (!vm.hasPristineVariants) { - vm.hasPristineVariants = pristineVariantFilter(variant); + vm.displayVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; } - }); - - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); - var active = _.find(vm.variants, function (v) { - return v.active; + _.each(vm.variants, function (v) { + if (v.active) { + v.save = true; + } }); - - if (active) { - //ensure that the current one is selected - active.save = true; - } - + $scope.model.disableSubmitButton = !canSchedule(); } @@ -127,6 +132,7 @@ * @param {any} type publish or unpublish */ function datePickerChange(variant, dateStr, type) { + console.log("datePickerChange", variant, dateStr, type) if (type === 'publish') { setPublishDate(variant, dateStr); } else if (type === 'unpublish') { @@ -241,6 +247,7 @@ * @param {any} variant */ function clearPublishDate(variant) { + console.log("clearPublishDate", variant, variant.releaseDate) if(variant && variant.releaseDate) { variant.releaseDate = null; // we don't have a publish date anymore so we can clear the min date for unpublish @@ -256,6 +263,7 @@ * @param {any} variant */ function clearUnpublishDate(variant) { + console.log("clearUnpublishDate", variant) if(variant && variant.expireDate) { variant.expireDate = null; // we don't have a unpublish date anymore so we can clear the max date for publish @@ -297,8 +305,11 @@ return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges" || variant.state === "NotCreated"); } - function pristineVariantFilter(variant) { - return !(dirtyVariantFilter(variant)); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } /** Returns true if publishing is possible based on if there are un-published mandatory languages */ @@ -321,7 +332,7 @@ return true; } - var isMandatory = variant.language && variant.language.isMandatory; + var isMandatory = variant.segment == null && variant.language && variant.language.isMandatory; //if this variant will show up in the publish-able list var publishable = dirtyVariantFilter(variant); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index 4f4e3c2874..8f515a10e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -1,4 +1,4 @@ -
+
@@ -84,28 +84,39 @@
-
+
-
+
+ server-validation-field="{{variant.htmlId}}" + > + + + * + + + + — {{variant.language.name}} + * + + + + - + +
- - - - - +
-
+
Publish:  {{variant.releaseDateFormatted}}
@@ -117,7 +128,7 @@ on-open="vm.datePickerShow(variant, 'publish')" on-close="vm.datePickerClose(variant, 'publish')">
- @@ -126,9 +137,9 @@
- +
@@ -143,7 +154,7 @@ on-open="vm.datePickerShow(variant, 'unpublish')" on-close="vm.datePickerClose(variant, 'unpublish')">
- @@ -152,7 +163,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js index c85e8d7013..f425dce4a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.controller.js @@ -6,8 +6,6 @@ var vm = this; vm.loading = true; - vm.modifiedVariantFilter = modifiedVariantFilter; - vm.unmodifiedVariantFilter = unmodifiedVariantFilter; vm.changeSelection = changeSelection; function onInit() { @@ -21,28 +19,40 @@ }); } + _.each(vm.variants, function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + }); + + vm.availableVariants = vm.variants.filter(publishableVariantFilter); + + if (vm.availableVariants.length !== 0) { - if (vm.variants.length !== 0) { - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - }); - - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + vm.availableVariants = vm.availableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); - var active = _.find(vm.variants, function (v) { - return v.active; + _.each(vm.availableVariants, function (v) { + if(v.active) { + v.save = true; + } }); - if (active) { - //ensure that the current one is selected - active.save = true; - } - } else { //disable save button if we have nothing to save $scope.model.disableSubmitButton = true; @@ -59,20 +69,20 @@ $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected } - function modifiedVariantFilter(variant) { - //determine a variant is 'modified' (meaning it will show up as able to send for approval) - // * it's editor is in a $dirty state - // * it is in Draft state - // * it is published with pending changes - return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } - function unmodifiedVariantFilter(variant) { - //determine a variant is 'unmodified' (meaning it will NOT show up as able to send for approval) + function publishableVariantFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * variant is active // * it's editor is in a $dirty state - // * it has been published - // * it is not created for that specific language - return (variant.state === "Published" && !variant.isDirty && !variant.active || variant.state === "NotCreated" && !variant.isDirty && !variant.active); + // * it has pending saves + // * it is unpublished + return (variant.active || variant.isDirty || variant.state === "Draft" || variant.state === "PublishedPendingChanges"); } //when this dialog is closed, reset all 'save' flags diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html index ae8cd87484..91fa1799fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/sendtopublish.html @@ -1,4 +1,4 @@ -
+

@@ -10,31 +10,36 @@
-
+
-
+
- - -
- + > + + + * + + + + — {{variant.language.name}} + * + + - - + - + - - - {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + - -
+
@@ -42,20 +47,4 @@
-
-
-

-
- -
-
-
- {{ variant.language.name }} - * -
- -
-
-
-
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 3011bf49ee..3db76ee49f 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 @@ -7,12 +7,11 @@ var autoSelectedVariants = []; vm.changeSelection = changeSelection; - vm.publishedVariantFilter = publishedVariantFilter; - vm.unpublishedVariantFilter = unpublishedVariantFilter; function onInit() { vm.variants = $scope.model.variants; + vm.unpublishableVariants = vm.variants.filter(publishedVariantFilter) // set dialog title if (!$scope.model.title) { @@ -21,24 +20,38 @@ }); } - _.each(vm.variants, - function (variant) { - variant.compositeId = contentEditingHelper.buildCompositeVariantId(variant); - variant.htmlId = "_content_variant_" + variant.compositeId; - }); + _.each(vm.variants, function (variant) { + variant.isMandatory = isMandatoryFilter(variant); + }); // node has variants if (vm.variants.length !== 1) { - //now sort it so that the current one is at the top - vm.variants = _.sortBy(vm.variants, function (v) { - return v.active ? 0 : 1; + + vm.unpublishableVariants.sort(function (a, b) { + if (a.language && b.language) { + if (a.language.name > b.language.name) { + return -1; + } + if (a.language.name < b.language.name) { + return 1; + } + } + if (a.segment && b.segment) { + if (a.segment > b.segment) { + return -1; + } + if (a.segment < b.segment) { + return 1; + } + } + return 0; }); var active = _.find(vm.variants, function (v) { return v.active; }); - if (active) { + if (active && publishedVariantFilter(active)) { //ensure that the current one is selected active.save = true; } @@ -51,21 +64,15 @@ function changeSelection(selectedVariant) { - // disable submit button if nothing is selected - var firstSelected = _.find(vm.variants, function (v) { - return v.save; - }); - $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected - - // if a mandatory variant is selected we want to select all other variants + // if a mandatory variant is selected we want to select all other variants, we cant have anything published if a mandatory variants gets unpublished. // and disable selection for the others - if(selectedVariant.save && selectedVariant.language.isMandatory) { + if(selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { - angular.forEach(vm.variants, function(variant){ - if(!variant.save && publishedVariantFilter(variant)) { + vm.variants.forEach(function(variant) { + if(!variant.save) { // keep track of the variants we automaically select // so we can remove the selection again - autoSelectedVariants.push(variant.language.culture); + autoSelectedVariants.push(variant); variant.save = true; } variant.disabled = true; @@ -79,12 +86,12 @@ // if a mandatory variant is deselected we want to deselet all the variants // 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.language.isMandatory) { + if(!selectedVariant.save && selectedVariant.segment == null && selectedVariant.language && selectedVariant.language.isMandatory) { - angular.forEach(vm.variants, function(variant){ + vm.variants.forEach( function(variant){ // check if variant was auto selected, then deselect - if(_.contains(autoSelectedVariants, variant.language.culture)) { + if(_.contains(autoSelectedVariants, variant)) { variant.save = false; }; @@ -93,6 +100,19 @@ autoSelectedVariants = []; } + // disable submit button if nothing is selected + var firstSelected = _.find(vm.variants, function (v) { + return v.save; + }); + $scope.model.disableSubmitButton = !firstSelected; //disable submit button if there is none selected + + } + + function isMandatoryFilter(variant) { + //determine a variant is 'dirty' (meaning it will show up as publish-able) if it's + // * has a mandatory language + // * without having a segment, segments cant be mandatory at current state of code. + return (variant.language && variant.language.isMandatory === true && variant.segment == null); } function publishedVariantFilter(variant) { @@ -102,13 +122,6 @@ return (variant.state === "Published" || variant.state === "PublishedPendingChanges"); } - function unpublishedVariantFilter(variant) { - //determine a variant is 'modified' (meaning it will NOT show up as able to unpublish) - // * it's editor is in a $dirty state - // * it is published with pending changes - return (variant.state !== "Published" && variant.state !== "PublishedPendingChanges"); - } - //when this dialog is closed, remove all unpublish and disabled flags $scope.$on('$destroy', function () { for (var i = 0; i < vm.variants.length; i++) { 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 e384823cc3..94dd9fdac5 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 @@ -1,4 +1,4 @@ -
+
@@ -10,12 +10,12 @@

- +
- -
+ +
-
+
- -
- - - - + > + + + * -
+ + + — {{variant.language.name}} + * + + + + - + + + + {{publishVariantSelectorForm.publishVariantSelector.errorMsg}} + +
+ + +
- +

- -
-
-

-
- -
-
-
- {{ variant.language.name }} - * -
- -
- -
-
-
-
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js index 982af76d69..15112650c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.controller.js @@ -44,6 +44,7 @@ function ContentBlueprintEditController($scope, $routeParams, contentResource) { //load the default culture selected in the main tree if any $scope.culture = $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture; + $scope.segment = $routeParams.csegment ? $routeParams.csegment : null; //Bind to $routeUpdate which will execute anytime a location changes but the route is not triggered. //This is so we can listen to changes on the cculture parameter since that will not cause a route change @@ -52,6 +53,7 @@ function ContentBlueprintEditController($scope, $routeParams, contentResource) { //will not cause a route change and so we can update the isNew and contentId flags accordingly. $scope.$on('$routeUpdate', function (event, next) { $scope.culture = next.params.cculture ? next.params.cculture : $routeParams.mculture; + $scope.segment = next.params.csegment ? next.params.csegment : null; $scope.isNew = $routeParams.id === "-1"; $scope.contentId = $routeParams.id; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html index 191dd4db5c..751bbe5970 100644 --- a/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/contentblueprints/edit.html @@ -5,6 +5,7 @@ get-scaffold-method="getScaffoldMethod" is-new="isNew" tree-alias="contentblueprints" - culture="culture"> + culture="culture" + segment="segment">
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js index 8631b09a45..991a8132cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/healthcheck.controller.js @@ -1,4 +1,4 @@ -(function() { +(function () { "use strict"; function HealthCheckController($scope, healthCheckResource) { @@ -22,7 +22,7 @@ // Get a (grouped) list of all health checks healthCheckResource.getAllChecks() - .then(function(response) { + .then(function (response) { vm.groups = response; }); @@ -33,11 +33,11 @@ var totalInfo = 0; // count total number of statusses - angular.forEach(group.checks, - function(check) { - angular.forEach(check.status, - function(status) { - switch (status.resultType) { + Utilities.forEach(group.checks, check => { + + if (check.status) { + check.status.forEach(status => { + switch (status.resultType) { case SUCCESS: totalSuccess = totalSuccess + 1; break; @@ -50,9 +50,10 @@ case INFO: totalInfo = totalInfo + 1; break; - } - }); - }); + } + }); + } + }); group.totalSuccess = totalSuccess; group.totalError = totalError; @@ -66,7 +67,7 @@ check.loading = true; check.status = null; healthCheckResource.getStatus(check.id) - .then(function(response) { + .then(function (response) { check.loading = false; check.status = response; }); @@ -75,7 +76,7 @@ function executeAction(check, index, action) { check.loading = true; healthCheckResource.executeAction(action) - .then(function(response) { + .then(function (response) { check.status[index] = response; check.loading = false; }); @@ -94,13 +95,12 @@ group.checkCounter = 0; group.loading = true; - angular.forEach(checks, - function(check) { - + if (checks) { + checks.forEach(check => { check.loading = true; healthCheckResource.getStatus(check.id) - .then(function(response) { + .then(function (response) { check.status = response; group.checkCounter = group.checkCounter + 1; check.loading = false; @@ -112,6 +112,7 @@ } }); }); + } } function openGroup(group) { diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js index e1334aa816..732aa898a7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/create.controller.js @@ -97,7 +97,7 @@ function DocumentTypesCreateController($scope, $location, navigationService, con $scope.error = err; //show any notifications - if (angular.isArray(err.data.notifications)) { + if (Utilities.isArray(err.data.notifications)) { for (var i = 0; i < err.data.notifications.length; i++) { notificationsService.showNotification(err.data.notifications[i]); } diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 21eb77b1e9..42dba5f580 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -48,7 +48,7 @@ "shortcuts_navigateSections", "shortcuts_addGroup", "shortcuts_addProperty", - "shortcuts_addEditor", + "defaultdialogs_selectEditor", "shortcuts_editDataType", "shortcuts_toggleListView", "shortcuts_toggleAllowAsRoot", diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js index dd591090f9..da8dee8e03 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.controller.js @@ -20,12 +20,14 @@ vm.selectedChildren = []; vm.overlayTitle = ""; + vm.showAllowSegmentationOption = Umbraco.Sys.ServerVariables.umbracoSettings.showAllowSegmentationForDocumentTypes || false; vm.addChild = addChild; vm.removeChild = removeChild; vm.sortChildren = sortChildren; vm.toggleAllowAsRoot = toggleAllowAsRoot; vm.toggleAllowCultureVariants = toggleAllowCultureVariants; + vm.toggleAllowSegmentVariants = toggleAllowSegmentVariants; vm.canToggleIsElement = false; vm.toggleIsElement = toggleIsElement; @@ -110,6 +112,10 @@ $scope.model.allowCultureVariant = $scope.model.allowCultureVariant ? false : true; } + function toggleAllowSegmentVariants() { + $scope.model.allowSegmentVariant = $scope.model.allowSegmentVariant ? false : true; + } + function toggleIsElement() { $scope.model.isElement = $scope.model.isElement ? false : true; } diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html index e29956a48e..b44ce0ff7e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/views/permissions/permissions.html @@ -43,8 +43,8 @@
-
- +
+
@@ -57,6 +57,23 @@
+ +
+ +
+
+ +
+ +
+ + +
+ +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js index 5f1c46de4c..96441e6101 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js @@ -57,7 +57,7 @@ }; if ($routeParams.create) { - vm.page.name = vm.labels.addLanguage; + vm.page.name = vm.labels.addLanguage; } }); @@ -87,14 +87,14 @@ if (!$routeParams.create) { - promises.push(languageResource.getById($routeParams.id).then(function(lang) { + promises.push(languageResource.getById($routeParams.id).then(function (lang) { vm.language = lang; vm.page.name = vm.language.name; /* we need to store the initial default state so we can disable the toggle if it is the default. we need to prevent from not having a default language. */ - vm.initIsDefault = angular.copy(vm.language.isDefault); + vm.initIsDefault = Utilities.copy(vm.language.isDefault); makeBreadcrumbs(); @@ -182,12 +182,12 @@ function toggleDefault() { // it shouldn't be possible to uncheck the default language - if(vm.initIsDefault) { + if (vm.initIsDefault) { return; } vm.language.isDefault = !vm.language.isDefault; - if(vm.language.isDefault) { + if (vm.language.isDefault) { vm.showDefaultLanguageInfo = true; } else { vm.showDefaultLanguageInfo = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index 7ed6735292..f6ae77ebc4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -16,7 +16,7 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html b/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html index c1f83cd0b7..5a18ddf2cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html @@ -47,13 +47,18 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js index 70688f045c..63750ff0f2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packages/edit.controller.js @@ -40,7 +40,7 @@ vm.versionRegex = /^(\d+\.)(\d+\.)(\*|\d+)$/; function onInit() { - + if (create) { // Pre populate package with some values packageResource.getEmpty().then(scaffold => { @@ -78,7 +78,7 @@ }); - + localizationService.localizeMany(["buttons_save", "packager_includeAllChildNodes"]).then(function (values) { vm.labels.button = values[0]; vm.labels.includeAllChildNodes = values[1]; @@ -232,7 +232,7 @@ function openFilePicker() { - let selection = angular.copy(vm.package.files); + let selection = Utilities.copy(vm.package.files); const filePicker = { title: "Select files", diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js index b4381b699b..9ad2c87ab4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/colorpicker.controller.js @@ -19,7 +19,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.ColorPickerControl // Make an array from the dictionary var items = []; - if (angular.isArray($scope.model.prevalues)) { + if (Utilities.isArray($scope.model.prevalues)) { for (var i in $scope.model.prevalues) { var oldValue = $scope.model.prevalues[i]; diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.controller.js index 3da57943f9..c37c382dac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.controller.js @@ -7,7 +7,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiValuesControl $scope.hasError = false; $scope.focusOnNew = false; - if (!angular.isArray($scope.model.value)) { + if (!Utilities.isArray($scope.model.value)) { //make an array from the dictionary var items = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js index 951b76193f..0359043da4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.controller.js @@ -118,7 +118,7 @@ angular.module('umbraco') } function populate(data) { - if (angular.isArray(data)) { + if (Utilities.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js index 49058dfe29..ab7f5c66e0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js @@ -38,7 +38,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordCont } //set the model defaults - if (!angular.isObject($scope.model.value)) { + if (!Utilities.isObject($scope.model.value)) { //if it's not an object then just create a new one $scope.model.value = { newPassword: null, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js index 297bf23cef..10668808a5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js @@ -10,7 +10,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListContro function init() { // currently the property editor will onyl work if our input is an object. - if (angular.isObject($scope.model.config.items)) { + if (Utilities.isObject($scope.model.config.items)) { // formatting the items in the dictionary into an array var sortedItems = []; 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 ba2ad72191..e2f502e463 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 @@ -25,7 +25,7 @@ function ColorPickerController($scope, $timeout) { initActiveColor(); } - if (!angular.isArray($scope.model.config.items)) { + if (!Utilities.isArray($scope.model.config.items)) { //make an array from the dictionary var items = []; for (var i in $scope.model.config.items) { 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 91c6e673b9..2cbad88a43 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 @@ -55,7 +55,7 @@ }); }); - if (!angular.isArray($scope.model.value)) { + if (!Utilities.isArray($scope.model.value)) { //make an array from the dictionary var items = []; for (var i in $scope.model.value) { 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 5df324c60f..238872db40 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 @@ -152,7 +152,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper dataTypeKey: $scope.model.dataTypeKey, currentNode: editorState ? editorState.current : null, callback: function (data) { - if (angular.isArray(data)) { + if (Utilities.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); @@ -233,7 +233,7 @@ function contentPickerController($scope, entityResource, editorState, iconHelper $scope.currentPicker = dialogOptions; $scope.currentPicker.submit = function (model) { - if (angular.isArray(model.selection)) { + if (Utilities.isArray(model.selection)) { _.each(model.selection, function (item, i) { $scope.add(item); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js index a6d615cdd1..afbb4feb20 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/dropdownFlexible/dropdownFlexible.controller.js @@ -15,7 +15,14 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo //ensure this is a bool, old data could store zeros/ones or string versions $scope.model.config.multiple = Object.toBoolean($scope.model.config.multiple); - + + //ensure when form is saved that we don't store [] or [null] as string values in the database when no items are selected + $scope.$on("formSubmitting", function () { + if ($scope.model.value.length === 0 || $scope.model.value[0] === null) { + $scope.model.value = null; + } + }); + function convertArrayToDictionaryArray(model){ //now we need to format the items in the dictionary because we always want to have an array var newItems = []; @@ -45,14 +52,14 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.DropdownFlexibleCo $scope.model.value = [$scope.model.singleDropdownValue]; } - if (angular.isArray($scope.model.config.items)) { + if (Utilities.isArray($scope.model.config.items)) { //PP: I dont think this will happen, but we have tests that expect it to happen.. //if array is simple values, convert to array of objects - if(!angular.isObject($scope.model.config.items[0])){ + if (!Utilities.isObject($scope.model.config.items[0])){ $scope.model.config.items = convertArrayToDictionaryArray($scope.model.config.items); } } - else if (angular.isObject($scope.model.config.items)) { + else if (Utilities.isObject($scope.model.config.items)) { $scope.model.config.items = convertObjectToDictionaryArray($scope.model.config.items); } else { 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 17959b9950..c485f4bbc6 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 @@ -34,6 +34,7 @@ fileManager.setFiles({ propertyAlias: $scope.model.alias, culture: $scope.model.culture, + segment: $scope.model.segment, files: [] }); } 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 2bc609714a..522278e99e 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 @@ -1,5 +1,6 @@ 
0 && $scope.model.value.sections[0].rows && $scope.model.value.sections[0].rows.length > 0) { - if ($scope.model.value.name && angular.isArray($scope.model.config.items.templates)) { + if ($scope.model.value.name && Utilities.isArray($scope.model.config.items.templates)) { //This will occur if it is an existing value, in which case // we need to determine which layout was applied by looking up @@ -756,14 +756,14 @@ angular.module("umbraco") return t.name === $scope.model.value.name; }); - if (found && angular.isArray(found.sections) && found.sections.length === $scope.model.value.sections.length) { + if (found && Utilities.isArray(found.sections) && found.sections.length === $scope.model.value.sections.length) { //Cool, we've found the template associated with our current value with matching sections counts, now we need to // merge this template data on to our current value (as if it was new) so that we can preserve what is and isn't // allowed for this template based on the current config. _.each(found.sections, function (templateSection, index) { - angular.extend($scope.model.value.sections[index], angular.copy(templateSection)); + angular.extend($scope.model.value.sections[index], Utilities.copy(templateSection)); }); } @@ -835,7 +835,7 @@ angular.module("umbraco") return null; } else { //make a copy to not touch the original config - original = angular.copy(original); + original = Utilities.copy(original); original.styles = row.styles; original.config = row.config; original.hasConfig = gridItemHasConfig(row.styles, row.config); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 7fdefb4d78..5048c479bb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -256,7 +256,7 @@ - Add new role + Add new row diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js index 27ea819884..f273ea63e6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.prevalues.controller.js @@ -1,296 +1,296 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.GridPrevalueEditorController", - function ($scope, gridService, editorService) { + function ($scope, gridService, editorService) { - var emptyModel = { - styles:[ - { - label: "Set a background image", - description: "Set a row background", - key: "background-image", - view: "imagepicker", - modifier: "url({0})" - } - ], - - config:[ - { - label: "Class", - description: "Set a css class", - key: "class", - view: "textstring" - } - ], - - columns: 12, - templates:[ - { - name: "1 column layout", - sections: [ - { - grid: 12 - } - ] - }, - { - name: "2 column layout", - sections: [ - { - grid: 4 - }, - { - grid: 8 - } - ] - } - ], - - - layouts:[ - { - label: "Headline", - name: "Headline", - areas: [ - { - grid: 12, - editors: ["headline"] - } - ] - }, - { - label: "Article", - name: "Article", - areas: [ - { - grid: 4 - }, - { - grid: 8 - } - ] - } - ] - }; - - /**************** - template - *****************/ - - $scope.configureTemplate = function(template) { - - var index = $scope.model.value.templates.indexOf(template); - - if (template === undefined) { - template = { - name: "", - sections: [ - - ] - }; - } - - var layoutConfigOverlay = { - currentLayout: angular.copy(template), - rows: $scope.model.value.layouts, - columns: $scope.model.value.columns, - view: "views/propertyEditors/grid/dialogs/layoutconfig.html", - size: "small", - submit: function (model) { - if (index === -1) { - $scope.model.value.templates.push(model); - } else { - $scope.model.value.templates[index] = model; + var emptyModel = { + styles: [ + { + label: "Set a background image", + description: "Set a row background", + key: "background-image", + view: "imagepicker", + modifier: "url({0})" } - editorService.close(); - }, - close: function(model) { - editorService.close(); - } + ], + + config: [ + { + label: "Class", + description: "Set a css class", + key: "class", + view: "textstring" + } + ], + + columns: 12, + templates: [ + { + name: "1 column layout", + sections: [ + { + grid: 12 + } + ] + }, + { + name: "2 column layout", + sections: [ + { + grid: 4 + }, + { + grid: 8 + } + ] + } + ], + + + layouts: [ + { + label: "Headline", + name: "Headline", + areas: [ + { + grid: 12, + editors: ["headline"] + } + ] + }, + { + label: "Article", + name: "Article", + areas: [ + { + grid: 4 + }, + { + grid: 8 + } + ] + } + ] }; - editorService.open(layoutConfigOverlay); - - }; + /**************** + template + *****************/ - $scope.deleteTemplate = function(index){ - $scope.model.value.templates.splice(index, 1); - }; - + $scope.configureTemplate = function (template) { - /**************** - Row - *****************/ + var index = $scope.model.value.templates.indexOf(template); - $scope.configureLayout = function(layout) { + if (template === undefined) { + template = { + name: "", + sections: [ - var index = $scope.model.value.layouts.indexOf(layout); - - if(layout === undefined){ - layout = { - name: "", - areas:[ + ] + }; + } - ] + var layoutConfigOverlay = { + currentLayout: Utilities.copy(template), + rows: $scope.model.value.layouts, + columns: $scope.model.value.columns, + view: "views/propertyEditors/grid/dialogs/layoutconfig.html", + size: "small", + submit: function (model) { + if (index === -1) { + $scope.model.value.templates.push(model); + } else { + $scope.model.value.templates[index] = model; + } + editorService.close(); + }, + close: function (model) { + editorService.close(); + } }; - } - - var rowConfigOverlay = { - currentRow: angular.copy(layout), - editors: $scope.editors, - columns: $scope.model.value.columns, - view: "views/propertyEditors/grid/dialogs/rowconfig.html", - size: "small", - submit: function (model) { - if (index === -1) { - $scope.model.value.layouts.push(model); - } else { - $scope.model.value.layouts[index] = model; - } - editorService.close(); - }, - close: function(model) { - editorService.close(); - } - }; - editorService.open(rowConfigOverlay); - - }; + editorService.open(layoutConfigOverlay); - //var rowDeletesPending = false; - $scope.deleteLayout = function(index) { - - var rowDeleteOverlay = { - dialogData: { - rowName: $scope.model.value.layouts[index].name - }, - view: "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html", - size: "small", - submit: function(model) { - $scope.model.value.layouts.splice(index, 1); - editorService.close(); - }, - close: function(model) { - editorService.close(); + }; + + $scope.deleteTemplate = function (index) { + $scope.model.value.templates.splice(index, 1); + }; + + + /**************** + Row + *****************/ + + $scope.configureLayout = function (layout) { + + var index = $scope.model.value.layouts.indexOf(layout); + + if (layout === undefined) { + layout = { + name: "", + areas: [ + + ] + }; + } + + var rowConfigOverlay = { + currentRow: Utilities.copy(layout), + editors: $scope.editors, + columns: $scope.model.value.columns, + view: "views/propertyEditors/grid/dialogs/rowconfig.html", + size: "small", + submit: function (model) { + if (index === -1) { + $scope.model.value.layouts.push(model); + } else { + $scope.model.value.layouts[index] = model; + } + editorService.close(); + }, + close: function (model) { + editorService.close(); + } + }; + + editorService.open(rowConfigOverlay); + + }; + + //var rowDeletesPending = false; + $scope.deleteLayout = function (index) { + + var rowDeleteOverlay = { + dialogData: { + rowName: $scope.model.value.layouts[index].name + }, + view: "views/propertyEditors/grid/dialogs/rowdeleteconfirm.html", + size: "small", + submit: function (model) { + $scope.model.value.layouts.splice(index, 1); + editorService.close(); + }, + close: function (model) { + editorService.close(); + } + }; + + editorService.open(rowDeleteOverlay); + }; + + + /**************** + utillities + *****************/ + $scope.toggleCollection = function (collection, toggle) { + if (toggle) { + collection = []; + } else { + collection = null; } }; - editorService.open(rowDeleteOverlay); - }; + $scope.percentage = function (spans) { + return ((spans / $scope.model.value.columns) * 100).toFixed(8); + }; - - /**************** - utillities - *****************/ - $scope.toggleCollection = function(collection, toggle){ - if(toggle){ - collection = []; - }else{ - collection = null; - } - }; - - $scope.percentage = function(spans){ - return ((spans / $scope.model.value.columns) * 100).toFixed(8); - }; - - $scope.zeroWidthFilter = function (cell) { + $scope.zeroWidthFilter = function (cell) { return cell.grid > 0; - }; - - /**************** - Config - *****************/ - - $scope.removeConfigValue = function(collection, index){ - collection.splice(index, 1); - }; - - var editConfigCollection = function(configValues, title, callback) { - - var editConfigCollectionOverlay = { - config: configValues, - title: title, - view: "views/propertyeditors/grid/dialogs/editconfig.html", - size: "small", - submit: function(model) { - callback(model.config); - editorService.close(); - }, - close: function(model) { - editorService.close(); - } }; - editorService.open(editConfigCollectionOverlay); - }; + /**************** + Config + *****************/ - $scope.editConfig = function() { - editConfigCollection($scope.model.value.config, "Settings", function(data) { - $scope.model.value.config = data; - }); - }; + $scope.removeConfigValue = function (collection, index) { + collection.splice(index, 1); + }; - $scope.editStyles = function() { - editConfigCollection($scope.model.value.styles, "Styling", function(data){ - $scope.model.value.styles = data; - }); - }; + var editConfigCollection = function (configValues, title, callback) { - /**************** - editors - *****************/ - gridService.getGridEditors().then(function(response){ - $scope.editors = response.data; - }); + var editConfigCollectionOverlay = { + config: configValues, + title: title, + view: "views/propertyeditors/grid/dialogs/editconfig.html", + size: "small", + submit: function (model) { + callback(model.config); + editorService.close(); + }, + close: function (model) { + editorService.close(); + } + }; + editorService.open(editConfigCollectionOverlay); + }; - /* init grid data */ - if (!$scope.model.value || $scope.model.value === "" || !$scope.model.value.templates) { - $scope.model.value = emptyModel; - } else { + $scope.editConfig = function () { + editConfigCollection($scope.model.value.config, "Settings", function (data) { + $scope.model.value.config = data; + }); + }; - if (!$scope.model.value.columns) { - $scope.model.value.columns = emptyModel.columns; - } + $scope.editStyles = function () { + editConfigCollection($scope.model.value.styles, "Styling", function (data) { + $scope.model.value.styles = data; + }); + }; - - if (!$scope.model.value.config) { - $scope.model.value.config = []; - } - - if (!$scope.model.value.styles) { - $scope.model.value.styles = []; - } - } - - /**************** - Clean up - *****************/ - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var ts = $scope.model.value.templates; - var ls = $scope.model.value.layouts; - - _.each(ts, function(t){ - _.each(t.sections, function(section, index){ - if(section.grid === 0){ - t.sections.splice(index, 1); - } - }); + /**************** + editors + *****************/ + gridService.getGridEditors().then(function (response) { + $scope.editors = response.data; }); - _.each(ls, function(l){ - _.each(l.areas, function(area, index){ - if(area.grid === 0){ - l.areas.splice(index, 1); - } - }); + + /* init grid data */ + if (!$scope.model.value || $scope.model.value === "" || !$scope.model.value.templates) { + $scope.model.value = emptyModel; + } else { + + if (!$scope.model.value.columns) { + $scope.model.value.columns = emptyModel.columns; + } + + + if (!$scope.model.value.config) { + $scope.model.value.config = []; + } + + if (!$scope.model.value.styles) { + $scope.model.value.styles = []; + } + } + + /**************** + Clean up + *****************/ + var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { + var ts = $scope.model.value.templates; + var ls = $scope.model.value.layouts; + + _.each(ts, function (t) { + _.each(t.sections, function (section, index) { + if (section.grid === 0) { + t.sections.splice(index, 1); + } + }); + }); + + _.each(ls, function (l) { + _.each(l.areas, function (area, index) { + if (area.grid === 0) { + l.areas.splice(index, 1); + } + }); + }); }); - }); - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); + //when the scope is destroyed we need to unsubscribe + $scope.$on('$destroy', function () { + unsubscribe(); + }); - }); + }); 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 e3576426a3..70c74d0391 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,223 +1,225 @@ angular.module('umbraco') .controller("Umbraco.PropertyEditors.ImageCropperController", - function ($scope, fileManager, $timeout) { + function ($scope, fileManager, $timeout) { - var config = angular.copy($scope.model.config); + var config = Utilities.copy($scope.model.config); - $scope.filesSelected = onFileSelected; - $scope.filesChanged = onFilesChanged; - $scope.fileUploaderInit = onFileUploaderInit; - $scope.imageLoaded = imageLoaded; - $scope.crop = crop; - $scope.done = done; - $scope.clear = clear; - $scope.reset = reset; - $scope.close = close; - $scope.isCustomCrop = isCustomCrop; - $scope.focalPointChanged = focalPointChanged; - //declare a special method which will be called whenever the value has changed from the server - $scope.model.onValueChanged = onValueChanged; + $scope.filesSelected = onFileSelected; + $scope.filesChanged = onFilesChanged; + $scope.fileUploaderInit = onFileUploaderInit; + $scope.imageLoaded = imageLoaded; + $scope.crop = crop; + $scope.done = done; + $scope.clear = clear; + $scope.reset = reset; + $scope.close = close; + $scope.isCustomCrop = isCustomCrop; + $scope.focalPointChanged = focalPointChanged; + //declare a special method which will be called whenever the value has changed from the server + $scope.model.onValueChanged = onValueChanged; - /** - * Called when the umgImageGravity component updates the focal point value - * @param {any} left - * @param {any} top - */ - function focalPointChanged(left, top) { - //update the model focalpoint value - $scope.model.value.focalPoint = { - left: left, - top: top + /** + * Called when the umgImageGravity component updates the focal point value + * @param {any} left + * @param {any} top + */ + function focalPointChanged(left, top) { + //update the model focalpoint value + $scope.model.value.focalPoint = { + left: left, + top: top + }; + + //set form to dirty to track changes + $scope.imageCropperForm.$setDirty(); + } + + /** + * Used to assign a new model value + * @param {any} src + */ + function setModelValueWithSrc(src) { + if (!$scope.model.value || !$scope.model.value.src) { + //we are copying to not overwrite the original config + $scope.model.value = angular.extend(Utilities.copy($scope.model.config), { src: src }); + } + } + + /** + * called whenever the value has changed from the server + * @param {any} newVal + * @param {any} oldVal + */ + function onValueChanged(newVal, oldVal) { + //clear current uploaded files + fileManager.setFiles({ + propertyAlias: $scope.model.alias, + culture: $scope.model.culture, + segment: $scope.model.segment, + files: [] + }); + } + + /** + * Called when the a new file is selected + * @param {any} value + */ + function onFileSelected(value, files) { + setModelValueWithSrc(value); + //set form to dirty to track changes + $scope.imageCropperForm.$setDirty(); + } + + function imageLoaded(isCroppable, hasDimensions) { + $scope.isCroppable = isCroppable; + $scope.hasDimensions = hasDimensions; }; - //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); - } - - /** - * Used to assign a new model value - * @param {any} src - */ - function setModelValueWithSrc(src) { - if (!$scope.model.value || !$scope.model.value.src) { - //we are copying to not overwrite the original config - $scope.model.value = angular.extend(angular.copy($scope.model.config), { src: src }); - } - } - - /** - * called whenever the value has changed from the server - * @param {any} newVal - * @param {any} oldVal - */ - function onValueChanged(newVal, oldVal) { - //clear current uploaded files - fileManager.setFiles({ - propertyAlias: $scope.model.alias, - culture: $scope.model.culture, - files: [] - }); - } - - /** - * Called when the a new file is selected - * @param {any} value - */ - function onFileSelected(value, files) { - setModelValueWithSrc(value); - //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); - } - - function imageLoaded (isCroppable, hasDimensions) { - $scope.isCroppable = isCroppable; - $scope.hasDimensions = hasDimensions; - }; - - /** - * Called when the file collection changes - * @param {any} value - * @param {any} files - */ - function onFilesChanged(files) { - if (files && files[0]) { - $scope.imageSrc = files[0].fileSrc; - //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); - } - } - - /** - * Called when the file uploader initializes - * @param {any} value - */ - function onFileUploaderInit(value, files) { - //move previously saved value to the editor - if ($scope.model.value) { - //backwards compat with the old file upload (incase some-one swaps them..) - if (angular.isString($scope.model.value)) { - setModelValueWithSrc($scope.model.value); - } - else { - //sync any config changes with the editor and drop outdated crops - _.each($scope.model.value.crops, function (saved) { - var configured = _.find(config.crops, function (item) { return item.alias === saved.alias }); - - if (configured && configured.height === saved.height && configured.width === saved.width) { - configured.coordinates = saved.coordinates; - } - }); - $scope.model.value.crops = config.crops; - - //restore focalpoint if missing - if (!$scope.model.value.focalPoint) { - $scope.model.value.focalPoint = { left: 0.5, top: 0.5 }; - } - } - - //if there are already files in the client assigned then set the src + /** + * Called when the file collection changes + * @param {any} value + * @param {any} files + */ + function onFilesChanged(files) { if (files && files[0]) { $scope.imageSrc = files[0].fileSrc; + //set form to dirty to track changes + $scope.imageCropperForm.$setDirty(); + } + } + + /** + * Called when the file uploader initializes + * @param {any} value + */ + function onFileUploaderInit(value, files) { + //move previously saved value to the editor + if ($scope.model.value) { + //backwards compat with the old file upload (incase some-one swaps them..) + if (Utilities.isString($scope.model.value)) { + setModelValueWithSrc($scope.model.value); + } + else { + //sync any config changes with the editor and drop outdated crops + _.each($scope.model.value.crops, function (saved) { + var configured = _.find(config.crops, function (item) { return item.alias === saved.alias }); + + if (configured && configured.height === saved.height && configured.width === saved.width) { + configured.coordinates = saved.coordinates; + } + }); + $scope.model.value.crops = config.crops; + + //restore focalpoint if missing + if (!$scope.model.value.focalPoint) { + $scope.model.value.focalPoint = { left: 0.5, top: 0.5 }; + } + } + + //if there are already files in the client assigned then set the src + if (files && files[0]) { + $scope.imageSrc = files[0].fileSrc; + } + else { + $scope.imageSrc = $scope.model.value.src; + } + + } + } + + /** + * crop a specific crop + * @param {any} targetCrop + */ + function crop(targetCrop) { + if (!$scope.currentCrop) { + // clone the crop so we can discard the changes + $scope.currentCrop = Utilities.copy(targetCrop); + $scope.currentPoint = null; + + //set form to dirty to track changes + $scope.imageCropperForm.$setDirty(); } else { - $scope.imageSrc = $scope.model.value.src; - } - - } - } + // we have a crop open already - close the crop (this will discard any changes made) + close(); - /** - * crop a specific crop - * @param {any} targetCrop - */ - function crop(targetCrop) { - if (!$scope.currentCrop) { - // clone the crop so we can discard the changes - $scope.currentCrop = angular.copy(targetCrop); - $scope.currentPoint = null; + // 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); + $scope.pendingCrop = false; + }); + + // this is necessary to keep the screen from flickering too badly while we wait for the new crop to open + // - check the view for its usage (basically it makes sure we keep the space reserved for the new crop) + $scope.pendingCrop = true; + } + }; + + /** done cropping */ + function done() { + if (!$scope.currentCrop) { + return; + } + // find the original crop by crop alias and update its coordinates + var editedCrop = _.find($scope.model.value.crops, crop => crop.alias === $scope.currentCrop.alias); + editedCrop.coordinates = $scope.currentCrop.coordinates; + $scope.close(); //set form to dirty to track changes $scope.imageCropperForm.$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 - // is reused for the new crop... and that's really bad - $timeout(function () { - crop(targetCrop); - $scope.pendingCrop = false; + function reset() { + $scope.currentCrop.coordinates = undefined; + $scope.done(); + } + + function close() { + $scope.currentCrop = undefined; + $scope.currentPoint = undefined; + } + + /** + * crop a specific crop + * @param {any} crop + */ + function clear(crop) { + //clear current uploaded files + fileManager.setFiles({ + propertyAlias: $scope.model.alias, + culture: $scope.model.culture, + segment: $scope.model.segment, + files: [] }); - // this is necessary to keep the screen from flickering too badly while we wait for the new crop to open - // - check the view for its usage (basically it makes sure we keep the space reserved for the new crop) - $scope.pendingCrop = true; + //clear the ui + $scope.imageSrc = null; + if ($scope.model.value) { + $scope.model.value = null; + } + + //set form to dirty to track changes + $scope.imageCropperForm.$setDirty(); + }; + + function isCustomCrop(crop) { + return !!crop.coordinates; } - }; - /** done cropping */ - function done() { - if (!$scope.currentCrop) { - return; - } - // find the original crop by crop alias and update its coordinates - var editedCrop = _.find($scope.model.value.crops, crop => crop.alias === $scope.currentCrop.alias); - editedCrop.coordinates = $scope.currentCrop.coordinates; - $scope.close(); - - //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); - }; - - function reset() { - $scope.currentCrop.coordinates = undefined; - $scope.done(); - } - - function close() { - $scope.currentCrop = undefined; - $scope.currentPoint = undefined; - } - - /** - * crop a specific crop - * @param {any} crop - */ - function clear(crop) { - //clear current uploaded files - fileManager.setFiles({ - propertyAlias: $scope.model.alias, - culture: $scope.model.culture, - files: [] + var unsubscribe = $scope.$on("formSubmitting", function () { + $scope.currentCrop = null; + $scope.currentPoint = null; }); - //clear the ui - $scope.imageSrc = null; - if ($scope.model.value) { - $scope.model.value = null; - } - - //set form to dirty to track changes - $scope.imageCropperForm.$setDirty(); - }; - - function isCustomCrop(crop) { - return !!crop.coordinates; - } - - var unsubscribe = $scope.$on("formSubmitting", function () { - $scope.currentCrop = null; - $scope.currentPoint = null; - }); - - $scope.$on('$destroy', function () { - unsubscribe(); - }); - }) + $scope.$on('$destroy', function () { + unsubscribe(); + }); + }) .run(function (mediaHelper, umbRequestHelper) { if (mediaHelper && mediaHelper.registerFileResolver) { - + //NOTE: The 'entity' can be either a normal media entity or an "entity" returned from the entityResource // they contain different data structures so if we need to query against it we need to be aware of this. mediaHelper.registerFileResolver("Umbraco.ImageCropper", function (property, entity, thumbnail) { @@ -232,7 +234,7 @@ angular.module('umbraco') //this is a fallback in case the cropper has been asssigned a upload field } - else if (angular.isString(property.value)) { + else if (Utilities.isString(property.value)) { if (thumbnail) { if (mediaHelper.detectIfImageByExtension(property.value)) { 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 84c696ab2e..0f225d4401 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 @@ -3,6 +3,7 @@ 0 && $scope.newItem.height > 0) { var exists = _.find($scope.model.value, function (item) { return $scope.newItem.alias === item.alias; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index 139a2515b5..16c1be98a0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -382,7 +382,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time return serial(selected, fn, getStatusMsg, 0).then(function (result) { // executes once the whole selection has been processed // in case of an error (caught by serial), result will be the error - if (!(result.data && angular.isArray(result.data.notifications))) + if (!(result.data && Utilities.isArray(result.data.notifications))) showNotificationsAndReset(result, true, getSuccessMsg(selected.length)); }); } @@ -709,7 +709,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time } function isDate(val) { - if (angular.isString(val)) { + if (Utilities.isString(val)) { return val.match(/^(\d{4})\-(\d{2})\-(\d{2})\ (\d{2})\:(\d{2})\:(\d{2})$/); } return false; 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 c3acf020b8..95e595a97a 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 @@ -20,7 +20,7 @@ function memberPickerController($scope, entityResource, iconHelper, angularHelpe }, filterCssClass: "not-allowed", callback: function(data) { - if (angular.isArray(data)) { + if (Utilities.isArray(data)) { _.each(data, function (item, i) { $scope.add(item); }); 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 a25d5e798e..7cad5a5f05 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 @@ -14,7 +14,7 @@ }); function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService, $routeParams, editorState) { - + var vm = this; var model = $scope.$parent.$parent.model; @@ -58,9 +58,9 @@ updateModel(); vm.currentNode = node; } - - var copyAllEntries = function() { - + + var copyAllEntries = function () { + syncCurrentNode(); // list aliases @@ -68,14 +68,14 @@ // remove dublicates aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); - + var nodeName = ""; - if(vm.umbVariantContent) { + if (vm.umbVariantContent) { nodeName = vm.umbVariantContent.editor.content.name; } - localizationService.localize("clipboard_labelForArrayOfItemsFrom", [model.label, nodeName]).then(function(data) { + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [model.label, nodeName]).then(function (data) { clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id); }); } @@ -146,7 +146,7 @@ orderBy: "$index", view: "itempicker", event: $event, - clickPasteItem: function(item) { + clickPasteItem: function (item) { if (item.type === "elementTypeArray") { _.each(item.data, function (entry) { pasteFromClipboard(entry); @@ -183,9 +183,9 @@ if (vm.overlayMenu.availableItems.length === 0) { return; } - + vm.overlayMenu.size = vm.overlayMenu.availableItems.length > 6 ? "medium" : "small"; - + vm.overlayMenu.pasteItems = []; var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", contentTypeAliases); @@ -197,7 +197,7 @@ icon: entry.icon }); }); - + var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", contentTypeAliases); _.each(arrayEntriesForPaste, function (entry) { vm.overlayMenu.pasteItems.push({ @@ -208,7 +208,8 @@ }); }); - vm.overlayMenu.title = vm.overlayMenu.pasteItems.length > 0 ? labels.grid_addElement : labels.content_createEmpty; + vm.overlayMenu.title = labels.grid_addElement; + vm.overlayMenu.hideHeader = vm.overlayMenu.pasteItems.length > 0; vm.overlayMenu.clickClearPaste = function ($event) { $event.stopPropagation(); @@ -216,6 +217,7 @@ clipboardService.clearEntriesOfType("elementType", contentTypeAliases); clipboardService.clearEntriesOfType("elementTypeArray", contentTypeAliases); vm.overlayMenu.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. + vm.overlayMenu.hideHeader = false; }; if (vm.overlayMenu.availableItems.length === 1 && vm.overlayMenu.pasteItems.length === 0) { @@ -393,10 +395,10 @@ clipboardService.copy("elementType", node.contentTypeAlias, node); $event.stopPropagation(); } - - + + function pasteFromClipboard(newNode) { - + if (newNode === undefined) { return; } @@ -407,7 +409,7 @@ vm.nodes.push(newNode); setDirty(); //updateModel();// done by setting current node... - + setCurrentNode(newNode); } @@ -515,7 +517,7 @@ } function createNode(scaffold, fromNcEntry) { - var node = angular.copy(scaffold); + var node = Utilities.copy(scaffold); node.key = fromNcEntry && fromNcEntry.key ? fromNcEntry.key : String.CreateGuid(); @@ -596,12 +598,12 @@ } - + var propertyActions = [ copyAllEntriesAction, removeAllEntriesAction ]; - + this.$onInit = function () { if (this.umbProperty) { this.umbProperty.setPropertyActions(propertyActions); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js index 2db7eaf562..6bfde10e9c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js @@ -8,7 +8,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.RadioButtonsContro function init() { //we can't really do anything if the config isn't an object - if (angular.isObject($scope.model.config.items)) { + if (Utilities.isObject($scope.model.config.items)) { // formatting the items in the dictionary into an array var sortedItems = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index 6a5a76b800..e642051733 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -80,7 +80,7 @@ }; $scope.add = function ($event) { - if (!angular.isArray($scope.model.value)) { + if (!Utilities.isArray($scope.model.value)) { $scope.model.value = []; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 748d8da1a4..74a70118eb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -12,7 +12,7 @@ angular.module("umbraco") $scope.textAreaHtmlId = $scope.model.alias + "_" + String.CreateGuid(); var editorConfig = $scope.model.config ? $scope.model.config.editor : null; - if (!editorConfig || angular.isString(editorConfig)) { + if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); } //make sure there's a max image size diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 47b0215dac..33e2b834f3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -3,7 +3,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", var cfg = tinyMceService.defaultPrevalues(); if($scope.model.value){ - if(angular.isString($scope.model.value)){ + if(Utilities.isString($scope.model.value)){ $scope.model.value = cfg; } }else{ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index d255c4a5d6..87f6ffeac9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -1,6 +1,6 @@
- + {{mandatoryMessage}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js index b147c4620b..1b913d7014 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.controller.js @@ -2,7 +2,7 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.UrlListController" function($rootScope, $scope, $filter) { function formatDisplayValue() { - if (angular.isArray($scope.model.value)) { + if (Utilities.isArray($scope.model.value)) { //it's the json value $scope.renderModel = _.map($scope.model.value, function (item) { return { @@ -42,4 +42,4 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.UrlListController" formatDisplayValue(); }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html index 35e7aa5176..4b51c44c18 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html @@ -6,6 +6,7 @@ { overlayService.close(); vm.changePasswordModel.value = model.changePassword; - changePassword(); + changePassword(); } }; overlayService.open(overlay); - }); + }); } function save() { @@ -165,16 +165,16 @@ .then(function (saved) { //if the user saved, then try to execute all extended save options - extendedSave(saved).then(function(result) { + extendedSave(saved).then(function (result) { //if all is good, then reset the form formHelper.resetForm({ scope: $scope }); }, Utilities.noop); - + vm.user = _.omit(saved, "navigation"); //restore vm.user.navigation = currentNav; setUserDisplayState(); - formatDatesToLocal(vm.user); + formatDatesToLocal(vm.user); vm.page.saveButtonState = "success"; @@ -184,7 +184,7 @@ err: err, showNotifications: true }); - + vm.page.saveButtonState = "error"; }); } @@ -201,15 +201,15 @@ //if allowManuallyChangingPassword=false, then we are using default settings and the user will need to enter their old password to change their own password. vm.changePasswordModel.value.reset = (!vm.changePasswordModel.value.oldPassword && !vm.user.isCurrentUser) || vm.changePasswordModel.config.allowManuallyChangingPassword; } - + // since we don't send the entire user model, the id is required vm.changePasswordModel.value.id = vm.user.id; - + usersResource.changePassword(vm.changePasswordModel.value) .then(() => { vm.changePasswordModel.isChanging = false; vm.changePasswordModel.value = {}; - + //the user has a password if they are not states: Invited, NoCredentials vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; }, err => { @@ -219,7 +219,7 @@ }); }); } - + /** * Used to emit the save event and await any async operations being performed by editor extensions * @param {any} savedUser @@ -228,7 +228,7 @@ //used to track any promises added by the event handlers to be awaited var promises = []; - + var args = { //getPromise: getPromise, user: savedUser, @@ -240,10 +240,10 @@ //emit the event eventsService.emit("editors.user.editController.save", args); - + //await all promises to complete var resultPromise = $q.all(promises); - + return resultPromise; } @@ -253,7 +253,7 @@ function openUserGroupPicker() { var currentSelection = []; - angular.copy(vm.user.userGroups, currentSelection); + Utilities.copy(vm.user.userGroups, currentSelection); var userGroupPicker = { selection: currentSelection, submit: function (model) { @@ -263,7 +263,7 @@ } editorService.close(); }, - close: function () { + close: function () { editorService.close(); } }; @@ -355,10 +355,10 @@ vm.user.userState = 1; setUserDisplayState(); vm.disableUserButtonState = "success"; - + }, function (error) { vm.disableUserButtonState = "error"; - + }); } @@ -380,7 +380,7 @@ vm.user.failedPasswordAttempts = 0; setUserDisplayState(); vm.unlockUserButtonState = "success"; - + }, function (error) { vm.unlockUserButtonState = "error"; }); @@ -448,7 +448,7 @@ function clearAvatar() { // get user usersResource.clearAvatar(vm.user.id).then(function (data) { - vm.user.avatars = data; + vm.user.avatars = data; }); } @@ -469,15 +469,15 @@ }).progress(function (evt) { if (vm.avatarFile.uploadStatus !== "done" && vm.avatarFile.uploadStatus !== "error") { - // set uploading status on file - vm.avatarFile.uploadStatus = "uploading"; + // set uploading status on file + vm.avatarFile.uploadStatus = "uploading"; - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - // set percentage property on file - vm.avatarFile.uploadProgress = progressPercentage; - } + // set percentage property on file + vm.avatarFile.uploadProgress = progressPercentage; + } }).success(function (data, status, headers, config) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 4ac385921e..2217628872 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -67,7 +67,7 @@ ]; // Get last selected layout for "users" (defaults to first layout = card layout) - vm.activeLayout = listViewHelper.getLayout("users", vm.layouts); + vm.activeLayout = listViewHelper.getLayout("users", vm.layouts); // Don't show the invite button if no email is configured if (Umbraco.Sys.ServerVariables.umbracoSettings.showUserInvite) { @@ -247,19 +247,19 @@ function selectLayout(selectedLayout) { // save the selected layout for "users" so it's applied next time the user visits this section - vm.activeLayout = listViewHelper.setLayout("users", selectedLayout, vm.layouts); + vm.activeLayout = listViewHelper.setLayout("users", selectedLayout, vm.layouts); } - + function isSelectable(user) { return !user.isCurrentUser; } - + function selectUser(user) { - + if (!isSelectable(user)) { return; } - + if (user.selected) { var index = vm.selection.indexOf(user.id); vm.selection.splice(index, 1); @@ -268,9 +268,9 @@ user.selected = true; vm.selection.push(user.id); } - + setBulkActions(vm.users); - + } function clearSelection() { @@ -279,14 +279,14 @@ }); vm.selection = []; } - + function clickUser(user, $event) { - + $event.stopPropagation(); - + if ($event) { // targeting a new tab/window? - if ($event.ctrlKey || + if ($event.ctrlKey || $event.shiftKey || $event.metaKey || // apple ($event.button && $event.button === 1) // middle click, >IE9 + everyone else @@ -295,7 +295,7 @@ return; } } - + goToUser(user); $event.preventDefault(); @@ -398,7 +398,7 @@ function openUserGroupPicker() { var currentSelection = []; - angular.copy(vm.newUser.userGroups, currentSelection); + Utilities.copy(vm.newUser.userGroups, currentSelection); var userGroupPicker = { selection: currentSelection, submit: function (model) { @@ -611,7 +611,7 @@ // copy to clip board success function copySuccess() { if (vm.page.copyPasswordButtonState !== "success") { - $timeout(function(){ + $timeout(function () { vm.page.copyPasswordButtonState = "success"; }); $timeout(function () { @@ -623,7 +623,7 @@ // copy to clip board error function copyError() { if (vm.page.copyPasswordButtonState !== "error") { - $timeout(function() { + $timeout(function () { vm.page.copyPasswordButtonState = "error"; }); $timeout(function () { @@ -654,7 +654,7 @@ return null; } - + function getEditPath(user) { return pathToUser(user) + usersOptionsAsQueryString(); } @@ -699,7 +699,7 @@ vm.usersOptions.pageSize = data.pageSize; vm.usersOptions.totalItems = data.totalItems; vm.usersOptions.totalPages = data.totalPages; - + formatDates(vm.users); setUserDisplayState(vm.users); vm.userStatesFilter = usersHelper.getUserStatesFilter(data.userStates); @@ -753,19 +753,19 @@ var firstSelectedUserGroups; angular.forEach(users, function (user) { - + if (!user.selected) { return; } - - + + // if the current user is selected prevent any bulk actions with the user included if (user.isCurrentUser) { vm.allowDisableUser = false; vm.allowEnableUser = false; vm.allowUnlockUser = false; vm.allowSetUserGroup = false; - + return false; } @@ -814,6 +814,7 @@ vm.newUser.message = ""; // clear button state vm.page.createButtonState = "init"; + $scope.$emit("$setAccessibleHeader", true, "general_user", false, "", "", true); } init(); diff --git a/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js b/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js index b4a3fb9ed6..31310e1c46 100644 --- a/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js +++ b/src/Umbraco.Web.UI.Client/test/lib/angular/angular-mocks.js @@ -51,12 +51,12 @@ angular.mock.$Browser = function () { self.onUrlChange = function (listener) { self.pollFns.push( - function () { - if (self.$$lastUrl != self.$$url) { - self.$$lastUrl = self.$$url; - listener(self.$$url); - } - } + function () { + if (self.$$lastUrl != self.$$url) { + self.$$lastUrl = self.$$url; + listener(self.$$url); + } + } ); return listener; @@ -104,7 +104,7 @@ angular.mock.$Browser = function () { * @param {number=} number of milliseconds to flush. See {@link #defer.now} */ self.defer.flush = function (delay) { - if (angular.isDefined(delay)) { + if (Utilities.isDefined(delay)) { self.defer.now += delay; } else { if (self.deferredFns.length) { @@ -165,15 +165,15 @@ angular.mock.$Browser.prototype = { if (value == undefined) { delete this.cookieHash[name]; } else { - if (angular.isString(value) && //strings only + if (Utilities.isString(value) && //strings only value.length <= 4096) { //strict cookie storage limits this.cookieHash[name] = value; } } } else { if (!angular.equals(this.cookieHash, this.lastCookieHash)) { - this.lastCookieHash = angular.copy(this.cookieHash); - this.cookieHash = angular.copy(this.cookieHash); + this.lastCookieHash = Utilities.copy(this.cookieHash); + this.cookieHash = Utilities.copy(this.cookieHash); } return this.cookieHash; } @@ -397,7 +397,7 @@ angular.mock.$LogProvider = function () { }); if (errors.length) { errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or an expected " + - "log message was not checked and removed:"); + "log message was not checked and removed:"); errors.push(''); throw new Error(errors.join('\n---------\n')); } @@ -486,7 +486,7 @@ angular.mock.$LogProvider = function () { */ angular.mock.TzDate = function (offset, timestamp) { var self = new Date(0); - if (angular.isString(timestamp)) { + if (Utilities.isString(timestamp)) { var tsStr = timestamp; self.origDate = jsonStringToDate(timestamp); @@ -581,12 +581,12 @@ angular.mock.$LogProvider = function () { if (self.toISOString) { self.toISOString = function () { return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + - padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + - padNumber(self.origDate.getUTCDate(), 2) + 'T' + - padNumber(self.origDate.getUTCHours(), 2) + ':' + - padNumber(self.origDate.getUTCMinutes(), 2) + ':' + - padNumber(self.origDate.getUTCSeconds(), 2) + '.' + - padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' + padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumber(self.origDate.getUTCDate(), 2) + 'T' + + padNumber(self.origDate.getUTCHours(), 2) + ':' + + padNumber(self.origDate.getUTCMinutes(), 2) + ':' + + padNumber(self.origDate.getUTCSeconds(), 2) + '.' + + padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z' } } @@ -686,19 +686,19 @@ angular.mock.dump = function (object) { var out; if (angular.isElement(object)) { - object = angular.element(object); - out = angular.element('
'); + object = $(object); + out = $('
'); angular.forEach(object, function (element) { - out.append(angular.element(element).clone()); + out.append($(element).clone()); }); out = out.html(); - } else if (angular.isArray(object)) { + } else if (Utilities.isArray(object)) { out = []; angular.forEach(object, function (o) { out.push(serialize(o)); }); out = '[ ' + out.join(', ') + ' ]'; - } else if (angular.isObject(object)) { + } else if (Utilities.isObject(object)) { if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { out = serializeScope(object); } else if (object instanceof Error) { @@ -943,7 +943,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { wasExpected = false; function prettyPrint(data) { - return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) + return (Utilities.isString(data) || angular.isFunction(data) || data instanceof RegExp) ? data : angular.toJson(data); } @@ -1004,7 +1004,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { throw wasExpected ? Error('No response defined !') : Error('Unexpected request: ' + method + ' ' + url + '\n' + - (expectation ? 'Expected ' + expectation : 'No more request expected')); + (expectation ? 'Expected ' + expectation : 'No more request expected')); } /** @@ -1267,7 +1267,7 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { $rootScope.$digest(); if (!responses.length) throw Error('No pending request to flush !'); - if (angular.isDefined(count)) { + if (Utilities.isDefined(count)) { while (count--) { if (!responses.length) throw Error('No more pending request to flush !'); responses.shift()(); @@ -1365,8 +1365,8 @@ function MockHttpExpectation(method, url, data, headers) { this.match = function (m, u, d, h) { if (method != m) return false; if (!this.matchUrl(u)) return false; - if (angular.isDefined(d) && !this.matchData(d)) return false; - if (angular.isDefined(h) && !this.matchHeaders(h)) return false; + if (Utilities.isDefined(d) && !this.matchData(d)) return false; + if (Utilities.isDefined(h) && !this.matchHeaders(h)) return false; return true; }; @@ -1385,7 +1385,7 @@ function MockHttpExpectation(method, url, data, headers) { this.matchData = function (d) { if (angular.isUndefined(data)) return true; if (data && angular.isFunction(data.test)) return data.test(d); - if (data && !angular.isString(data)) return angular.toJson(data) == d; + if (data && !Utilities.isString(data)) return angular.toJson(data) == d; return data == d; }; @@ -1499,7 +1499,7 @@ angular.mock.$TimeoutDecorator = function ($delegate, $browser) { */ angular.mock.$RootElementProvider = function () { this.$get = function () { - return angular.element('
'); + return $('
'); } }; @@ -1572,7 +1572,7 @@ angular.module('ngMockE2E', ['ng']).config(function ($provide) { * * // adds a new phone to the phones array * $httpBackend.whenPOST('/phones').respond(function(method, url, data) { - * phones.push(angular.fromJSON(data)); + * phones.push(JSON.parse(data)); * }); * $httpBackend.whenGET(/^\/templates\//).passThrough(); * //... @@ -1710,7 +1710,7 @@ angular.mock.clearDataCache = function () { if (cache.hasOwnProperty(key)) { var handle = cache[key].handle; - handle && angular.element(handle.elem).unbind(); + handle && $(handle.elem).unbind(); delete cache[key]; } } diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js index 2ccf9e886a..227a359d1d 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js @@ -121,13 +121,15 @@ describe('contentEditingHelper tests', function () { //act //note the null, that's because culture is null - formHelper.handleServerValidation({ "_Properties.bodyText.null.value": ["Required"] }); + formHelper.handleServerValidation({ "_Properties.bodyText.null.null.value": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); expect(serverValidationManager.items[0].fieldName).toBe("value"); expect(serverValidationManager.items[0].errorMsg).toBe("Required"); expect(serverValidationManager.items[0].propertyAlias).toBe("bodyText"); + expect(serverValidationManager.items[0].culture).toBe("invariant"); + expect(serverValidationManager.items[0].segment).toBeNull(); }); it('adds a multiple property and field level server validation errors when they are invalid', function () { @@ -142,7 +144,7 @@ describe('contentEditingHelper tests', function () { "Name": ["Required"], "UpdateDate": ["Invalid date"], //note the null, that's because culture is null - "_Properties.bodyText.null.value": ["Required field"], + "_Properties.bodyText.en-US.mySegment.value": ["Required field"], "_Properties.textarea": ["Invalid format"] }); @@ -157,6 +159,8 @@ describe('contentEditingHelper tests', function () { expect(serverValidationManager.items[2].fieldName).toBe("value"); expect(serverValidationManager.items[2].errorMsg).toBe("Required field"); expect(serverValidationManager.items[2].propertyAlias).toBe("bodyText"); + expect(serverValidationManager.items[2].culture).toBe("en-US"); + expect(serverValidationManager.items[2].segment).toBe("mySegment"); expect(serverValidationManager.items[3].fieldName).toBe(""); expect(serverValidationManager.items[3].errorMsg).toBe("Invalid format"); expect(serverValidationManager.items[3].propertyAlias).toBe("textarea"); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 966731f0f7..5d2a618f46 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -61,12 +61,12 @@ it('can retrieve property validation errors for a sub field', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); //act - var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1"); - var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2"); + var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1", null); + var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2", null); //assert expect(err1).not.toBeUndefined(); @@ -85,14 +85,14 @@ it('can retrieve property validation errors for a sub field for culture', function () { //arrange - serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 2", null); //act - var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "value1"); - var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2"); - var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2"); - var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2"); + var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "value1", null); + var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, "value1", null); + var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2", null); + var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2", null); //assert @@ -111,12 +111,77 @@ expect(err2.errorMsg).toEqual("Another value 2"); expect(err2.culture).toEqual("fr-FR"); }); + + it('can retrieve property validation errors for a sub field for segments', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", "segment1"); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", "segment2"); + + //act + var err1 = serverValidationManager.getPropertyError("myProperty", null, "value1", "segment1"); + var err1NotFound = serverValidationManager.getPropertyError("myProperty", null, "value1", null); + var err2 = serverValidationManager.getPropertyError("myProperty", null, "value2", "segment2"); + var err2NotFound = serverValidationManager.getPropertyError("myProperty", null, "value2", null); + + + //assert + expect(err1NotFound).toBeUndefined(); + expect(err2NotFound).toBeUndefined(); + + expect(err1).not.toBeUndefined(); + expect(err1.propertyAlias).toEqual("myProperty"); + expect(err1.fieldName).toEqual("value1"); + expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.segment).toEqual("segment1"); + + expect(err2).not.toBeUndefined(); + expect(err2.propertyAlias).toEqual("myProperty"); + expect(err2.fieldName).toEqual("value2"); + expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.segment).toEqual("segment2"); + }); + + + it('can retrieve property validation errors for a sub field for culture with segments', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", "segment1"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 2", "segment2"); + + //act + var err1 = serverValidationManager.getPropertyError("myProperty", "en-US", "value1", "segment1"); + expect(serverValidationManager.getPropertyError("myProperty", null, "value1", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", "en-US", "value1", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", null, "value1", "segment1")).toBeUndefined(); + var err2 = serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2", "segment2"); + expect(serverValidationManager.getPropertyError("myProperty", null, "value2", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", "fr-FR", "value2", null)).toBeUndefined(); + expect(serverValidationManager.getPropertyError("myProperty", null, "value2", "segment2")).toBeUndefined(); + + + //assert + + expect(err1).not.toBeUndefined(); + expect(err1.propertyAlias).toEqual("myProperty"); + expect(err1.fieldName).toEqual("value1"); + expect(err1.errorMsg).toEqual("Some value 1"); + expect(err1.culture).toEqual("en-US"); + expect(err1.segment).toEqual("segment1"); + + expect(err2).not.toBeUndefined(); + expect(err2.propertyAlias).toEqual("myProperty"); + expect(err2.fieldName).toEqual("value2"); + expect(err2.errorMsg).toEqual("Another value 2"); + expect(err2.culture).toEqual("fr-FR"); + expect(err2.segment).toEqual("segment2"); + }); it('can add a property errors with multiple sub fields and it the first will be retreived with only the property alias', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); //act var err = serverValidationManager.getPropertyError("myProperty"); @@ -132,10 +197,10 @@ it('will return null for a non-existing property error', function () { //arrage - serverValidationManager.addPropertyError("myProperty", null, "value", "Required"); + serverValidationManager.addPropertyError("myProperty", null, "value", "Required", null); //act - var err = serverValidationManager.getPropertyError("DoesntExist", null, "value"); + var err = serverValidationManager.getPropertyError("DoesntExist", null, "value", null); //assert expect(err).toBeUndefined(); @@ -145,15 +210,15 @@ it('detects if a property error exists', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); //act var err1 = serverValidationManager.hasPropertyError("myProperty"); - var err2 = serverValidationManager.hasPropertyError("myProperty", null, "value1"); - var err3 = serverValidationManager.hasPropertyError("myProperty", null, "value2"); + var err2 = serverValidationManager.hasPropertyError("myProperty", null, "value1", null); + var err3 = serverValidationManager.hasPropertyError("myProperty", null, "value2", null); var err4 = serverValidationManager.hasPropertyError("notFound"); - var err5 = serverValidationManager.hasPropertyError("myProperty", null, "notFound"); + var err5 = serverValidationManager.hasPropertyError("myProperty", null, "notFound", null); //assert expect(err1).toBe(true); @@ -167,15 +232,15 @@ it('can remove a property error with a sub field specified', function () { //arrage - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); //act - serverValidationManager.removePropertyError("myProperty", null, "value1"); + serverValidationManager.removePropertyError("myProperty", null, "value1", null); //assert - expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false); - expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(true); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value1", null)).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value2", null)).toBe(true); }); @@ -189,8 +254,8 @@ serverValidationManager.removePropertyError("myProperty"); //assert - expect(serverValidationManager.hasPropertyError("myProperty", null, "value1")).toBe(false); - expect(serverValidationManager.hasPropertyError("myProperty", null, "value2")).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value1", null)).toBe(false); + expect(serverValidationManager.hasPropertyError("myProperty", null, "value2", null)).toBe(false); }); @@ -201,10 +266,10 @@ it('can retrieve culture validation errors', function () { //arrange - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2"); - serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3", null); //assert expect(serverValidationManager.hasCultureError(null)).toBe(true); @@ -216,6 +281,39 @@ }); + describe('managing variant validation errors', function () { + + it('can retrieve variant validation errors', function () { + + //arrange + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3", null); + + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", "MySegment"); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 2", "MySegment"); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Another value 2", "MySegment"); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "value2", "Another value 3", "MySegment"); + + //assert + expect(serverValidationManager.hasVariantError(null, null)).toBe(true); + expect(serverValidationManager.hasVariantError("en-US", null)).toBe(true); + expect(serverValidationManager.hasVariantError("fr-FR", null)).toBe(true); + + expect(serverValidationManager.hasVariantError(null, "MySegment")).toBe(true); + expect(serverValidationManager.hasVariantError("en-US", "MySegment")).toBe(true); + expect(serverValidationManager.hasVariantError("fr-FR", "MySegment")).toBe(true); + + expect(serverValidationManager.hasVariantError("es-ES", null)).toBe(false); + expect(serverValidationManager.hasVariantError("es-ES", "MySegment")).toBe(false); + expect(serverValidationManager.hasVariantError("fr-FR", "MySegmentNotRight")).toBe(false); + expect(serverValidationManager.hasVariantError(null, "MySegmentNotRight")).toBe(false); + + }); + + }); + describe('validation error subscriptions', function() { it('can subscribe to a field error', function() { @@ -228,11 +326,11 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); //act serverValidationManager.addFieldError("Name", "Required"); - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); //assert expect(args).not.toBeUndefined(); @@ -249,8 +347,8 @@ }; var cb2 = function () { }; - serverValidationManager.subscribe(null, null, "Name", cb1); - serverValidationManager.subscribe(null, null, "Title", cb2); + serverValidationManager.subscribe(null, null, "Name", cb1, null); + serverValidationManager.subscribe(null, null, "Title", cb2, null); //act serverValidationManager.addFieldError("Name", "Required"); @@ -284,7 +382,7 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); serverValidationManager.subscribe("myProperty", null, "", function (isValid, propertyErrors, allErrors) { numCalled++; @@ -293,12 +391,12 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); //act - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", null, "value2", "Some value 2"); - serverValidationManager.addPropertyError("myProperty", null, "", "Some value 3"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", null, "value2", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", null, "", "Some value 3", null); //assert expect(args1).not.toBeUndefined(); @@ -335,7 +433,7 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); serverValidationManager.subscribe(null, "es-ES", null, function (isValid, propertyErrors, allErrors) { numCalled++; @@ -344,13 +442,13 @@ propertyErrors: propertyErrors, allErrors: allErrors }; - }); + }, null); //act - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1"); - serverValidationManager.addPropertyError("myProperty", "en-US", "value2", "Some value 2"); - serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3"); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", null); + serverValidationManager.addPropertyError("myProperty", "en-US", "value2", "Some value 2", null); + serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3", null); //assert expect(args1).not.toBeUndefined(); diff --git a/src/Umbraco.Web.UI.Client/test/unit/utilities.spec.js b/src/Umbraco.Web.UI.Client/test/unit/utilities.spec.js new file mode 100644 index 0000000000..db642fde9f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/utilities.spec.js @@ -0,0 +1,63 @@ +(function () { + describe("Utilities", function () { + describe("fromJson", function () { + it("should deserialize json as object", function () { + expect(Utilities.fromJson('{"a":1,"b":2}')).toEqual({ a: 1, b: 2 }); + }); + it("should return object as object", function () { + expect(Utilities.fromJson({ a: 1, b: 2 })).toEqual({ a: 1, b: 2 }); + }); + }), + describe("toJson", function () { + it("should delegate to JSON.stringify", function () { + var spy = spyOn(JSON, "stringify").and.callThrough(); + + expect(Utilities.toJson({})).toEqual("{}"); + expect(spy).toHaveBeenCalled(); + }); + + it("should format objects pretty", function () { + expect(Utilities.toJson({ a: 1, b: 2 }, true)).toBe( + '{\n "a": 1,\n "b": 2\n}' + ); + expect(Utilities.toJson({ a: { b: 2 } }, true)).toBe( + '{\n "a": {\n "b": 2\n }\n}' + ); + expect(Utilities.toJson({ a: 1, b: 2 }, false)).toBe('{"a":1,"b":2}'); + expect(Utilities.toJson({ a: 1, b: 2 }, 0)).toBe('{"a":1,"b":2}'); + expect(Utilities.toJson({ a: 1, b: 2 }, 1)).toBe( + '{\n "a": 1,\n "b": 2\n}' + ); + expect(Utilities.toJson({ a: 1, b: 2 }, {})).toBe( + '{\n "a": 1,\n "b": 2\n}' + ); + }); + + it("should not serialize properties starting with $$", function () { + expect(Utilities.toJson({ $$some: "value" }, false)).toEqual("{}"); + }); + + it("should serialize properties starting with $", function () { + expect(Utilities.toJson({ $few: "v" }, false)).toEqual('{"$few":"v"}'); + }); + + it("should not serialize $window object", function () { + expect(Utilities.toJson(window)).toEqual('"$WINDOW"'); + }); + + it("should not serialize $document object", function () { + expect(Utilities.toJson(document)).toEqual('"$DOCUMENT"'); + }); + + it("should not serialize scope instances", inject(function ( + $rootScope + ) { + expect(Utilities.toJson({ key: $rootScope })).toEqual('{"key":"$SCOPE"}'); + })); + + it("should serialize undefined as undefined", function () { + expect(Utilities.toJson(undefined)).toEqual(undefined); + }); + }); + }); +})(); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index d5f91ffb13..8a5f223bd5 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -348,10 +348,7 @@ True 8700 / - http://localhost:8700 - 8610 - / - http://localhost:8610 + http://localhost:8700 False False diff --git a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml index 056d2b6f51..bd8d23a851 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml @@ -66,11 +66,7 @@
-
-
- -
@@ -93,7 +89,7 @@
- + + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index f0625ad767..ecb504ca50 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -8,7 +8,8 @@ Tilføj domæne Revisionsspor Gennemse elementer - Skift dokumenttype + Skift Dokument Type + Skift Input Type Kopier Opret Eksportér @@ -482,6 +483,7 @@ Fjern link fra dit konto Vælg editor + Vælg konfiguration Vælg snippet Dette vil slette noden og alle dets sprog. Hvis du kun vil slette et sprog, så afpublicér det i stedet. @@ -784,6 +786,7 @@ Generelt Editor Skift tillad sprogvarianter + Skift tillad segmentering Baggrundsfarve @@ -1382,9 +1385,11 @@ Mange hilsner fra Umbraco robotten Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition Der er ingen indholdstyper tilgængelige at bruge som komposition Når du fjerner en komposition vil alle associerede indholdsdata blive slettet. Når først dokumenttypen er gemt, er der ingen vej tilbage. - Tilgængelige editors + Opret ny indstilling Genbrug - Editor indstillinger + Input indstillinger + Tilgængelige indstillinger + Opret ny indstilling Konfiguration Ja, slet blev flyttet til @@ -1410,9 +1415,16 @@ Mange hilsner fra Umbraco robotten fane har ingen sorteringsrækkefølge Hvor er denne komposition brugt? Denne komposition brugt i kompositionen af de følgende indholdstyper: - Tillad sprogvariation + Tillad variationer + Tillad sprogvariation + Tillad segmentering + Tillader sprogvariationer + Tillader segmentering Tillad at redaktører kan oprette indhold af denne type på flere sprog. + Tillad at redaktører kan oprette dette indhold på flere sprog. + Tillad at redaktører kan oprette flere udgaver af denne type indhold. Tillad sprogvariation + Tillad segmentering Element-type Er en Element-type En Element-type er tiltænkt brug i f.eks. Nested Content, ikke i indholdstræet. diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index e617f9b07f..d92d1e8598 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -172,6 +172,7 @@ Content sent for publishing Content sent for publishing for languages: %0% Sort child items performed by user + %0% Copy Publish Publish @@ -184,6 +185,7 @@ Send To Publish Send To Publish Sort + Custom History (all variants) @@ -254,8 +256,8 @@ Published Published (pending changes) Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + %0% and all content items underneath and thereby making their content publicly available.]]> + Publish at Unpublish at Clear Date @@ -292,7 +294,7 @@ Add another text box Remove this text box Content root - Include drafts: also publish unpublished content items. + Include drafts and unpublished content items. This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. What languages would you like to publish? All languages with content are saved! @@ -306,7 +308,17 @@ Unpublished Languages Unmodified Languages These languages haven't been created - Ready to Publish? + + All new variants will be saved. + Which variants you would like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + The following variants is required for publishing to take place: + + We are not ready to Publish + Ready to publish? Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. @@ -805,6 +817,7 @@ General Editor Toggle allow culture variants + Toggle allow segmentation Background colour @@ -1655,9 +1668,16 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: - Allow varying by culture + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. Allow varying by culture + Allow segmentation Element type Is an Element type An Element type is meant to be used for instance in Nested Content, and not in the tree. @@ -2232,6 +2252,8 @@ To manage your website, simply open the Umbraco back office and start adding con Create Edit Name + Add new row + View more options References 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 df3cd8c4fe..a20b6e58a2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -9,6 +9,7 @@ Audit Trail Browse Node Change Document Type + Change Data Type Copy Create Export @@ -173,6 +174,7 @@ Content sent for publishing Content sent for publishing for languages: %0% Sort child items performed by user + %0% Copy Publish Publish @@ -186,6 +188,7 @@ Send To Publish Send To Publish Sort + Custom History (all variants) @@ -257,8 +260,8 @@ Published Published (pending changes)> Publication Status - Publish with descendants to publish %0% and all content items underneath and thereby making their content publicly available.]]> - Publish with descendants to publish the selected languages and the same languages of content items underneath and thereby making their content publicly available.]]> + %0% and all content items underneath and thereby making their content publicly available.]]> + Publish at Unpublish at Clear Date @@ -296,7 +299,7 @@ Add another text box Remove this text box Content root - Include drafts: also publish unpublished content items. + Include drafts and unpublished content items. This value is hidden. If you need access to view this value please contact your website administrator. This value is hidden. What languages would you like to publish? All languages with content are saved! @@ -310,7 +313,17 @@ Unpublished Languages Unmodified Languages These languages haven't been created - Ready to Publish? + + All new variants will be saved. + Which variants you would like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + The following variants is required for publishing to take place: + + We are not ready to Publish + Ready to publish? Ready to Save? Send for approval Select the date and time to publish and/or unpublish the content item. @@ -500,6 +513,7 @@ Un-link your account Select editor + Select configuration Select snippet This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. @@ -805,6 +819,7 @@ General Editor Toggle allow culture variants + Toggle allow segmentation Background color @@ -1639,6 +1654,8 @@ To manage your website, simply open the Umbraco back office and start adding con Create new Use existing Editor settings + Available configurations + Create a new configuration Configuration Yes, delete was moved underneath @@ -1664,9 +1681,16 @@ To manage your website, simply open the Umbraco back office and start adding con tab has no sort order Where is this composition used? This composition is currently used in the composition of the following content types: - Allow varying by culture + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. Allow varying by culture + Allow segmentation Element type Is an element type An element type is meant to be used for instance in Nested Content, and not in the tree. @@ -2243,6 +2267,8 @@ To manage your website, simply open the Umbraco back office and start adding con Create Edit Name + Add new row + View more options References diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index 7b3f2a2184..d0aa1a1c34 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -14,6 +14,10 @@ "content": "

Thank you for using Umbraco! Would you like to stay up-to-date with Umbraco product updates, security advisories, community news and special offers? Sign up for our newsletter and never miss out on the latest Umbraco news.

By signing up, you agree that we can use your info according to our privacy policy.

", "view": "emails", "type": "promotion" + }, + { + "title": "Thank you for subscribing to our mailing list", + "view": "confirm" } ] }, diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 89413db84d..53865830d8 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -349,6 +349,7 @@ namespace Umbraco.Web.Editors {"loginBackgroundImage", Current.Configs.Settings().Content.LoginBackgroundImage}, {"showUserInvite", EmailSender.CanSendRequiredEmail}, {"canSendRequiredEmail", EmailSender.CanSendRequiredEmail}, + {"showAllowSegmentationForDocumentTypes", false}, } }, { diff --git a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs index 6fc0e123a5..b962de74ac 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs @@ -50,7 +50,19 @@ namespace Umbraco.Web.Editors.Binders } } - // TODO: anything after 3 parts we can put in metadata + //if there are 4 parts part 4 is always segment + string segment = null; + if (parts.Length > 3) + { + segment = parts[3]; + //normalize to null if empty + if (segment.IsNullOrWhiteSpace()) + { + segment = null; + } + } + + // TODO: anything after 4 parts we can put in metadata var fileName = file.Headers.ContentDisposition.FileName.Trim('\"'); @@ -59,6 +71,7 @@ namespace Umbraco.Web.Editors.Binders TempFilePath = file.LocalFileName, PropertyAlias = propAlias, Culture = culture, + Segment = segment, FileName = fileName }); } diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 521626f666..565eebf80e 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -37,6 +37,7 @@ using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence; using Umbraco.Core.Security; using Umbraco.Web.Routing; +using Umbraco.Core.Collections; namespace Umbraco.Web.Editors { @@ -706,12 +707,19 @@ namespace Umbraco.Web.Editors { if (variantCount > 1) { - var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors); - foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray()) + var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); + + var validVariants = contentItem.Variants + .Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment))) + .Select(x => (culture: x.Culture, segment: x.Segment)); + + foreach (var (culture, segment) in validVariants) { - AddSuccessNotification(notifications, c, + var variantName = GetVariantName(culture, segment); + + AddSuccessNotification(notifications, culture, segment, Services.TextService.Localize("speechBubbles/editContentSendToPublish"), - Services.TextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { _allLangs.Value[c].CultureName })); + Services.TextService.Localize("speechBubbles/editVariantSendToPublishText", new[] { variantName })); } } else if (ModelState.IsValid) @@ -842,7 +850,7 @@ namespace Umbraco.Web.Editors //if there's more than 1 variant, then we need to add the culture specific error //messages based on the variants in error so that the messages show in the publish/save dialog if (variants.Count > 1) - AddCultureValidationError(variant.Culture, "publish/contentPublishedFailedByMissingName"); + AddVariantValidationError(variant.Culture, variant.Segment, "publish/contentPublishedFailedByMissingName"); else return false; //It's invariant and is missing critical data, it cannot be saved } @@ -896,12 +904,19 @@ namespace Umbraco.Web.Editors { if (variantCount > 1) { - var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors); - foreach (var c in contentItem.Variants.Where(x => x.Save && !cultureErrors.Contains(x.Culture)).Select(x => x.Culture).ToArray()) + var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); + + var savedWithoutErrors = contentItem.Variants + .Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment))) + .Select(x => (culture: x.Culture, segment: x.Segment)); + + foreach (var (culture, segment) in savedWithoutErrors) { - AddSuccessNotification(notifications, c, + var variantName = GetVariantName(culture, segment); + + AddSuccessNotification(notifications, culture, segment, Services.TextService.Localize("speechBubbles/editContentSavedHeader"), - Services.TextService.Localize(variantSavedLocalizationKey, new[] { _allLangs.Value[c].CultureName })); + Services.TextService.Localize(variantSavedLocalizationKey, new[] { variantName })); } } else if (ModelState.IsValid) @@ -1034,14 +1049,16 @@ namespace Umbraco.Web.Editors if (!isPublished && releaseDates.Count == 0) { //can't continue, a mandatory variant is not published and not scheduled for publishing - AddCultureValidationError(culture, "speechBubbles/scheduleErrReleaseDate2"); + // TODO: Add segment + AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate2"); isValid = false; continue; } if (!isPublished && releaseDates.Any(x => nonMandatoryVariantReleaseDates.Any(r => x.Date > r.Date))) { //can't continue, a mandatory variant is not published and it's scheduled for publishing after a non-mandatory - AddCultureValidationError(culture, "speechBubbles/scheduleErrReleaseDate3"); + // TODO: Add segment + AddVariantValidationError(culture, null, "speechBubbles/scheduleErrReleaseDate3"); isValid = false; continue; } @@ -1055,7 +1072,7 @@ namespace Umbraco.Web.Editors //1) release date cannot be less than now if (variant.ReleaseDate.HasValue && variant.ReleaseDate < DateTime.Now) { - AddCultureValidationError(variant.Culture, "speechBubbles/scheduleErrReleaseDate1"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrReleaseDate1"); isValid = false; continue; } @@ -1063,7 +1080,7 @@ namespace Umbraco.Web.Editors //2) expire date cannot be less than now if (variant.ExpireDate.HasValue && variant.ExpireDate < DateTime.Now) { - AddCultureValidationError(variant.Culture, "speechBubbles/scheduleErrExpireDate1"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate1"); isValid = false; continue; } @@ -1071,7 +1088,7 @@ namespace Umbraco.Web.Editors //3) expire date cannot be less than release date if (variant.ExpireDate.HasValue && variant.ReleaseDate.HasValue && variant.ExpireDate <= variant.ReleaseDate) { - AddCultureValidationError(variant.Culture, "speechBubbles/scheduleErrExpireDate2"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/scheduleErrExpireDate2"); isValid = false; continue; } @@ -1096,12 +1113,13 @@ namespace Umbraco.Web.Editors /// global notifications will be shown if all variant processing is successful and the save/publish dialog is closed, otherwise /// variant specific notifications are used to show success messages in the save/publish dialog. /// - private static void AddSuccessNotification(IDictionary notifications, string culture, string header, string msg) + private static void AddSuccessNotification(IDictionary notifications, string culture, string segment, string header, string msg) { //add the global notification (which will display globally if all variants are successfully processed) notifications[string.Empty].AddSuccessNotification(header, msg); //add the variant specific notification (which will display in the dialog if all variants are not successfully processed) - notifications.GetOrCreate(culture).AddSuccessNotification(header, msg); + var key = culture + "_" + segment; + notifications.GetOrCreate(key).AddSuccessNotification(header, msg); } /// @@ -1151,17 +1169,16 @@ namespace Umbraco.Web.Editors return publishStatus; } - //All variants in this collection should have a culture if we get here! but we'll double check and filter here - var cultureVariants = contentItem.Variants.Where(x => !x.Culture.IsNullOrWhiteSpace()).ToList(); - var mandatoryCultures = _allLangs.Value.Values.Where(x => x.IsMandatory).Select(x => x.IsoCode).ToList(); - var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors); + var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); + + var variants = contentItem.Variants.ToList(); //validate if we can publish based on the mandatory language requirements var canPublish = ValidatePublishingMandatoryLanguages( - cultureErrors, - contentItem, cultureVariants, mandatoryCultures, + variantErrors, + contentItem, variants, mandatoryCultures, mandatoryVariant => mandatoryVariant.Publish); //Now check if there are validation errors on each variant. @@ -1171,11 +1188,11 @@ namespace Umbraco.Web.Editors foreach (var variant in contentItem.Variants) { - if (cultureErrors.Contains(variant.Culture)) + if (variantErrors.Contains((variant.Culture, variant.Segment))) variant.Publish = false; } - var culturesToPublish = cultureVariants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); + var culturesToPublish = variants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); if (canPublish) { @@ -1223,17 +1240,16 @@ namespace Umbraco.Web.Editors return publishStatus; } - //All variants in this collection should have a culture if we get here! but we'll double check and filter here - var cultureVariants = contentItem.Variants.Where(x => !x.Culture.IsNullOrWhiteSpace()).ToList(); - var mandatoryCultures = _allLangs.Value.Values.Where(x => x.IsMandatory).Select(x => x.IsoCode).ToList(); - var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors); + var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); + + var variants = contentItem.Variants.ToList(); //validate if we can publish based on the mandatory languages selected var canPublish = ValidatePublishingMandatoryLanguages( - cultureErrors, - contentItem, cultureVariants, mandatoryCultures, + variantErrors, + contentItem, variants, mandatoryCultures, mandatoryVariant => mandatoryVariant.Publish); //if none are published and there are validation errors for mandatory cultures, then we can't publish anything @@ -1245,19 +1261,19 @@ namespace Umbraco.Web.Editors //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. foreach (var variant in contentItem.Variants) { - if (cultureErrors.Contains(variant.Culture)) + if (variantErrors.Contains((variant.Culture, variant.Segment))) variant.Publish = false; } //At this stage all variants might have failed validation which means there are no cultures flagged for publishing! - var culturesToPublish = cultureVariants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); + var culturesToPublish = variants.Where(x => x.Publish).Select(x => x.Culture).ToArray(); canPublish = canPublish && culturesToPublish.Length > 0; if (canPublish) { //try to publish all the values on the model - this will generally only fail if someone is tampering with the request //since there's no reason variant rules would be violated in normal cases. - canPublish = PublishCulture(contentItem.PersistedContent, cultureVariants, defaultCulture); + canPublish = PublishCulture(contentItem.PersistedContent, variants, defaultCulture); } if (canPublish) @@ -1282,16 +1298,16 @@ namespace Umbraco.Web.Editors /// /// Validate if publishing is possible based on the mandatory language requirements /// - /// + /// /// - /// + /// /// /// /// private bool ValidatePublishingMandatoryLanguages( - IReadOnlyCollection culturesWithValidationErrors, + IReadOnlyCollection<(string culture, string segment)> variantsWithValidationErrors, ContentItemSave contentItem, - IReadOnlyCollection cultureVariants, + IReadOnlyCollection variants, IReadOnlyList mandatoryCultures, Func publishingCheck) { @@ -1302,11 +1318,11 @@ namespace Umbraco.Web.Editors { //Check if a mandatory language is missing from being published - var mandatoryVariant = cultureVariants.First(x => x.Culture.InvariantEquals(culture)); + var mandatoryVariant = variants.First(x => x.Culture.InvariantEquals(culture)); var isPublished = contentItem.PersistedContent.Published && contentItem.PersistedContent.IsCulturePublished(culture); var isPublishing = isPublished || publishingCheck(mandatoryVariant); - var isValid = !culturesWithValidationErrors.InvariantContains(culture); + var isValid = !variantsWithValidationErrors.Select(v => v.culture).InvariantContains(culture); result.Add((mandatoryVariant, isPublished || isPublishing, isValid)); } @@ -1321,19 +1337,19 @@ namespace Umbraco.Web.Editors if (r.publishing && !r.isValid) { //flagged for publishing but the mandatory culture is invalid - AddCultureValidationError(r.model.Culture, "publish/contentPublishedFailedReqCultureValidationError"); + AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError"); canPublish = false; } else if (r.publishing && r.isValid && firstInvalidMandatoryCulture != null) { //in this case this culture also cannot be published because another mandatory culture is invalid - AddCultureValidationError(r.model.Culture, "publish/contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture); + AddVariantValidationError(r.model.Culture, r.model.Segment, "publish/contentPublishedFailedReqCultureValidationError", firstInvalidMandatoryCulture); canPublish = false; } else if (!r.publishing) { //cannot continue publishing since a required culture that is not currently being published isn't published - AddCultureValidationError(r.model.Culture, "speechBubbles/contentReqCulturePublishError"); + AddVariantValidationError(r.model.Culture, r.model.Segment, "speechBubbles/contentReqCulturePublishError"); canPublish = false; } } @@ -1358,7 +1374,7 @@ namespace Umbraco.Web.Editors var valid = persistentContent.PublishCulture(CultureImpact.Explicit(variant.Culture, defaultCulture.InvariantEquals(variant.Culture))); if (!valid) { - AddCultureValidationError(variant.Culture, "speechBubbles/contentCultureValidationError"); + AddVariantValidationError(variant.Culture, variant.Segment, "speechBubbles/contentCultureValidationError"); return false; } } @@ -1370,14 +1386,40 @@ namespace Umbraco.Web.Editors /// Adds a generic culture error for use in displaying the culture validation error in the save/publish/etc... dialogs /// /// Culture to assign the error to + /// Segment to assign the error to /// /// /// The culture used in the localization message, null by default which means will be used. /// - private void AddCultureValidationError(string culture, string localizationKey, string cultureToken = null) + private void AddVariantValidationError(string culture, string segment, string localizationKey, string cultureToken = null) { - var errMsg = Services.TextService.Localize(localizationKey, new[] { cultureToken == null ? _allLangs.Value[culture].CultureName : _allLangs.Value[cultureToken].CultureName }); - ModelState.AddCultureValidationError(culture, errMsg); + var cultureToUse = cultureToken ?? culture; + var variantName = GetVariantName(cultureToUse, segment); + + var errMsg = Services.TextService.Localize(localizationKey, new[] { variantName }); + + ModelState.AddVariantValidationError(culture, segment, errMsg); + } + + /// + /// Creates the human readable variant name based on culture and segment + /// + /// Culture + /// Segment + /// + private string GetVariantName(string culture, string segment) + { + if(culture.IsNullOrWhiteSpace() && segment.IsNullOrWhiteSpace()) + { + // TODO: Get name for default variant from somewhere? + return "Default"; + } + + var cultureName = culture == null ? null : _allLangs.Value[culture].CultureName; + var variantName = string.Join(" — ", new[] { segment, cultureName }.Where(x => !x.IsNullOrWhiteSpace())); + + // Format: [—] + return variantName; } /// @@ -1818,11 +1860,11 @@ namespace Umbraco.Web.Editors if (!ModelState.IsValid && display.Variants.Count() > 1) { //Add any culture specific errors here - var cultureErrors = ModelState.GetCulturesWithErrors(Services.LocalizationService, cultureForInvariantErrors); + var variantErrors = ModelState.GetVariantsWithErrors(cultureForInvariantErrors); - foreach (var cultureError in cultureErrors) + foreach (var (culture, segment) in variantErrors) { - AddCultureValidationError(cultureError, "speechBubbles/contentCultureValidationError"); + AddVariantValidationError(culture, segment, "speechBubbles/contentCultureValidationError"); } } @@ -1874,7 +1916,8 @@ namespace Umbraco.Web.Editors ? variant.PropertyCollectionDto : new ContentPropertyCollectionDto { - Properties = variant.PropertyCollectionDto.Properties.Where(x => !x.Culture.IsNullOrWhiteSpace()) + Properties = variant.PropertyCollectionDto.Properties.Where( + x => !x.Culture.IsNullOrWhiteSpace() || !x.Segment.IsNullOrWhiteSpace()) }; //for each variant, map the property values diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index 300c777b3a..893c9f5941 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -76,7 +76,7 @@ namespace Umbraco.Web.Editors // prepare files, if any matching property and culture var files = contentItem.UploadedFiles - .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture) + .Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment) .ToArray(); foreach (var file in files) diff --git a/src/Umbraco.Web/Editors/ExamineManagementController.cs b/src/Umbraco.Web/Editors/ExamineManagementController.cs index 0953b41cac..cf1dfd5d5d 100644 --- a/src/Umbraco.Web/Editors/ExamineManagementController.cs +++ b/src/Umbraco.Web/Editors/ExamineManagementController.cs @@ -141,9 +141,6 @@ namespace Umbraco.Web.Editors try { - //clear and replace - index.CreateIndex(); - var cacheKey = "temp_indexing_op_" + index.Name; //put temp val in cache which is used as a rudimentary way to know when the indexing is done AppCaches.RuntimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5)); diff --git a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs index 0d77b35528..810c2d1bea 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs @@ -198,7 +198,7 @@ namespace Umbraco.Web.Editors.Filters r.ErrorMessage = property.ValidationRegExpMessage; } - modelState.AddPropertyError(r, property.Alias, property.Culture); + modelState.AddPropertyError(r, property.Alias, property.Culture, property.Segment); } } } diff --git a/src/Umbraco.Web/GridTemplateExtensions.cs b/src/Umbraco.Web/GridTemplateExtensions.cs index afa929cfbb..81dc33d2c6 100644 --- a/src/Umbraco.Web/GridTemplateExtensions.cs +++ b/src/Umbraco.Web/GridTemplateExtensions.cs @@ -45,6 +45,34 @@ namespace Umbraco.Web return html.Partial(view, model); } + public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedElement contentItem) + { + return html.GetGridHtml(contentItem, "bodyText", "bootstrap3"); + } + + public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedElement contentItem, string propertyAlias) + { + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); + + return html.GetGridHtml(contentItem, propertyAlias, "bootstrap3"); + } + + public static MvcHtmlString GetGridHtml(this HtmlHelper html, IPublishedElement contentItem, string propertyAlias, string framework) + { + if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); + if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); + + var view = "Grid/" + framework; + var prop = contentItem.GetProperty(propertyAlias); + if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); + var model = prop.GetValue(); + + var asString = model as string; + if (asString != null && string.IsNullOrEmpty(asString)) return new MvcHtmlString(string.Empty); + + return html.Partial(view, model); + } public static MvcHtmlString GetGridHtml(this IPublishedProperty property, HtmlHelper html, string framework = "bootstrap3") { var asString = property.GetValue() as string; diff --git a/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs new file mode 100644 index 0000000000..0c3e2f3d91 --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/Data/DatabaseIntegrityCheck.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Serilog.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Services; + +namespace Umbraco.Web.HealthCheck.Checks.Data +{ + [HealthCheck( + "73DD0C1C-E0CA-4C31-9564-1DCA509788AF", + "Database data integrity check", + Description = "Checks for various data integrity issues in the Umbraco database.", + Group = "Data Integrity")] + public class DatabaseIntegrityCheck : HealthCheck + { + private readonly IContentService _contentService; + private readonly IMediaService _mediaService; + private const string _fixMediaPaths = "fixMediaPaths"; + private const string _fixContentPaths = "fixContentPaths"; + private const string _fixMediaPathsTitle = "Fix media paths"; + private const string _fixContentPathsTitle = "Fix content paths"; + + public DatabaseIntegrityCheck(IContentService contentService, IMediaService mediaService) + { + _contentService = contentService; + _mediaService = mediaService; + } + + /// + /// Get the status for this health check + /// + /// + public override IEnumerable GetStatus() + { + //return the statuses + return new[] + { + CheckDocuments(false), + CheckMedia(false) + }; + } + + private HealthCheckStatus CheckMedia(bool fix) + { + return CheckPaths(_fixMediaPaths, _fixMediaPathsTitle, Core.Constants.UdiEntityType.Media, fix, + () => _mediaService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); + } + + private HealthCheckStatus CheckDocuments(bool fix) + { + return CheckPaths(_fixContentPaths, _fixContentPathsTitle, Core.Constants.UdiEntityType.Document, fix, + () => _contentService.CheckDataIntegrity(new ContentDataIntegrityReportOptions {FixIssues = fix})); + } + + private HealthCheckStatus CheckPaths(string actionAlias, string actionName, string entityType, bool detailedReport, Func doCheck) + { + var report = doCheck(); + + var actions = new List(); + if (!report.Ok) + { + actions.Add(new HealthCheckAction(actionAlias, Id) + { + Name = actionName + }); + } + + return new HealthCheckStatus(GetReport(report, entityType, detailedReport)) + { + ResultType = report.Ok ? StatusResultType.Success : StatusResultType.Error, + Actions = actions + }; + } + + private static string GetReport(ContentDataIntegrityReport report, string entityType, bool detailed) + { + var sb = new StringBuilder(); + + if (report.Ok) + { + sb.AppendLine($"

All {entityType} paths are valid

"); + + if (!detailed) + return sb.ToString(); + } + else + { + sb.AppendLine($"

{report.DetectedIssues.Count} invalid {entityType} paths detected.

"); + } + + if (detailed && report.DetectedIssues.Count > 0) + { + sb.AppendLine("
    "); + foreach (var issueGroup in report.DetectedIssues.GroupBy(x => x.Value.IssueType)) + { + var countByGroup = issueGroup.Count(); + var fixedByGroup = issueGroup.Count(x => x.Value.Fixed); + sb.AppendLine("
  • "); + sb.AppendLine($"{countByGroup} issues of type {issueGroup.Key} ... {fixedByGroup} fixed"); + sb.AppendLine("
  • "); + } + sb.AppendLine("
"); + } + + return sb.ToString(); + } + + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) + { + case _fixContentPaths: + return CheckDocuments(true); + case _fixMediaPaths: + return CheckMedia(true); + default: + throw new InvalidOperationException("Action not supported"); + } + } + } +} diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 00b17781e0..10b1b5dadd 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -49,13 +49,15 @@ namespace Umbraco.Web /// /// The culture for the property, if the property is invariant than this is empty internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, - ValidationResult result, string propertyAlias, string culture = "") + ValidationResult result, string propertyAlias, string culture = "", string segment = "") { if (culture == null) culture = ""; modelState.AddValidationError(result, "_Properties", propertyAlias, //if the culture is null, we'll add the term 'invariant' as part of the key - culture.IsNullOrWhiteSpace() ? "invariant" : culture); + culture.IsNullOrWhiteSpace() ? "invariant" : culture, + // if the segment is null, we'll add the term 'null' as part of the key + segment.IsNullOrWhiteSpace() ? "null" : segment); } /// @@ -63,11 +65,12 @@ namespace Umbraco.Web /// /// /// + /// /// - internal static void AddCultureValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, - string culture, string errMsg) + internal static void AddVariantValidationError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, + string culture, string segment, string errMsg) { - var key = "_content_variant_" + culture + "_"; + var key = "_content_variant_" + (culture.IsNullOrWhiteSpace() ? "invariant" : culture) + "_" + (segment.IsNullOrWhiteSpace() ? "null" : segment) + "_"; if (modelState.ContainsKey(key)) return; modelState.AddModelError(key, errMsg); } @@ -81,23 +84,28 @@ namespace Umbraco.Web /// /// A list of cultures that have property validation errors. The default culture will be returned for any invariant property errors. /// - internal static IReadOnlyList GetCulturesWithPropertyErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, - ILocalizationService localizationService, string cultureForInvariantErrors) + internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithPropertyErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, + string cultureForInvariantErrors) { - //Add any culture specific errors here - var cultureErrors = modelState.Keys + //Add any variant specific errors here + var variantErrors = modelState.Keys + .Where(key => key.StartsWith("_Properties.")) //only choose _Properties errors .Select(x => x.Split('.')) //split into parts - .Where(x => x.Length >= 3 && x[0] == "_Properties") //only choose _Properties errors - .Select(x => x[2]) //select the culture part - .Where(x => !x.IsNullOrWhiteSpace()) //if it has a value - //if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language + .Where(x => x.Length >= 4 && !x[2].IsNullOrWhiteSpace() && !x[3].IsNullOrWhiteSpace()) + .Select(x => (culture: x[2], segment: x[3])) + //if the culture is marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language //so errors for those must show up under the default lang. - .Select(x => x == "invariant" ? cultureForInvariantErrors : x) - .WhereNotNull() + //if the segment is marked "null" then return an actual null + .Select(x => + { + var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture; + var segment = x.segment == "null" ? null : x.segment; + return (culture, segment); + }) .Distinct() .ToList(); - return cultureErrors; + return variantErrors; } /// @@ -109,23 +117,33 @@ namespace Umbraco.Web /// /// A list of cultures that have validation errors. The default culture will be returned for any invariant errors. /// - internal static IReadOnlyList GetCulturesWithErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, - ILocalizationService localizationService, string cultureForInvariantErrors) + internal static IReadOnlyList<(string culture, string segment)> GetVariantsWithErrors(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, string cultureForInvariantErrors) { - var propertyCultureErrors = modelState.GetCulturesWithPropertyErrors(localizationService, cultureForInvariantErrors); + var propertyVariantErrors = modelState.GetVariantsWithPropertyErrors(cultureForInvariantErrors); - //now check the other special culture errors that are - var genericCultureErrors = modelState.Keys + //now check the other special variant errors that are + var genericVariantErrors = modelState.Keys .Where(x => x.StartsWith("_content_variant_") && x.EndsWith("_")) - .Select(x => x.TrimStart("_content_variant_").TrimEnd("_")) - .Where(x => !x.IsNullOrWhiteSpace()) + .Select(x => x.TrimStart("_content_variant_").TrimEnd("_")) + .Select(x => + { + // Format "_" + var cs = x.Split(new[] { '_' }); + return (culture: cs[0], segment: cs[1]); + }) + .Where(x => !x.culture.IsNullOrWhiteSpace()) //if it's marked "invariant" than return the default language, this is because we can only edit invariant properties on the default language //so errors for those must show up under the default lang. - .Select(x => x == "invariant" ? cultureForInvariantErrors : x) - .WhereNotNull() + //if the segment is marked "null" then return an actual null + .Select(x => + { + var culture = x.culture == "invariant" ? cultureForInvariantErrors : x.culture; + var segment = x.segment == "null" ? null : x.segment; + return (culture, segment); + }) .Distinct(); - return propertyCultureErrors.Union(genericCultureErrors).ToList(); + return propertyVariantErrors.Union(genericVariantErrors).Distinct().ToList(); } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs index b1d24c5fd2..a93be56846 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -69,6 +69,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } + //Tabs [DataMember(Name = "groups")] public IEnumerable> Groups { get; set; } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs index aff79d7b9d..2a2ec49002 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentVariationDisplay.cs @@ -24,6 +24,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "name", IsRequired = true)] public string Name { get; set; } + [DataMember(Name = "displayName")] + public string DisplayName { get; set; } + /// /// Defines the tabs containing display properties /// diff --git a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs index 6b82f74ca7..9467033aec 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DocumentTypeDisplay.cs @@ -20,9 +20,12 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "defaultTemplate")] public EntityBasic DefaultTemplate { get; set; } - + [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } + } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs index d180a68a2c..5ff1744ea2 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs @@ -54,5 +54,8 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowCultureVariant")] public bool AllowCultureVariant { get; set; } + + [DataMember(Name = "allowSegmentVariant")] + public bool AllowSegmentVariant { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs index b7f209cd20..9f53ef37e4 100644 --- a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs @@ -13,6 +13,9 @@ namespace Umbraco.Web.Models.ContentEditing Notifications = new List(); } + [DataMember(Name = "isSystemRelationType")] + public bool IsSystemRelationType { get; set; } + /// /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) /// diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index c25c932b64..865057ba24 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -52,7 +52,7 @@ namespace Umbraco.Web.Models.Mapping _tabsAndPropertiesMapper = new TabsAndPropertiesMapper(localizedTextService); _stateMapper = new ContentSavedStateMapper(); _basicStateMapper = new ContentBasicSavedStateMapper(); - _contentVariantMapper = new ContentVariantMapper(_localizationService); + _contentVariantMapper = new ContentVariantMapper(_localizationService, localizedTextService); } public void DefineMaps(UmbracoMapper mapper) @@ -104,7 +104,7 @@ namespace Umbraco.Web.Models.Mapping target.ContentDto.Properties = context.MapEnumerable(source.Properties); } - // Umbraco.Code.MapAll -Segment -Language + // Umbraco.Code.MapAll -Segment -Language -DisplayName private void Map(IContent source, ContentVariantDisplay target, MapperContext context) { target.CreateDate = source.CreateDate; diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs index cdd9ad74eb..c767e9aaf3 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeMapDefinition.cs @@ -121,6 +121,7 @@ namespace Umbraco.Web.Models.Mapping MapTypeToDisplayBase(source, target); target.AllowCultureVariant = source.VariesByCulture(); + target.AllowSegmentVariant = source.VariesBySegment(); //sync templates target.AllowedTemplates = context.MapEnumerable(source.AllowedTemplates); @@ -227,7 +228,8 @@ namespace Umbraco.Web.Models.Mapping target.ValidationRegExp = source.Validation.Pattern; target.ValidationRegExpMessage = source.Validation.PatternMessage; target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); - + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); + if (source.Id > 0) target.Id = source.Id; @@ -338,6 +340,7 @@ namespace Umbraco.Web.Models.Mapping { target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; target.DataTypeId = source.DataTypeId; target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; @@ -354,6 +357,7 @@ namespace Umbraco.Web.Models.Mapping { target.Alias = source.Alias; target.AllowCultureVariant = source.AllowCultureVariant; + target.AllowSegmentVariant = source.AllowSegmentVariant; target.DataTypeId = source.DataTypeId; target.DataTypeKey = source.DataTypeKey; target.Description = source.Description; @@ -399,6 +403,7 @@ namespace Umbraco.Web.Models.Mapping if (!(target is IMemberType)) { target.SetVariesBy(ContentVariation.Culture, source.AllowCultureVariant); + target.SetVariesBy(ContentVariation.Segment, source.AllowSegmentVariant); } // handle property groups and property types diff --git a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs index 5d076812f3..0b6be53045 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentVariantMapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using Umbraco.Core; using Umbraco.Core.Mapping; @@ -13,10 +14,12 @@ namespace Umbraco.Web.Models.Mapping internal class ContentVariantMapper { private readonly ILocalizationService _localizationService; + private readonly ILocalizedTextService _localizedTextService; - public ContentVariantMapper(ILocalizationService localizationService) + public ContentVariantMapper(ILocalizationService localizationService, ILocalizedTextService localizedTextService) { _localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); + _localizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService)); } public IEnumerable Map(IContent source, MapperContext context) @@ -113,11 +116,19 @@ namespace Umbraco.Web.Models.Mapping /// /// /// - /// Returns all segments assigned to the content including 'null' values + /// Returns all segments assigned to the content including the default `null` segment. /// private IEnumerable GetSegments(IContent content) { - return content.Properties.SelectMany(p => p.Values.Select(v => v.Segment)).Distinct(); + // The default segment (null) is always there, + // even when there is no property data at all yet + var segments = new List { null }; + + // Add actual segments based on the property values + segments.AddRange(content.Properties.SelectMany(p => p.Values.Select(v => v.Segment))); + + // Do not return a segment more than once + return segments.Distinct(); } private ContentVariantDisplay CreateVariantDisplay(MapperContext context, IContent content, Language language, string segment) @@ -130,8 +141,31 @@ namespace Umbraco.Web.Models.Mapping variantDisplay.Segment = segment; variantDisplay.Language = language; variantDisplay.Name = content.GetCultureName(language?.IsoCode); + variantDisplay.DisplayName = GetDisplayName(language, segment); return variantDisplay; } + + private string GetDisplayName(Language language, string segment) + { + var isCultureVariant = language != null; + var isSegmentVariant = !segment.IsNullOrWhiteSpace(); + + if(!isCultureVariant && !isSegmentVariant) + { + return _localizedTextService.Localize("general/default"); + } + + var parts = new List(); + + if (isSegmentVariant) + parts.Add(segment); + + if (isCultureVariant) + parts.Add(language.Name); + + return string.Join(" — ", parts); + + } } } diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs index 2f5822d1e3..370a459279 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupMapper.cs @@ -241,7 +241,8 @@ namespace Umbraco.Web.Models.Mapping SortOrder = p.SortOrder, ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, - AllowCultureVariant = p.VariesByCulture() + AllowCultureVariant = p.VariesByCulture(), + AllowSegmentVariant = p.VariesBySegment() }); } diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs index d26a867858..a1d782e678 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapDefinition.cs @@ -39,6 +39,8 @@ namespace Umbraco.Web.Models.Mapping target.Udi = Udi.Create(Constants.UdiEntityType.RelationType, source.Key); target.Path = "-1," + source.Id; + target.IsSystemRelationType = source.IsSystemRelationType(); + // Set the "friendly" and entity names for the parent and child object types if (source.ParentObjectType.HasValue) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 34d21497a2..a3f918c92c 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -502,6 +502,14 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + /// + /// Validate the and try to create a parent + /// + /// + /// + /// + /// Returns false if the parent was not found or if the kit validation failed + /// private bool BuildKit(ContentNodeKit kit, out LinkedNode parent) { // make sure parent exists @@ -512,6 +520,15 @@ namespace Umbraco.Web.PublishedCache.NuCache return false; } + // We cannot continue if there's no value. This shouldn't happen but it can happen if the database umbracoNode.path + // data is invalid/corrupt. If that is the case, the parentId might be ok but not the Path which can result in null + // because the data sort operation is by path. + if (parent.Value == null) + { + _logger.Warn($"Skip item id={kit.Node.Id}, no Data assigned for linked node with path {kit.Node.Path} and parent id {kit.Node.ParentContentId}. This can indicate data corruption for the Path value for node {kit.Node.Id}. See the Health Check dashboard in Settings to resolve data integrity issues."); + return false; + } + // make sure the kit is valid if (kit.DraftData == null && kit.PublishedData == null) { @@ -800,7 +817,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { //this zero's out the branch (recursively), if we're in a new gen this will add a NULL placeholder for the gen ClearBranchLocked(existing); - //TODO: This removes the current GEN from the tree - do we really want to do that? + //TODO: This removes the current GEN from the tree - do we really want to do that? (not sure if this is still an issue....) RemoveTreeNodeLocked(existing); } @@ -865,6 +882,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void ClearBranchLocked(ContentNode content) { + // This should never be null, all code that calls this method is null checking but we've seen + // issues of null ref exceptions in issue reports so we'll double check here + if (content == null) throw new ArgumentNullException(nameof(content)); + SetValueLocked(_contentNodes, content.Id, null); if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null); @@ -1032,6 +1053,12 @@ namespace Umbraco.Web.PublishedCache.NuCache var parent = parentLink.Value; + // We are doing a null check here but this should no longer be possible because we have a null check in BuildKit + // for the parent.Value property and we'll output a warning. However I'll leave this additional null check in place. + // see https://github.com/umbraco/Umbraco-CMS/issues/7868 + if (parent == null) + throw new PanicException($"A null Value was returned on the {nameof(parentLink)} LinkedNode with id={content.ParentContentId}, potentially your database paths are corrupted."); + // if parent has no children, clone parent + add as first child if (parent.FirstChildContentId < 0) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 19aab7ea65..694dac04df 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -20,6 +20,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource // provides efficient database access for NuCache internal class DatabaseDataSource : IDataSource { + private const int PageSize = 500; + // we want arrays, we want them all loaded, not an enumerable private Sql ContentSourcesSelect(IScope scope, Func, Sql> joins = null) @@ -79,33 +81,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateContentNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } public IEnumerable GetBranchContentSources(IScope scope, int id) { var syntax = scope.SqlContext.SqlSyntax; - var sql = ContentSourcesSelect(scope, s => s + var sql = ContentSourcesSelect(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) + .Where(x => x.NodeId == id, "x") + .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. - .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) - .Where(x => x.NodeId == id, "x") - .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - - return scope.Database.Query(sql).Select(CreateContentNodeKit); + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } public IEnumerable GetTypeContentSources(IScope scope, IEnumerable ids) { - if (!ids.Any()) return Enumerable.Empty(); + if (!ids.Any()) yield break; var sql = ContentSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Document && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateContentNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateContentNodeKit(row); } private Sql MediaSourcesSelect(IScope scope, Func, Sql> joins = null) @@ -116,11 +128,8 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource x => Alias(x.Level, "Level"), x => Alias(x.Path, "Path"), x => Alias(x.SortOrder, "SortOrder"), x => Alias(x.ParentId, "ParentId"), x => Alias(x.CreateDate, "CreateDate"), x => Alias(x.UserId, "CreatorId")) .AndSelect(x => Alias(x.ContentTypeId, "ContentTypeId")) - .AndSelect(x => Alias(x.Id, "VersionId"), x => Alias(x.Text, "EditName"), x => Alias(x.VersionDate, "EditVersionDate"), x => Alias(x.UserId, "EditWriterId")) - .AndSelect("nuEdit", x => Alias(x.Data, "EditData")) - .From(); if (joins != null) @@ -128,9 +137,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource sql = sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId) - .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) - .LeftJoin("nuEdit").On((left, right) => left.NodeId == right.NodeId && !right.Published, aliasRight: "nuEdit"); return sql; @@ -152,33 +159,43 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } public IEnumerable GetBranchMediaSources(IScope scope, int id) { var syntax = scope.SqlContext.SqlSyntax; - var sql = MediaSourcesSelect(scope, s => s - - .InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) - + var sql = MediaSourcesSelect(scope, + s => s.InnerJoin("x").On((left, right) => left.NodeId == right.NodeId || SqlText(left.Path, right.Path, (lp, rp) => $"({lp} LIKE {syntax.GetConcat(rp, "',%'")})"), aliasRight: "x")) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .Where(x => x.NodeId == id, "x") .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } public IEnumerable GetTypeMediaSources(IScope scope, IEnumerable ids) { - if (!ids.Any()) return Enumerable.Empty(); + if (!ids.Any()) yield break; var sql = MediaSourcesSelect(scope) .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media && !x.Trashed) .WhereIn(x => x.ContentTypeId, ids) .OrderBy(x => x.Level, x => x.ParentId, x => x.SortOrder); - return scope.Database.Query(sql).Select(CreateMediaNodeKit); + // We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout. + // We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that. + + foreach (var row in scope.Database.QueryPaged(PageSize, sql)) + yield return CreateMediaNodeKit(row); } private static ContentNodeKit CreateContentNodeKit(ContentSourceDto dto) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index a33d9ee427..a39e26e2b1 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -5,6 +5,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Threading; +using System.Threading.Tasks; using CSharpTest.Net.Collections; using Newtonsoft.Json; using Umbraco.Core; @@ -858,7 +859,7 @@ namespace Umbraco.Web.PublishedCache.NuCache Notify(_contentStore, payloads, RefreshContentTypesLocked); Notify(_mediaStore, payloads, RefreshMediaTypesLocked); - if (_publishedModelFactory.IsLiveFactory()) + if (_publishedModelFactory.IsLiveFactoryEnabled()) { //In the case of Pure Live - we actually need to refresh all of the content and the media //see https://github.com/umbraco/Umbraco-CMS/issues/5671 @@ -866,11 +867,29 @@ namespace Umbraco.Web.PublishedCache.NuCache //into a new DLL for the application which includes both content types and media types. //Since the models in the cache are based on these actual classes, all of the objects in the cache need to be updated //to use the newest version of the class. + + // NOTE: Ideally this can be run on background threads here which would prevent blocking the UI + // as is the case when saving a content type. Intially one would think that it won't be any different + // between running this here or in another background thread immediately after with regards to how the + // UI will respond because we already know between calling `WithSafeLiveFactoryReset` to reset the PureLive models + // and this code here, that many front-end requests could be attempted to be processed. If that is the case, those pages are going to get a + // model binding error and our ModelBindingExceptionFilter is going to to its magic to reload those pages so the end user is none the wiser. + // So whether or not this executes 'here' or on a background thread immediately wouldn't seem to make any difference except that we can return + // execution to the UI sooner. + // BUT!... there is a difference IIRC. There is still execution logic that continues after this call on this thread with the cache refreshers + // and those cache refreshers need to have the up-to-date data since other user cache refreshers will be expecting the data to be 'live'. If + // we ran this on a background thread then those cache refreshers are going to not get 'live' data when they query the content cache which + // they require. + + // These can be run side by side in parallel. using (_contentStore.GetScopedWriteLock(_scopeProvider)) + { + NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); + } + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) { - NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out var draftChanged, out var publishedChanged); - NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out var anythingChanged); + NotifyLocked(new[] { new MediaCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs index d187996df8..94f83ac4e5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs @@ -11,7 +11,7 @@ { public LinkedNode(TValue value, long gen, LinkedNode next = null) { - Value = value; + Value = value; // This is allowed to be null, we actually explicitly set this to null in ClearLocked Gen = gen; Next = next; } diff --git a/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs b/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs index 20d3e6d8e3..1b56f54569 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedSnapshotServiceBase.cs @@ -1,11 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.Cache; namespace Umbraco.Web.PublishedCache { - abstract class PublishedSnapshotServiceBase : IPublishedSnapshotService + internal abstract class PublishedSnapshotServiceBase : IPublishedSnapshotService { protected PublishedSnapshotServiceBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IVariationContextAccessor variationContextAccessor) { diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 59c394182a..f8233b1bcf 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -180,7 +180,7 @@ namespace Umbraco.Web.Runtime .Remove() .Remove() .Remove(); - + // add all known factories, devs can then modify this list on application // startup either by binding to events or in their own global.asax composition.FilteredControllerFactory() @@ -204,8 +204,12 @@ namespace Umbraco.Web.Runtime .Append() .Append() //.Append() // disabled, this is an odd finder - .Append() - .Append(); + .Append(); + //only append ContentFinderByRedirectUrl if RedirectUrlTracking is not disabled + if (composition.Configs.Settings().WebRouting.DisableRedirectUrlTracking == false) + { + composition.ContentFinders().Append(); + } composition.RegisterUnique(); diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index b30f0cbe03..64eeb6978a 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Examine; @@ -36,12 +37,14 @@ namespace Umbraco.Web.Search factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), true)); composition.RegisterUnique(factory => new ContentValueSetBuilder( factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), false)); composition.RegisterUnique, MediaValueSetBuilder>(); composition.RegisterUnique, MemberValueSetBuilder>(); diff --git a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs index 8e5e532731..5f1c1012f3 100644 --- a/src/Umbraco.Web/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web/Security/BackOfficeSignInManager.cs @@ -121,7 +121,9 @@ namespace Umbraco.Web.Security { _logger.WriteCore(TraceEventType.Information, 0, $"Login attempt failed for username {userName} from IP address {_request.RemoteIpAddress}, no content and/or media start nodes could be found for any of the user's groups", null, null); - return SignInStatus.Failure; + + // We will say its a sucessful login which it is, but they have no node access + return SignInStatus.Success; } } diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 6d156e3fc8..95de72b7bf 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -322,8 +322,10 @@ namespace Umbraco.Web.Trees var nodes = GetTreeNodesInternal(id, queryStrings); - //only render the recycle bin if we are not in dialog and the start id id still the root - if (IsDialog(queryStrings) == false && id == Constants.System.RootString) + //only render the recycle bin if we are not in dialog and the start id is still the root + //we need to check for the "application" key in the queryString because its value is required here, + //and for some reason when there are no dashboards, this parameter is missing + if (IsDialog(queryStrings) == false && id == Constants.System.RootString && queryStrings.HasKey("application")) { nodes.Add(CreateTreeNode( RecycleBinId.ToInvariantString(), diff --git a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs index aa3206b5e4..0f6f60b98d 100644 --- a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs @@ -5,6 +5,7 @@ using Umbraco.Web.WebApi.Filters; using Umbraco.Core; using Umbraco.Core.Services; using Umbraco.Web.Actions; +using Umbraco.Core.Models; namespace Umbraco.Web.Trees { @@ -16,8 +17,6 @@ namespace Umbraco.Web.Trees { protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { - //TODO: Do not allow deleting built in types - var menu = new MenuItemCollection(); if (id == Constants.System.RootString) @@ -34,7 +33,10 @@ namespace Umbraco.Web.Trees var relationType = Services.RelationService.GetRelationTypeById(int.Parse(id)); if (relationType == null) return new MenuItemCollection(); - menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.ActionAlias)); + if (relationType.IsSystemRelationType() == false) + { + menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.ActionAlias)); + } return menu; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index c657a52dcc..0cdbbaa984 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -158,6 +158,7 @@ + diff --git a/src/Umbraco.Web/UmbracoApplication.cs b/src/Umbraco.Web/UmbracoApplication.cs index f8ee238da7..f5667a5a85 100644 --- a/src/Umbraco.Web/UmbracoApplication.cs +++ b/src/Umbraco.Web/UmbracoApplication.cs @@ -2,6 +2,7 @@ using System.Threading; using System.Web; using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Logging.Serilog; using Umbraco.Core.Runtime; using Umbraco.Web.Runtime; @@ -17,16 +18,24 @@ namespace Umbraco.Web { var logger = SerilogLogger.CreateWithDefaultConfiguration(); + var runtime = new WebRuntime(this, logger, GetMainDom(logger)); + + return runtime; + } + + /// + /// Returns a new MainDom + /// + protected IMainDom GetMainDom(ILogger logger) + { // Determine if we should use the sql main dom or the default var appSettingMainDomLock = ConfigurationManager.AppSettings[Constants.AppSettings.MainDomLock]; var mainDomLock = appSettingMainDomLock == "SqlMainDomLock" ? (IMainDomLock)new SqlMainDomLock(logger) : new MainDomSemaphoreLock(logger); - - var runtime = new WebRuntime(this, logger, new MainDom(logger, mainDomLock)); - return runtime; + return new MainDom(logger, mainDomLock); } /// diff --git a/src/umbraco.sln b/src/umbraco.sln index a747f21d19..63fb856b5d 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -58,6 +58,11 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht StartServerOnDebug = "false" EndProjectSection EndProject +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" + ProjectSection(WebsiteProperties) = preProject + SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" + EndProjectSection +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Web", "Umbraco.Web\Umbraco.Web.csproj", "{651E1350-91B6-44B7-BD60-7207006D7003}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Umbraco.Core", "Umbraco.Core\Umbraco.Core.csproj", "{31785BC3-256C-4613-B2F5-A1B0BDDED8C1}" @@ -159,6 +164,7 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC}