diff --git a/.github/BUILD.md b/.github/BUILD.md index c89a1be460..a9c26f3a9b 100644 --- a/.github/BUILD.md +++ b/.github/BUILD.md @@ -1,4 +1,4 @@ -# Umbraco Cms Build +# Umbraco CMS Build ## Are you sure? @@ -66,7 +66,7 @@ The Visual Studio object is `null` when Visual Studio has not been detected (eg * `Path`: Visual Studio installation path (eg some place under `Program Files`) * `Major`: Visual Studio major version (eg `15` for VS 2017) * `Minor`: Visual Studio minor version -* `MsBUild`: the absolute path to the MsBuild executable +* `MsBuild`: the absolute path to the MsBuild executable #### GetUmbracoVersion diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 0e79851c0b..1526c54656 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -29,4 +29,4 @@ Don't rest on your laurels and never accept the status quo. Contribute and give ## Friendly -Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and builds long lasting relationships. \ No newline at end of file +Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and build long lasting relationships. \ No newline at end of file diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 84115b946a..b9f5696553 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -38,11 +38,11 @@ This document gives you a quick overview on how to get started. ### Guidelines for contributions we welcome -Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. +Not all changes are wanted, so on occasion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time. -We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes. +We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes, so we can ensure that you don't put all your hard work into something we would not be able to merge. -Remember, if an issue is in the `Up for grabs` list or you've asked for some feedback before you sent us a PR, your PR will not be closed as unwanted. +Remember, it is always worth working on an issue from the `Up for grabs` list or even asking for some feedback before you send us a PR. This way, your PR will not be closed as unwanted. ### What can I start with? @@ -64,32 +64,32 @@ Great question! The short version goes like this: * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions) * **Commit** - done? Yay! 🎉 **Important:** create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`. When you have a branch, commit your changes. Don't commit to `v8/dev`, create a new branch first. * **Push** - great, now you can push the changes up to your fork on GitHub - * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. + * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress - you can now make use of GitHub's draft pull request status, detailed [here] (https://github.blog/2019-02-14-introducing-draft-pull-requests/)). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. ![Create a pull request](img/createpullrequest.png) ### Pull requests The most successful pull requests usually look a like this: - * Fill in the required template + * 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. * 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 + * 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. -Again, these are guidelines, not strict requirements. +Again, these are guidelines, not strict requirements. However, the more information that you give to us, the more we have to work with when considering your contributions. Good documentation of a pull request can really speed up the time it takes to review and merge your work! ## Reviews -You've sent us your first contribution, congratulations! Now what? +You've sent us your first contribution - congratulations! Now what? -The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request you to make some additional changes. +The [pull request team](#the-pr-team) can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request that you make some additional changes. We have [a process in place which you can read all about](REVIEW_PROCESS.md). The very abbreviated version is: - Your PR will get a reply within 48 hours - An in-depth reply will be added within at most 2 weeks - The PR will be either merged or rejected within at most 4 weeks -- Sometimes it is difficult to meet these timelines and we'll talk to you +- Sometimes it is difficult to meet these timelines and we'll talk to you if this is the case. ### Styleguides @@ -99,21 +99,21 @@ That said, the Umbraco development team likes to follow the hints that ReSharper ### The PR team -The pull request team consists of a member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members +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: - [Anders Bjerner](https://github.com/abjerner) - [Dave Woestenborghs](https://github.com/dawoe) - [Emma Burstow](https://github.com/emmaburstow) - [Poornima Nayar](https://github.com/poornimanayar) -These wonderful volunteers will provide you with a first reply to your PR, review and test out your changes and might ask more questions. After that they'll let Umbraco HQ know if everything seems okay. +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 questions you have usually helps out multiple people with the same question. Ask away: +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: -- 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 +- 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. ## Working with the code @@ -125,19 +125,19 @@ In order to build the Umbraco source code locally, first make sure you have the * Node v10+ * npm v6.4.1+ -The easiest way to get started is to run `build.ps1` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. +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. -Alternatively, you can 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. +Alternatively, you can run `build.ps1` from the Powershell command line, which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. ![Gulp build in Visual Studio](img/gulpbuild.png) -After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser, follow the directions there to get a working Umbraco install up and running. +After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser. Follow the directions there to get a working Umbraco install up and running. ### Working with the source code Some parts of our source code are over 10 years old now. And when we say "old", we mean "mature" of course! -There's two big areas that you should know about: +There are two big areas that you should know about: 1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. You may need to run the following commands to set up gulp properly: @@ -146,20 +146,24 @@ There's two big areas that you should know about: npm install npm run build ``` + The caching for the back office has been described as 'aggressive' so we often find it's best when making back office changes to disable caching in the browser to help you to see the changes you're making. + 2. "The rest" is a C# based codebase, which is mostly ASP.NET MVC based. You can make changes, build them in Visual Studio, and hit `F5` to see the result. -To find the general areas of something you're looking to fix or improve, have a look at the following two parts of the API documentation. +To find the general areas for something you're looking to fix or improve, have a look at the following two parts of the API documentation. * [The AngularJS based backoffice files](https://our.umbraco.com/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) * [The C# application](https://our.umbraco.com/apidocs/csharp/) ### Which branch should I target for my contributions? -We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. Whatever the default is, that's where we'd like you to target your contributions. +We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), but don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `v8/dev`. If you are working on v8, this is the branch you should be targetting. For v7 contributions, please target 'v7/dev'. + +Please note: we are no longer accepting features for v7 but will continue to merge bug fixes as and when they arise. ![Which branch should I target?](img/defaultbranch.png) -### Making changes after the PR was opened +### Making changes after the PR is open If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco! @@ -167,9 +171,9 @@ If you make the corrections we ask for in the same branch and push them to your We recommend you sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. -Also, if you've submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course. +Also, if you submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course. -To sync your fork with this original one, you'll have to add the upstream url, you only have to do this once: +To sync your fork with this original one, you'll have to add the upstream url. You only have to do this once: ``` git remote add upstream https://github.com/umbraco/Umbraco-CMS.git @@ -185,3 +189,7 @@ git rebase upstream/v8/dev In this command we're syncing with the `v8/dev` branch, but you can of course choose another one if needed. (More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated)) + +### 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. diff --git a/.github/CONTRIBUTION_GUIDELINES.md b/.github/CONTRIBUTION_GUIDELINES.md index 7d2afb46bf..0ac35e6897 100644 --- a/.github/CONTRIBUTION_GUIDELINES.md +++ b/.github/CONTRIBUTION_GUIDELINES.md @@ -13,7 +13,7 @@ We’re usually able to handle small PRs pretty quickly. A community volunteer w Umbraco HQ will regularly mark newly created issues on the issue tracker with the `Up for grabs` tag. This means that the proposed changes are wanted in Umbraco but the HQ does not have the time to make them at this time. We encourage anyone to pick them up and help out. -If you do start working on something, make sure leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. +If you do start working on something, make sure to leave a small comment on the issue saying something like: "I'm working on this". That way other people stumbling upon the issue know they don't need to pick it up, someone already has. ## Large PRs New features and large refactorings - can be recognized by seeing a large number of changes, plenty of new files, updates to package manager files (NuGet’s packages.config, NPM’s packages.json, etc.). @@ -30,6 +30,6 @@ It is highly recommended that you speak to the HQ before making large, complex c ### Pull request or package? -If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and to fix bugs. +If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. Eventually, a package could "graduate" to be included in the CMS. diff --git a/.github/README.md b/.github/README.md index bdf9ef9f67..23142186df 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,4 +1,4 @@ -# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/dev)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![pullreminders](https://pullreminders.com/badge.svg)](https://pullreminders.com?ref=badge) +# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/dev)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Twitter](https://img.shields.io/twitter/follow/umbraco.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=umbraco) Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social. diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index b7bfaaff5b..97e9ef3df2 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -30,7 +30,6 @@ - diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index 2f52d03776..ba88f808c0 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -26,6 +26,6 @@ The following items will now be automatically included when creating a deploy pa system: umbraco, config\splashes and global.asax. Please read the release notes on our.umbraco.com: -http://our.umbraco.com/contribute/releases +https://our.umbraco.com/contribute/releases - Umbraco diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index f0bfb01585..2b79f95c70 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -53,7 +53,7 @@ - + @@ -76,7 +76,7 @@ - + diff --git a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs index e447f7f493..9a11b0ef3e 100644 --- a/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs +++ b/src/Umbraco.Core/Configuration/Grid/IGridEditorConfig.cs @@ -6,6 +6,7 @@ namespace Umbraco.Core.Configuration.Grid public interface IGridEditorConfig { string Name { get; } + string NameTemplate { get; } string Alias { get; } string View { get; } string Render { get; } diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs index 5163dda1f6..77ad7df0dc 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings { internal class ContentElement : UmbracoConfigurationElement, IContentSection { - private const string DefaultPreviewBadge = @"In Preview Mode - click to end"; + private const string DefaultPreviewBadge = @"
Preview modeClick to end
"; [ConfigurationProperty("imaging")] internal ContentImagingElement Imaging => (ContentImagingElement) this["imaging"]; diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index b55dc0ca18..eb2b3525a7 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -85,12 +85,7 @@ namespace Umbraco.Core /// ListView. /// public const string ListView = "Umbraco.ListView"; - - /// - /// Macro Container. - /// - public const string MacroContainer = "Umbraco.MacroContainer"; - + /// /// Media Picker. /// diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index d1012fb669..5da1062275 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Web.Hosting; using Umbraco.Core.Logging; @@ -65,7 +65,7 @@ namespace Umbraco.Core // a new process for the same application path var appPath = HostingEnvironment.ApplicationPhysicalPath; - var hash = (appId + ":::" + appPath).ToSHA1(); + var hash = (appId + ":::" + appPath).GenerateHash(); var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK"; _asyncLock = new AsyncLock(lockName); diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index e41a40e3d9..e62825101c 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -343,16 +343,20 @@ namespace Umbraco.Core.Mapping if (ctor == null) return null; - if (_ctors.ContainsKey(sourceType)) + _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => { + // Add missing constructors foreach (var c in sourceCtor) { - if (!_ctors[sourceType].TryGetValue(c.Key, out _)) - _ctors[sourceType].Add(c.Key, c.Value); - } - } - else - _ctors[sourceType] = sourceCtor; + if (!v.ContainsKey(c.Key)) + { + v.Add(c.Key, c.Value); + } + } + + return v; + }); + return ctor; } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index 7d5c05d584..94d8cfbc62 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -227,8 +227,8 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); - _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); + _database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing }); } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs index 7b2daa99ef..95b272dcb4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs @@ -74,9 +74,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 .From() .Where(x => x.NodeId == group.Key)).First(); + // check for duplicate aliases + var aliases = group.Select(x => x.Alias).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray(); + if (aliases.Distinct().Count() != aliases.Length) + throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataType.NodeId}, editor={dataType.EditorAlias}: duplicate alias."); + + // handle null/empty aliases + int index = 0; + var dictionary = group.ToDictionary(x => string.IsNullOrWhiteSpace(x.Alias) ? index++.ToString() : x.Alias); + // migrate the preValues to configuration var migrator = _preValueMigrators.GetMigrator(dataType.EditorAlias) ?? new DefaultPreValueMigrator(); - var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, group.ToDictionary(x => x.Alias, x => x)); + var config = migrator.GetConfiguration(dataType.NodeId, dataType.EditorAlias, dictionary); var json = JsonConvert.SerializeObject(config); // validate - and kill the migration if it fails diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs index 7112679de2..0c8161c9ef 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/DefaultPreValueMigrator.cs @@ -24,8 +24,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes } // assuming we don't want to fall back to array - if (aliases.Length != preValuesA.Count || aliases.Any(string.IsNullOrWhiteSpace)) - throw new InvalidOperationException($"Cannot migrate datatype w/ id={dataTypeId} preValues: duplicate or null/empty alias."); + if (aliases.Any(string.IsNullOrWhiteSpace)) + throw new InvalidOperationException($"Cannot migrate prevalues for datatype id={dataTypeId}, editor={editorAlias}: null/empty alias."); // dictionary-base prevalues return GetPreValues(preValuesA).ToDictionary(x => x.Alias, GetPreValueValue); diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs index bf28c28c9e..64e4b41186 100644 --- a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs +++ b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs @@ -67,6 +67,12 @@ namespace Umbraco.Core.Models.ContentEditing /// [DataMember(Name = "active")] public bool Active { get; set; } + + /// + /// Gets or sets the content app badge. + /// + [DataMember(Name = "badge")] + public ContentAppBadge Badge { get; set; } } } diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs new file mode 100644 index 0000000000..ba11fd338d --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadge.cs @@ -0,0 +1,39 @@ +namespace Umbraco.Core.Models.ContentEditing +{ + using System.Runtime.Serialization; + + using Umbraco.Core.Events; + + /// + /// Represents a content app badge + /// + [DataContract(Name = "badge", Namespace = "")] + public class ContentAppBadge + { + /// + /// Initializes a new instance of the class. + /// + public ContentAppBadge() + { + this.Type = ContentAppBadgeType.Default; + } + + /// + /// Gets or sets the number displayed in the badge + /// + [DataMember(Name = "count")] + public int Count { get; set; } + + /// + /// Gets or sets the type of badge to display + /// + /// + /// This controls the background color of the badge. + /// Warning will display a dark yellow badge + /// Alert will display a red badge + /// Default will display a turquoise badge + /// + [DataMember(Name = "type")] + public ContentAppBadgeType Type { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs new file mode 100644 index 0000000000..c3217099b6 --- /dev/null +++ b/src/Umbraco.Core/Models/ContentEditing/ContentAppBadgeType.cs @@ -0,0 +1,24 @@ +namespace Umbraco.Core.Models.ContentEditing +{ + using System.Runtime.Serialization; + + using Newtonsoft.Json; + using Newtonsoft.Json.Converters; + + /// + /// Represent the content app badge types + /// + [DataContract(Name = "contentAppBadgeType")] + [JsonConverter(typeof(StringEnumConverter))] + public enum ContentAppBadgeType + { + [EnumMember(Value = "default")] + Default = 0, + + [EnumMember(Value = "warning")] + Warning = 1, + + [EnumMember(Value = "alert")] + Alert = 2 + } +} diff --git a/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs new file mode 100644 index 0000000000..050a999cc2 --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/IMemberEntitySlim.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Core.Models.Entities +{ + public interface IMemberEntitySlim : IContentEntitySlim + { + + } +} diff --git a/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs new file mode 100644 index 0000000000..335e269467 --- /dev/null +++ b/src/Umbraco.Core/Models/Entities/MemberEntitySlim.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Core.Models.Entities +{ + public class MemberEntitySlim : EntitySlim, IMemberEntitySlim + { + public string ContentTypeAlias { get; set; } + + /// + public string ContentTypeIcon { get; set; } + + /// + public string ContentTypeThumbnail { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs index cf7df4fb86..e00ac4ba15 100644 --- a/src/Umbraco.Core/Models/UserExtensions.cs +++ b/src/Umbraco.Core/Models/UserExtensions.cs @@ -67,7 +67,7 @@ namespace Umbraco.Core.Models if (user.Avatar.IsNullOrWhiteSpace()) { - var gravatarHash = user.Email.ToMd5(); + var gravatarHash = user.Email.GenerateHash(); var gravatarUrl = "https://www.gravatar.com/avatar/" + gravatarHash + "?d=404"; //try Gravatar diff --git a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs b/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs deleted file mode 100644 index 48edee3c94..0000000000 --- a/src/Umbraco.Core/Persistence/DatabasenodeLockExtensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Runtime.CompilerServices; - -namespace Umbraco.Core.Persistence -{ - internal static class DatabaseNodeLockExtensions - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ValidateDatabase(IUmbracoDatabase database) - { - if (database == null) - throw new ArgumentNullException("database"); - if (database.GetCurrentTransactionIsolationLevel() < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - } - - // updating a record within a repeatable-read transaction gets an exclusive lock on - // that record which will be kept until the transaction is ended, effectively locking - // out all other accesses to that record - thus obtaining an exclusive lock over the - // protected resources. - public static void AcquireLockNodeWriteLock(this IUmbracoDatabase database, int nodeId) - { - ValidateDatabase(database); - - database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", - new { @id = nodeId }); - } - - // reading a record within a repeatable-read transaction gets a shared lock on - // that record which will be kept until the transaction is ended, effectively preventing - // other write accesses to that record - thus obtaining a shared lock over the protected - // resources. - public static void AcquireLockNodeReadLock(this IUmbracoDatabase database, int nodeId) - { - ValidateDatabase(database); - - database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", - new { @id = nodeId }); - } - } -} diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs index afb419ebd6..3a44cb10b4 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs @@ -7,5 +7,12 @@ namespace Umbraco.Core.Persistence.Repositories public interface IDataTypeRepository : IReadWriteQueryRepository { IEnumerable> Move(IDataType toMove, EntityContainer container); + + /// + /// Returns a dictionary of content type s and the property type aliases that use a + /// + /// + /// + IReadOnlyDictionary> FindUsages(int id); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index f2efb03ba4..6385482686 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -1,4 +1,5 @@ -using System; + +using System; using System.Collections.Generic; using System.Data; using System.Globalization; @@ -218,6 +219,7 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { + CorrectPropertyTypeVariations(entity); ValidateVariations(entity); var dto = ContentTypeFactory.BuildContentTypeDto(entity); @@ -410,26 +412,7 @@ AND umbracoNode.id <> @id", // note: this only deals with *local* property types, we're dealing w/compositions later below foreach (var propertyType in entity.PropertyTypes) { - if (contentTypeVariationChanging) - { - // content type is changing - switch (newContentTypeVariation) - { - case ContentVariation.Nothing: // changing to Nothing - // all property types must change to Nothing - propertyType.Variations = ContentVariation.Nothing; - break; - case ContentVariation.Culture: // changing to Culture - // all property types can remain Nothing - break; - case ContentVariation.CultureAndSegment: - case ContentVariation.Segment: - default: - throw new NotSupportedException(); // TODO: Support this - } - } - - // then, track each property individually + // track each property individually if (propertyType.IsPropertyDirty("Variations")) { // allocate the list only when needed @@ -455,23 +438,19 @@ AND umbracoNode.id <> @id", // via composition, with their original variations (ie not filtered by this // content type variations - we need this true value to make decisions. - foreach (var propertyType in ((ContentTypeCompositionBase)entity).RawComposedPropertyTypes) + propertyTypeVariationChanges = propertyTypeVariationChanges ?? new Dictionary(); + + foreach (var composedPropertyType in ((ContentTypeCompositionBase)entity).RawComposedPropertyTypes) { - if (propertyType.VariesBySegment() || newContentTypeVariation.VariesBySegment()) - throw new NotSupportedException(); // TODO: support this + if (composedPropertyType.Variations == ContentVariation.Nothing) continue; - if (propertyType.Variations == ContentVariation.Culture) - { - if (propertyTypeVariationChanges == null) - propertyTypeVariationChanges = new Dictionary(); + // Determine target variation of the composed property type. + // The composed property is only considered culture variant when the base content type is also culture variant. + // The composed property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + var target = newContentTypeVariation & composedPropertyType.Variations; - // if content type moves to Culture, property type becomes Culture here again - // if content type moves to Nothing, property type becomes Nothing here - if (newContentTypeVariation == ContentVariation.Culture) - propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Nothing, ContentVariation.Culture); - else if (newContentTypeVariation == ContentVariation.Nothing) - propertyTypeVariationChanges[propertyType.Id] = (ContentVariation.Culture, ContentVariation.Nothing); - } + propertyTypeVariationChanges[composedPropertyType.Id] = (composedPropertyType.Variations, target); } } @@ -512,7 +491,7 @@ AND umbracoNode.id <> @id", var impacted = GetImpactedContentTypes(entity, all); // if some property types have actually changed, move their variant data - if (propertyTypeVariationChanges != null) + if (propertyTypeVariationChanges?.Count > 0) MovePropertyTypeVariantData(propertyTypeVariationChanges, impacted); // deal with orphan properties: those that were in a deleted tab, @@ -524,23 +503,40 @@ AND umbracoNode.id <> @id", CommonRepository.ClearCache(); // always } + /// + /// Corrects the property type variations for the given entity + /// to make sure the property type variation is compatible with the + /// variation set on the entity itself. + /// + /// Entity to correct properties for + private void CorrectPropertyTypeVariations(IContentTypeComposition entity) + { + // Update property variations based on the content type variation + foreach (var propertyType in entity.PropertyTypes) + { + // Determine variation for the property type. + // The property is only considered culture variant when the base content type is also culture variant. + // The property is only considered segment variant when the base content type is also segment variant. + // Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture + propertyType.Variations = entity.Variations & propertyType.Variations; + } + } + /// /// Ensures that no property types are flagged for a variance that is not supported by the content type itself /// - /// + /// The entity for which the property types will be validated private void ValidateVariations(IContentTypeComposition entity) { - //if the entity does not vary at all, then the property cannot have a variance value greater than it - if (entity.Variations == ContentVariation.Nothing) + foreach (var prop in entity.PropertyTypes) { - foreach (var prop in entity.PropertyTypes) - { - if (prop.IsPropertyDirty(nameof(prop.Variations)) && prop.Variations > entity.Variations) - throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); - } - + // The variation of a property is only allowed if all its variation flags + // are also set on the entity itself. It cannot set anything that is not also set by the content type. + // For example, when entity.Variations is set to Culture a property cannot be set to Segment. + var isValid = entity.Variations.HasFlag(prop.Variations); + if (!isValid) + throw new InvalidOperationException($"The property {prop.Alias} cannot have variations of {prop.Variations} with the content type variations of {entity.Variations}"); } - } private IEnumerable GetImpactedContentTypes(IContentTypeComposition contentType, IEnumerable all) @@ -661,27 +657,27 @@ AND umbracoNode.id <> @id", var impactedL = impacted.Select(x => x.Id).ToList(); //Group by the "To" variation so we can bulk update in the correct batches - foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value.ToVariation)) + foreach (var grouping in propertyTypeChanges.GroupBy(x => x.Value)) { var propertyTypeIds = grouping.Select(x => x.Key).ToList(); - var toVariation = grouping.Key; + var (FromVariation, ToVariation) = grouping.Key; - switch (toVariation) + var fromCultureEnabled = FromVariation.HasFlag(ContentVariation.Culture); + var toCultureEnabled = ToVariation.HasFlag(ContentVariation.Culture); + + if (!fromCultureEnabled && toCultureEnabled) { - case ContentVariation.Culture: - CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); - CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - break; - case ContentVariation.Nothing: - CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); - CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); - RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); - break; - case ContentVariation.CultureAndSegment: - case ContentVariation.Segment: - default: - throw new NotSupportedException(); // TODO: Support this + // Culture has been enabled + CopyPropertyData(null, defaultLanguageId, propertyTypeIds, impactedL); + CopyTagData(null, defaultLanguageId, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); + } + else if (fromCultureEnabled && !toCultureEnabled) + { + // Culture has been disabled + CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); + CopyTagData(defaultLanguageId, null, propertyTypeIds, impactedL); + RenormalizeDocumentEditedFlags(propertyTypeIds, impactedL); } } } @@ -693,78 +689,72 @@ AND umbracoNode.id <> @id", { var defaultLanguageId = GetDefaultLanguageId(); - switch (toVariation) + var cultureIsNotEnabled = !fromVariation.HasFlag(ContentVariation.Culture); + var cultureWillBeEnabled = toVariation.HasFlag(ContentVariation.Culture); + + if (cultureIsNotEnabled && cultureWillBeEnabled) { - case ContentVariation.Culture: + //move the names + //first clear out any existing names that might already exists under the default lang + //there's 2x tables to update - //move the names - //first clear out any existing names that might already exists under the default lang - //there's 2x tables to update + //clear out the versionCultureVariation table + var sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.Id, x => x.VersionId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + var sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); - //clear out the versionCultureVariation table - var sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.Id, x => x.VersionId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - var sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); - Database.Execute(sqlDelete); + //clear out the documentCultureVariation table + sqlSelect = Sql().Select(x => x.Id) + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id) + .Where(x => x.LanguageId == defaultLanguageId); + sqlDelete = Sql() + .Delete() + .WhereIn(x => x.Id, sqlSelect); - //clear out the documentCultureVariation table - sqlSelect = Sql().Select(x => x.Id) - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id) - .Where(x => x.LanguageId == defaultLanguageId); - sqlDelete = Sql() - .Delete() - .WhereIn(x => x.Id, sqlSelect); + Database.Execute(sqlDelete); - Database.Execute(sqlDelete); + //now we need to insert names into these 2 tables based on the invariant data - //now we need to insert names into these 2 tables based on the invariant data + //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang + var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) + .Append($", {defaultLanguageId}") //default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - //insert rows into the versionCultureVariationDto table based on the data from contentVersionDto for the default lang - var cols = Sql().Columns(x => x.VersionId, x => x.Name, x => x.UpdateUserId, x => x.UpdateDate, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.Id, x => x.Text, x => x.UserId, x => x.VersionDate) - .Append($", {defaultLanguageId}") //default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - var sqlInsert = Sql($"INSERT INTO {ContentVersionCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); - Database.Execute(sqlInsert); + //insert rows into the documentCultureVariation table + cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); + sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) + .AndSelect(x => x.Text) + .Append($", 1, {defaultLanguageId}") //make Available + default language ID + .From() + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .InnerJoin().On(x => x.NodeId, x => x.NodeId) + .Where(x => x.ContentTypeId == contentType.Id); + sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); - //insert rows into the documentCultureVariation table - cols = Sql().Columns(x => x.NodeId, x => x.Edited, x => x.Published, x => x.Name, x => x.Available, x => x.LanguageId); - sqlSelect = Sql().Select(x => x.NodeId, x => x.Edited, x => x.Published) - .AndSelect(x => x.Text) - .Append($", 1, {defaultLanguageId}") //make Available + default language ID - .From() - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .InnerJoin().On(x => x.NodeId, x => x.NodeId) - .Where(x => x.ContentTypeId == contentType.Id); - sqlInsert = Sql($"INSERT INTO {DocumentCultureVariationDto.TableName} ({cols})").Append(sqlSelect); + Database.Execute(sqlInsert); + } + else + { + //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. - Database.Execute(sqlInsert); - - break; - case ContentVariation.Nothing: - - //we don't need to move the names! this is because we always keep the invariant names with the name of the default language. - - //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( - // if we want these SQL statements back, look into GIT history - - break; - case ContentVariation.CultureAndSegment: - case ContentVariation.Segment: - default: - throw new NotSupportedException(); // TODO: Support this + //however, if we were to move names, we could do this: BUT this doesn't work with SQLCE, for that we'd have to update row by row :( + // if we want these SQL statements back, look into GIT history } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs index dac8fda5ec..9ccf6e9623 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs @@ -279,6 +279,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return moveInfo; } + public IReadOnlyDictionary> FindUsages(int id) + { + if (id == default) + return new Dictionary>(); + + var sql = Sql() + .Select(ct => ct.Select(node => node.NodeDto)) + .AndSelect(pt => Alias(pt.Alias, "ptAlias"), pt => Alias(pt.Name, "ptName")) + .From() + .InnerJoin().On(ct => ct.NodeId, pt => pt.ContentTypeId) + .InnerJoin().On(n => n.NodeId, ct => ct.NodeId) + .Where(pt => pt.DataTypeId == id) + .OrderBy(node => node.NodeId) + .AndBy(pt => pt.Alias); + + var dtos = Database.FetchOneToMany(ct => ct.PropertyTypes, sql); + + return dtos.ToDictionary( + x => (Udi)new GuidUdi(ObjectTypes.GetUdiType(x.NodeDto.NodeObjectType.Value), x.NodeDto.UniqueId).EnsureClosed(), + x => (IEnumerable)x.PropertyTypes.Select(p => p.Alias).ToList()); + } + private string EnsureUniqueNodeName(string nodeName, int id = 0) { var template = SqlContext.Templates.Get("Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName", tsql => tsql @@ -291,5 +313,24 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return SimilarNodeName.GetUniqueName(names, id, nodeName); } + + + [TableName(Constants.DatabaseSchema.Tables.ContentType)] + private class ContentTypeReferenceDto : ContentTypeDto + { + [ResultColumn] + [Reference(ReferenceType.Many)] + public List PropertyTypes { get; set; } + } + + [TableName(Constants.DatabaseSchema.Tables.PropertyType)] + private class PropertyTypeReferenceDto + { + [Column("ptAlias")] + public string Alias { get; set; } + + [Column("ptName")] + public string Name { get; set; } + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index 344557d815..2649b9993f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -248,14 +248,63 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return dto == null ? null : MapDtoToContent(dto); } + // deletes a specific version + public override void DeleteVersion(int versionId) + { + // TODO: test object node type? + + // get the version we want to delete + var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersion", tsql => + tsql.Select() + .AndSelect() + .From() + .InnerJoin() + .On((c, d) => c.Id == d.Id) + .Where(x => x.Id == SqlTemplate.Arg("versionId")) + ); + var versionDto = Database.Fetch(template.Sql(new { versionId })).FirstOrDefault(); + + // nothing to delete + if (versionDto == null) + return; + + // don't delete the current or published version + if (versionDto.ContentVersionDto.Current) + throw new InvalidOperationException("Cannot delete the current version."); + else if (versionDto.Published) + throw new InvalidOperationException("Cannot delete the published version."); + + PerformDeleteVersion(versionDto.ContentVersionDto.NodeId, versionId); + } + + // deletes all versions of an entity, older than a date. + public override void DeleteVersions(int nodeId, DateTime versionDate) + { + // TODO: test object node type? + + // get the versions we want to delete, excluding the current one + var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetVersions", tsql => + tsql.Select() + .From() + .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) + ); + var versionDtos = Database.Fetch(template.Sql(new { nodeId, versionDate })); + foreach (var versionDto in versionDtos) + PerformDeleteVersion(versionDto.NodeId, versionDto.Id); + } + protected override void PerformDeleteVersion(int id, int versionId) { // raise event first else potential FK issues OnUowRemovingVersion(new ScopedVersionEventArgs(AmbientScope, id, versionId)); Database.Delete("WHERE versionId = @versionId", new { versionId }); - Database.Delete("WHERE id = @versionId", new { versionId }); + Database.Delete("WHERE versionId = @versionId", new { versionId }); Database.Delete("WHERE id = @versionId", new { versionId }); + Database.Delete("WHERE id = @versionId", new { versionId }); } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index b6d39fe54f..161db543ba 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs @@ -42,8 +42,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetBaseWhere(isContent, isMedia, false, x => + var sql = GetBaseWhere(isContent, isMedia, isMember, false, x => { if (filter == null) return; foreach (var filterClause in filter.GetWhereClauses()) @@ -54,7 +55,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, sql, ordering.IsEmpty); + sql = AddGroupBy(isContent, isMedia, isMember, sql, ordering.IsEmpty); if (!ordering.IsEmpty) { @@ -81,6 +82,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement dtos = page.Items; totalRecords = page.TotalItems; } + else if (isMember) + { + var page = Database.Page(pageIndexToFetch, pageSize, sql); + dtos = page.Items; + totalRecords = page.TotalItems; + } else { var page = Database.Page(pageIndexToFetch, pageSize, sql); @@ -88,7 +95,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; } - var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray(); + var entities = dtos.Select(x => BuildEntity(isContent, isMedia, isMember, x)).ToArray(); if (isContent) BuildVariants(entities.Cast()); @@ -98,13 +105,13 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public IEntitySlim Get(Guid key) { - var sql = GetBaseWhere(false, false, false, key); + var sql = GetBaseWhere(false, false, false, false, key); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, dto); + return dto == null ? null : BuildEntity(false, false, false, dto); } - private IEntitySlim GetEntity(Sql sql, bool isContent, bool isMedia) + private IEntitySlim GetEntity(Sql sql, bool isContent, bool isMedia, bool isMember) { //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) @@ -120,7 +127,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (dto == null) return null; - var entity = BuildEntity(false, isMedia, dto); + var entity = BuildEntity(false, isMedia, isMember, dto); return entity; } @@ -129,25 +136,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; - var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, key); - return GetEntity(sql, isContent, isMedia); + var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, key); + return GetEntity(sql, isContent, isMedia, isMember); } public IEntitySlim Get(int id) { - var sql = GetBaseWhere(false, false, false, id); + var sql = GetBaseWhere(false, false, false, false, id); var dto = Database.FirstOrDefault(sql); - return dto == null ? null : BuildEntity(false, false, dto); + return dto == null ? null : BuildEntity(false, false, false, dto); } public IEntitySlim Get(int id, Guid objectTypeId) { var isContent = objectTypeId == Constants.ObjectTypes.Document || objectTypeId == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectTypeId == Constants.ObjectTypes.Media; + var isMember = objectTypeId == Constants.ObjectTypes.Member; - var sql = GetFullSqlForEntityType(isContent, isMedia, objectTypeId, id); - return GetEntity(sql, isContent, isMedia); + var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectTypeId, id); + return GetEntity(sql, isContent, isMedia, isMember); } public IEnumerable GetAll(Guid objectType, params int[] ids) @@ -164,7 +173,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement : PerformGetAll(objectType); } - private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia) + private IEnumerable GetEntities(Sql sql, bool isContent, bool isMedia, bool isMember) { //isContent is going to return a 1:M result now with the variants so we need to do different things if (isContent) @@ -180,7 +189,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement ? (IEnumerable)Database.Fetch(sql) : Database.Fetch(sql); - var entities = dtos.Select(x => BuildEntity(false, isMedia, x)).ToArray(); + var entities = dtos.Select(x => BuildEntity(false, isMedia, isMember, x)).ToArray(); return entities; } @@ -189,9 +198,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetFullSqlForEntityType(isContent, isMedia, objectType, filter); - return GetEntities(sql, isContent, isMedia); + var sql = GetFullSqlForEntityType(isContent, isMedia, isMember, objectType, filter); + return GetEntities(sql, isContent, isMedia, isMember); } public IEnumerable GetAllPaths(Guid objectType, params int[] ids) @@ -218,26 +228,27 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public IEnumerable GetByQuery(IQuery query) { - var sqlClause = GetBase(false, false, null); + var sqlClause = GetBase(false, false, false, null); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - sql = AddGroupBy(false, false, sql, true); + sql = AddGroupBy(false, false, false, sql, true); var dtos = Database.Fetch(sql); - return dtos.Select(x => BuildEntity(false, false, x)).ToList(); + return dtos.Select(x => BuildEntity(false, false, false, x)).ToList(); } public IEnumerable GetByQuery(IQuery query, Guid objectType) { var isContent = objectType == Constants.ObjectTypes.Document || objectType == Constants.ObjectTypes.DocumentBlueprint; var isMedia = objectType == Constants.ObjectTypes.Media; + var isMember = objectType == Constants.ObjectTypes.Member; - var sql = GetBaseWhere(isContent, isMedia, false, null, objectType); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, null, objectType); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); - sql = AddGroupBy(isContent, isMedia, sql, true); + sql = AddGroupBy(isContent, isMedia, isMember, sql, true); - return GetEntities(sql, isContent, isMedia); + return GetEntities(sql, isContent, isMedia, isMember); } public UmbracoObjectTypes GetObjectType(int id) @@ -329,29 +340,29 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } // gets the full sql for a given object type and a given unique id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId) + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Guid uniqueId) { - var sql = GetBaseWhere(isContent, isMedia, false, objectType, uniqueId); - return AddGroupBy(isContent, isMedia, sql, true); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, uniqueId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the full sql for a given object type and a given node id - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, int nodeId) + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, int nodeId) { - var sql = GetBaseWhere(isContent, isMedia, false, objectType, nodeId); - return AddGroupBy(isContent, isMedia, sql, true); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, objectType, nodeId); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the full sql for a given object type, with a given filter - protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Action> filter) + protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, bool isMember, Guid objectType, Action> filter) { - var sql = GetBaseWhere(isContent, isMedia, false, filter, objectType); - return AddGroupBy(isContent, isMedia, sql, true); + var sql = GetBaseWhere(isContent, isMedia, isMember, false, filter, objectType); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM [+ filter] sql // always from the 'current' content version - protected Sql GetBase(bool isContent, bool isMedia, Action> filter, bool isCount = false) + protected Sql GetBase(bool isContent, bool isMedia, bool isMember, Action> filter, bool isCount = false) { var sql = Sql(); @@ -366,7 +377,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement .AndSelect(x => x.SortOrder, x => x.UniqueId, x => x.Text, x => x.NodeObjectType, x => x.CreateDate) .Append(", COUNT(child.id) AS children"); - if (isContent || isMedia) + if (isContent || isMedia || isMember) sql .AndSelect(x => Alias(x.Id, "versionId")) .AndSelect(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); @@ -387,7 +398,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement sql .From(); - if (isContent || isMedia) + if (isContent || isMedia || isMember) { sql .InnerJoin().On((left, right) => left.NodeId == right.NodeId && right.Current) @@ -422,49 +433,49 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // gets the base SELECT + FROM [+ filter] + WHERE sql // for a given object type, with a given filter - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Action> filter, Guid objectType) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Action> filter, Guid objectType) { - return GetBase(isContent, isMedia, filter, isCount) + return GetBase(isContent, isMedia, isMember, filter, isCount) .Where(x => x.NodeObjectType == objectType); } // gets the base SELECT + FROM + WHERE sql // for a given node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, int id) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, int id) { - var sql = GetBase(isContent, isMedia, null, isCount) + var sql = GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.NodeId == id); - return AddGroupBy(isContent, isMedia, sql, true); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM + WHERE sql // for a given unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid uniqueId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid uniqueId) { - var sql = GetBase(isContent, isMedia, null, isCount) + var sql = GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.UniqueId == uniqueId); - return AddGroupBy(isContent, isMedia, sql, true); + return AddGroupBy(isContent, isMedia, isMember, sql, true); } // gets the base SELECT + FROM + WHERE sql // for a given object type and node id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, int nodeId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, int nodeId) { - return GetBase(isContent, isMedia, null, isCount) + return GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.NodeId == nodeId && x.NodeObjectType == objectType); } // gets the base SELECT + FROM + WHERE sql // for a given object type and unique id - protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isCount, Guid objectType, Guid uniqueId) + protected Sql GetBaseWhere(bool isContent, bool isMedia, bool isMember, bool isCount, Guid objectType, Guid uniqueId) { - return GetBase(isContent, isMedia, null, isCount) + return GetBase(isContent, isMedia, isMember, null, isCount) .Where(x => x.UniqueId == uniqueId && x.NodeObjectType == objectType); } // gets the GROUP BY / ORDER BY sql // required in order to count children - protected Sql AddGroupBy(bool isContent, bool isMedia, Sql sql, bool defaultSort) + protected Sql AddGroupBy(bool isContent, bool isMedia, bool isMember, Sql sql, bool defaultSort) { sql .GroupBy(x => x.NodeId, x => x.Trashed, x => x.ParentId, x => x.UserId, x => x.Level, x => x.Path) @@ -483,7 +494,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement } - if (isContent || isMedia) + if (isContent || isMedia || isMember) sql .AndBy(x => x.Id) .AndBy(x => x.Alias, x => x.Icon, x => x.Thumbnail, x => x.IsContainer, x => x.Variations); @@ -528,6 +539,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement public string MediaPath { get; set; } } + private class MemberEntityDto : BaseDto + { + } + public class VariantInfoDto { public int NodeId { get; set; } @@ -574,12 +589,14 @@ namespace Umbraco.Core.Persistence.Repositories.Implement #region Factory - private EntitySlim BuildEntity(bool isContent, bool isMedia, BaseDto dto) + private EntitySlim BuildEntity(bool isContent, bool isMedia, bool isMember, BaseDto dto) { if (isContent) return BuildDocumentEntity(dto); if (isMedia) return BuildMediaEntity(dto); + if (isMember) + return BuildMemberEntity(dto); // EntitySlim does not track changes var entity = new EntitySlim(); @@ -644,6 +661,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement return entity; } + private MemberEntitySlim BuildMemberEntity(BaseDto dto) + { + // EntitySlim does not track changes + var entity = new MemberEntitySlim(); + BuildEntity(entity, dto); + + entity.ContentTypeAlias = dto.Alias; + entity.ContentTypeIcon = dto.Icon; + entity.ContentTypeThumbnail = dto.Thumbnail; + + return entity; + } + #endregion } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs index 1a8b2b8821..a905294417 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -182,6 +182,19 @@ namespace Umbraco.Core.Persistence.Repositories.Implement throw new InvalidOperationException($"Cannot save the default language ({entity.IsoCode}) as non-default. Make another language the default language instead."); } + if (entity.IsPropertyDirty(nameof(ILanguage.IsoCode))) + { + //if the iso code is changing, ensure there's not another lang with the same code already assigned + var sameCode = Sql() + .SelectCount() + .From() + .Where(x => x.IsoCode == entity.IsoCode && x.Id != entity.Id); + + var countOfSameCode = Database.ExecuteScalar(sameCode); + if (countOfSameCode > 0) + throw new InvalidOperationException($"Cannot update the language to a new culture: {entity.IsoCode} since that culture is already assigned to another language entity."); + } + // fallback cycles are detected at service level // update diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs index 1fc3568fc0..892122dff9 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberRepository.cs @@ -133,7 +133,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement // joining the type so we can do a query against the member type - not sure if this adds much overhead or not? // the execution plan says it doesn't so we'll go with that and in that case, it might be worth joining the content - // types by default on the document and media repo's so we can query by content type there too. + // types by default on the document and media repos so we can query by content type there too. .InnerJoin().On(left => left.ContentTypeId, right => right.NodeId); sql.Where(x => x.NodeObjectType == NodeObjectTypeId); @@ -546,6 +546,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (ordering.OrderBy.InvariantEquals("userName")) return SqlSyntax.GetFieldName(x => x.LoginName); + if (ordering.OrderBy.InvariantEquals("updateDate")) + return SqlSyntax.GetFieldName(x => x.VersionDate); + + if (ordering.OrderBy.InvariantEquals("createDate")) + return SqlSyntax.GetFieldName(x => x.CreateDate); + + if (ordering.OrderBy.InvariantEquals("contentTypeAlias")) + return SqlSyntax.GetFieldName(x => x.Alias); + return base.ApplySystemOrdering(ref sql, ordering); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs index baac02b6bf..acf6bb7df2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/RedirectUrlRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -105,7 +106,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); CreateDateUtc = redirectUrl.CreateDateUtc, Url = redirectUrl.Url, Culture = redirectUrl.Culture, - UrlHash = redirectUrl.Url.ToSHA1() + UrlHash = redirectUrl.Url.GenerateHash() }; } @@ -134,7 +135,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IRedirectUrl Get(string url, Guid contentKey, string culture) { - var urlHash = url.ToSHA1(); + var urlHash = url.GenerateHash(); var sql = GetBaseQuery(false).Where(x => x.Url == url && x.UrlHash == urlHash && x.ContentKey == contentKey && x.Culture == culture); var dto = Database.Fetch(sql).FirstOrDefault(); return dto == null ? null : Map(dto); @@ -157,7 +158,7 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID"); public IRedirectUrl GetMostRecentUrl(string url) { - var urlHash = url.ToSHA1(); + var urlHash = url.GenerateHash(); var sql = GetBaseQuery(false) .Where(x => x.Url == url && x.UrlHash == urlHash) .OrderByDescending(x => x.CreateDateUtc); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs index 55625ff04e..7ae001bf24 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/ISqlSyntaxProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Data; using System.Text.RegularExpressions; using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -76,6 +77,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax string ConvertIntegerToOrderableString { get; } string ConvertDateToOrderableString { get; } string ConvertDecimalToOrderableString { get; } + + /// + /// Returns the default isolation level for the database + /// + IsolationLevel DefaultIsolationLevel { get; } IEnumerable GetTablesInSchema(IDatabase db); IEnumerable GetColumnsInSchema(IDatabase db); @@ -121,5 +127,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax /// unspecified. /// bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName); + + void ReadLock(IDatabase db, params int[] lockIds); + void WriteLock(IDatabase db, params int[] lockIds); } } diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs index cb4b7a5176..2ed0fb878c 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlCeSyntaxProvider.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Data; +using System.Data.SqlServerCe; using System.Linq; using NPoco; using Umbraco.Core.Persistence.DatabaseAnnotations; @@ -52,6 +54,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax return "(" + string.Join("+", args) + ")"; } + public override System.Data.IsolationLevel DefaultIsolationLevel => System.Data.IsolationLevel.RepeatableRead; + public override string FormatColumnRename(string tableName, string oldName, string newName) { //NOTE Sql CE doesn't support renaming a column, so a new column needs to be created, then copy data and finally remove old column @@ -152,6 +156,39 @@ where table_name=@0 and column_name=@1", tableName, columnName).FirstOrDefault() return result > 0; } + public override void WriteLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + db.Execute(@"SET LOCK_TIMEOUT 1800;"); + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = db.Execute(@"UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + } + + public override void ReadLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) + throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + } + protected override string FormatIdentity(ColumnDefinition column) { return column.IsIdentity ? GetIdentityString(column) : string.Empty; diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index fab7526a6b..3d0adf175e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Data.SqlClient; using System.Linq; using NPoco; using Umbraco.Core.Logging; @@ -179,6 +180,8 @@ namespace Umbraco.Core.Persistence.SqlSyntax return items.Select(x => x.TABLE_NAME).Cast().ToList(); } + public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted; + public override IEnumerable GetColumnsInSchema(IDatabase db) { var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())"); @@ -246,6 +249,41 @@ where tbl.[name]=@0 and col.[name]=@1;", tableName, columnName) return result > 0; } + public override void WriteLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + db.Execute(@"SET LOCK_TIMEOUT 1800;"); + var i = db.Execute(@"UPDATE umbracoLock WITH (REPEATABLEREAD) SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); + if (i == 0) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist."); + } + } + + + public override void ReadLock(IDatabase db, params int[] lockIds) + { + // soon as we get Database, a transaction is started + + if (db.Transaction.IsolationLevel < IsolationLevel.ReadCommitted) + throw new InvalidOperationException("A transaction with minimum ReadCommitted isolation level is required."); + + // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks + foreach (var lockId in lockIds) + { + var i = db.ExecuteScalar("SELECT value FROM umbracoLock WITH (REPEATABLEREAD) WHERE id=@id", new { id = lockId }); + if (i == null) // ensure we are actually locking! + throw new ArgumentException($"LockObject with id={lockId} does not exist.", nameof(lockIds)); + } + } + public override string FormatColumnRename(string tableName, string oldName, string newName) { return string.Format(RenameColumn, tableName, oldName, newName); diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs index 0c27ac2d50..b2e03df96e 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs @@ -200,7 +200,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax return "NVARCHAR"; } - + + public abstract IsolationLevel DefaultIsolationLevel { get; } + public virtual IEnumerable GetTablesInSchema(IDatabase db) { return new List(); @@ -225,6 +227,9 @@ namespace Umbraco.Core.Persistence.SqlSyntax public abstract bool TryGetDefaultConstraint(IDatabase db, string tableName, string columnName, out string constraintName); + public abstract void ReadLock(IDatabase db, params int[] lockIds); + public abstract void WriteLock(IDatabase db, params int[] lockIds); + public virtual bool DoesTableExist(IDatabase db, string tableName) { return false; diff --git a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs index 51e0172f35..072813b4e6 100644 --- a/src/Umbraco.Core/Persistence/UmbracoDatabase.cs +++ b/src/Umbraco.Core/Persistence/UmbracoDatabase.cs @@ -20,9 +20,6 @@ namespace Umbraco.Core.Persistence /// public class UmbracoDatabase : Database, IUmbracoDatabase { - // Umbraco's default isolation level is RepeatableRead - private const IsolationLevel DefaultIsolationLevel = IsolationLevel.RepeatableRead; - private readonly ILogger _logger; private readonly RetryPolicy _connectionRetryPolicy; private readonly RetryPolicy _commandRetryPolicy; @@ -38,7 +35,7 @@ namespace Umbraco.Core.Persistence /// Also used by DatabaseBuilder for creating databases and installing/upgrading. /// public UmbracoDatabase(string connectionString, ISqlContext sqlContext, DbProviderFactory provider, ILogger logger, RetryPolicy connectionRetryPolicy = null, RetryPolicy commandRetryPolicy = null) - : base(connectionString, sqlContext.DatabaseType, provider, DefaultIsolationLevel) + : base(connectionString, sqlContext.DatabaseType, provider, sqlContext.SqlSyntax.DefaultIsolationLevel) { SqlContext = sqlContext; @@ -54,7 +51,7 @@ namespace Umbraco.Core.Persistence /// /// Internal for unit tests only. internal UmbracoDatabase(DbConnection connection, ISqlContext sqlContext, ILogger logger) - : base(connection, sqlContext.DatabaseType, DefaultIsolationLevel) + : base(connection, sqlContext.DatabaseType, sqlContext.SqlSyntax.DefaultIsolationLevel) { SqlContext = sqlContext; _logger = logger; diff --git a/src/Umbraco.Core/PropertyEditors/GridEditor.cs b/src/Umbraco.Core/PropertyEditors/GridEditor.cs index 986eed9ccc..cc3561fbc2 100644 --- a/src/Umbraco.Core/PropertyEditors/GridEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/GridEditor.cs @@ -18,6 +18,9 @@ namespace Umbraco.Core.PropertyEditors [JsonProperty("name", Required = Required.Always)] public string Name { get; set; } + [JsonProperty("nameTemplate")] + public string NameTemplate { get; set; } + [JsonProperty("alias", Required = Required.Always)] public string Alias { get; set; } diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index e9dd04c5fa..84273e23da 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -33,8 +33,6 @@ namespace Umbraco.Core.Scoping private ICompletable _fscope; private IEventDispatcher _eventDispatcher; - private const IsolationLevel DefaultIsolationLevel = IsolationLevel.RepeatableRead; - // initializes a new scope private Scope(ScopeProvider scopeProvider, ILogger logger, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable, @@ -205,7 +203,7 @@ namespace Umbraco.Core.Scoping { if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel; if (ParentScope != null) return ParentScope.IsolationLevel; - return DefaultIsolationLevel; + return Database.SqlContext.SqlSyntax.DefaultIsolationLevel; } } @@ -488,37 +486,9 @@ namespace Umbraco.Core.Scoping ?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value; /// - public void ReadLock(params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks - foreach (var lockId in lockIds) - { - var i = Database.ExecuteScalar("SELECT value FROM umbracoLock WHERE id=@id", new { id = lockId }); - if (i == null) // ensure we are actually locking! - throw new Exception($"LockObject with id={lockId} does not exist."); - } - } + public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds); /// - public void WriteLock(params int[] lockIds) - { - // soon as we get Database, a transaction is started - - if (Database.Transaction.IsolationLevel < IsolationLevel.RepeatableRead) - throw new InvalidOperationException("A transaction with minimum RepeatableRead isolation level is required."); - - // *not* using a unique 'WHERE IN' query here because the *order* of lockIds is important to avoid deadlocks - foreach (var lockId in lockIds) - { - var i = Database.Execute("UPDATE umbracoLock SET value = (CASE WHEN (value=1) THEN -1 ELSE 1 END) WHERE id=@id", new { id = lockId }); - if (i == 0) // ensure we are actually locking! - throw new Exception($"LockObject with id={lockId} does not exist."); - } - } + public void WriteLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.WriteLock(Database, lockIds); } } diff --git a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs index b86494adb5..51e5d756eb 100644 --- a/src/Umbraco.Core/Services/IContentTypeServiceBase.cs +++ b/src/Umbraco.Core/Services/IContentTypeServiceBase.cs @@ -25,7 +25,7 @@ namespace Umbraco.Core.Services /// /// Gets a content type. /// - TItem Get(int id); + new TItem Get(int id); /// /// Gets a content type. @@ -40,6 +40,7 @@ namespace Umbraco.Core.Services int Count(); IEnumerable GetAll(params int[] ids); + IEnumerable GetAll(IEnumerable ids); IEnumerable GetDescendants(int id, bool andSelf); // parent-child axis IEnumerable GetComposedOf(int id); // composition axis diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index bb56e110cd..3ebfa95bfb 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -10,6 +10,13 @@ namespace Umbraco.Core.Services /// public interface IDataTypeService : IService { + /// + /// Returns a dictionary of content type s and the property type aliases that use a + /// + /// + /// + IReadOnlyDictionary> GetReferences(int id); + Attempt> CreateContainer(int parentId, string name, int userId = Constants.Security.SuperUserId); Attempt SaveContainer(EntityContainer container, int userId = Constants.Security.SuperUserId); EntityContainer GetContainer(int containerId); diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index ce63be9f2c..720713e9ad 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1848,7 +1848,7 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.ContentTree); var c = _documentRepository.Get(id); - if (c.VersionId != versionId) // don't delete the current version + if (c.VersionId != versionId && c.PublishedVersionId != versionId) // don't delete the current or published version _documentRepository.DeleteVersion(versionId); scope.Events.Dispatch(DeletedVersions, this, new DeleteRevisionsEventArgs(id, false,/* specificVersion:*/ versionId)); diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index 6ac8e1404a..705a876d83 100644 --- a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs @@ -252,12 +252,12 @@ namespace Umbraco.Core.Services.Implement } } - public IEnumerable GetAll(params Guid[] ids) + public IEnumerable GetAll(IEnumerable ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(ReadLockIds); - return Repository.GetMany(ids); + return Repository.GetMany(ids.ToArray()); } } diff --git a/src/Umbraco.Core/Services/Implement/DataTypeService.cs b/src/Umbraco.Core/Services/Implement/DataTypeService.cs index dc998b18dd..5a93fb91b1 100644 --- a/src/Umbraco.Core/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/DataTypeService.cs @@ -466,6 +466,14 @@ namespace Umbraco.Core.Services.Implement } } + public IReadOnlyDictionary> GetReferences(int id) + { + using (var scope = ScopeProvider.CreateScope(autoComplete:true)) + { + return _dataTypeRepository.FindUsages(id); + } + } + private void Audit(AuditType type, int userId, int objectId) { _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.DataType))); diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index b846095bd1..a037a83920 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -131,6 +131,12 @@ namespace Umbraco.Core.Services private bool IsPropertyValueValid(PropertyType propertyType, object value) { var editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) + { + // nothing much we can do validation wise if the property editor has been removed. + // the property will be displayed as a label, so flagging it as invalid would be pointless. + return true; + } var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId).Configuration; var valueEditor = editor.GetValueEditor(configuration); return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any(); diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 4df1105bf7..9bb61c7f2e 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -724,67 +724,56 @@ namespace Umbraco.Core /// /// Generates a hash of a string based on the FIPS compliance setting. /// - /// Refers to itself - /// The hashed string + /// The to hash. + /// + /// The hashed string. + /// public static string GenerateHash(this string str) { - return CryptoConfig.AllowOnlyFipsAlgorithms - ? str.ToSHA1() - : str.ToMd5(); + return str.GenerateHash(CryptoConfig.AllowOnlyFipsAlgorithms ? "SHA1" : "MD5"); } /// - /// Converts the string to MD5 + /// Generate a hash of a string based on the specified hash algorithm. /// - /// Refers to itself - /// The MD5 hashed string - [Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")] - internal static string ToMd5(this string stringToConvert) + /// The hash algorithm implementation to use. + /// The to hash. + /// + /// The hashed string. + /// + internal static string GenerateHash(this string str) + where T : HashAlgorithm { - return stringToConvert.GenerateHash("MD5"); + return str.GenerateHash(typeof(T).FullName); } /// - /// Converts the string to SHA1 + /// Generate a hash of a string based on the specified . /// - /// refers to itself - /// The SHA1 hashed string - [Obsolete("Please use the GenerateHash method instead. This may be removed in future versions")] - internal static string ToSHA1(this string stringToConvert) + /// The to hash. + /// The hash algorithm implementation to use. + /// + /// The hashed string. + /// + /// No hashing type found by name . + /// + internal static string GenerateHash(this string str, string hashType) { - return stringToConvert.GenerateHash("SHA1"); - } - - /// Generate a hash of a string based on the hashType passed in - /// - /// Refers to itself - /// String with the hash type. See remarks section of the CryptoConfig Class in MSDN docs for a list of possible values. - /// The hashed string - private static string GenerateHash(this string str, string hashType) - { - //create an instance of the correct hashing provider based on the type passed in var hasher = HashAlgorithm.Create(hashType); - if (hasher == null) throw new InvalidOperationException("No hashing type found by name " + hashType); + if (hasher == null) throw new InvalidOperationException($"No hashing type found by name {hashType}."); + using (hasher) { - //convert our string into byte array var byteArray = Encoding.UTF8.GetBytes(str); - - //get the hashed values created by our selected provider var hashedByteArray = hasher.ComputeHash(byteArray); - //create a StringBuilder object - var stringBuilder = new StringBuilder(); - - //loop to each byte + var sb = new StringBuilder(); foreach (var b in hashedByteArray) { - //append it to our StringBuilder - stringBuilder.Append(b.ToString("x2")); + sb.Append(b.ToString("x2")); } - //return the hashed value - return stringBuilder.ToString(); + return sb.ToString(); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a0d0fad6d9..309dc97b81 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -245,6 +245,8 @@ + + @@ -263,7 +265,9 @@ + + @@ -982,7 +986,6 @@ - diff --git a/src/Umbraco.Examine/ContentIndexPopulator.cs b/src/Umbraco.Examine/ContentIndexPopulator.cs index 51b9de4a0b..dd4176a774 100644 --- a/src/Umbraco.Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Examine/ContentIndexPopulator.cs @@ -52,7 +52,7 @@ namespace Umbraco.Examine if (sqlContext == null) throw new ArgumentNullException(nameof(sqlContext)); _contentService = contentService ?? throw new ArgumentNullException(nameof(contentService)); _contentValueSetBuilder = contentValueSetBuilder ?? throw new ArgumentNullException(nameof(contentValueSetBuilder)); - if (_publishedQuery != null) + if (_publishedQuery == null) _publishedQuery = sqlContext.Query().Where(x => x.Published); _publishedValuesOnly = publishedValuesOnly; _parentId = parentId; diff --git a/src/Umbraco.Examine/IUmbracoIndexConfig.cs b/src/Umbraco.Examine/IUmbracoIndexConfig.cs new file mode 100644 index 0000000000..02c6c51d0c --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoIndexConfig.cs @@ -0,0 +1,12 @@ +using Examine; + +namespace Umbraco.Examine +{ + public interface IUmbracoIndexConfig + { + IContentValueSetValidator GetContentValueSetValidator(); + IContentValueSetValidator GetPublishedContentValueSetValidator(); + IValueSetValidator GetMemberValueSetValidator(); + + } +} diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index e28a8e674e..e30d355dfe 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -64,9 +64,11 @@ + + diff --git a/src/Umbraco.Examine/UmbracoIndexConfig.cs b/src/Umbraco.Examine/UmbracoIndexConfig.cs new file mode 100644 index 0000000000..7ad9c638d3 --- /dev/null +++ b/src/Umbraco.Examine/UmbracoIndexConfig.cs @@ -0,0 +1,34 @@ +using Examine; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Examine +{ + public class UmbracoIndexConfig : IUmbracoIndexConfig + { + public UmbracoIndexConfig(IPublicAccessService publicAccessService) + { + PublicAccessService = publicAccessService; + } + + protected IPublicAccessService PublicAccessService { get; } + public IContentValueSetValidator GetContentValueSetValidator() + { + return new ContentValueSetValidator(false, true, PublicAccessService); + } + + public IContentValueSetValidator GetPublishedContentValueSetValidator() + { + return new ContentValueSetValidator(true, false, PublicAccessService); + } + + /// + /// Returns the for the member indexer + /// + /// + public IValueSetValidator GetMemberValueSetValidator() + { + return new MemberValueSetValidator(); + } + } +} diff --git a/src/Umbraco.Tests/Composing/CompositionTests.cs b/src/Umbraco.Tests/Composing/CompositionTests.cs index f4478e2add..33855a8bfb 100644 --- a/src/Umbraco.Tests/Composing/CompositionTests.cs +++ b/src/Umbraco.Tests/Composing/CompositionTests.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; +using Umbraco.Core.IO; using Umbraco.Core.Logging; namespace Umbraco.Tests.Composing @@ -35,7 +36,7 @@ namespace Umbraco.Tests.Composing .Returns(() => factoryFactory?.Invoke(mockedFactory)); var logger = new ProfilingLogger(Mock.Of(), Mock.Of()); - var typeLoader = new TypeLoader(Mock.Of(), "", logger); + var typeLoader = new TypeLoader(Mock.Of(), IOHelper.MapPath("~/App_Data/TEMP"), logger); var composition = new Composition(mockedRegister, typeLoader, logger, Mock.Of()); // create the factory, ensure it is the mocked factory diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 9cd4f39c17..7459ae848b 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(39, types.Count()); + Assert.AreEqual(38, types.Count()); } /// diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs index 26e6a1ad8a..0245159c6e 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/ContentElementTests.cs @@ -67,7 +67,7 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings [Test] public void PreviewBadge() { - Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"In Preview Mode - click to end"); + Assert.AreEqual(SettingsSection.Content.PreviewBadge, @"
Preview modeClick to end
"); } [Test] public void ResolveUrlsFromTextString() diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config index ac022a5489..8cbb799d88 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/umbracoSettings.config @@ -41,9 +41,7 @@ - In Preview Mode - click to end - ]]> + Preview modeClick to end]]> @@ -64,63 +67,75 @@
  • + ng-click="vm.pickDataType(dataType)" + class="cursor-pointer">
    - + {{ dataType.name }} - +
  • + -
    - + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index 704b61e333..6ba2ec0270 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -23,6 +23,7 @@ placeholder="@general_url" class="umb-property-editor umb-textstring" ng-model="model.target.url" + umb-auto-focus ng-disabled="model.target.id || model.target.udi" /> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.controller.js new file mode 100644 index 0000000000..5b81cb947d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.controller.js @@ -0,0 +1,127 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.MacroParameterPickerController + * @function + * + * @description + * The controller for the content type editor macro parameter dialog + */ + +(function() { + "use strict"; + + function MacroParameterController($scope, $filter, macroResource, localizationService, editorService) { + + var vm = this; + + vm.searchTerm = ""; + vm.parameterEditors = []; + vm.loading = false; + vm.labels = {}; + + vm.filterItems = filterItems; + vm.showDetailsOverlay = showDetailsOverlay; + vm.hideDetailsOverlay = hideDetailsOverlay; + vm.pickParameterEditor = pickParameterEditor; + vm.close = close; + + function init() { + setTitle(); + getGroupedParameterEditors(); + } + + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectEditor") + .then(function(data){ + $scope.model.title = data; + }); + } + } + + function getGroupedParameterEditors() { + + vm.loading = true; + + macroResource.getGroupedParameterEditors().then(function (data) { + vm.parameterEditors = data; + vm.loading = false; + }, function () { + vm.loading = false; + }); + + } + + function filterItems() { + // clear item details + $scope.model.itemDetails = null; + + if (vm.searchTerm) { + + var regex = new RegExp(vm.searchTerm, "i"); + + var parameterEditors = filterCollection(vm.parameterEditors, regex); + + var totalResults = _.reduce(_.pluck(parameterEditors, 'count'), (m, n) => m + n, 0); + + vm.filterResult = { + parameterEditors: parameterEditors, + totalResults: totalResults + }; + } else { + vm.filterResult = null; + } + } + + function filterCollection(collection, regex) { + return _.map(_.keys(collection), function (key) { + + var filteredEditors = $filter('filter')(collection[key], function (editor) { + return regex.test(editor.name) || regex.test(editor.alias); + }); + + return { + group: key, + count: filteredEditors.length, + parameterEditors: filteredEditors + } + }); + } + + function showDetailsOverlay(property) { + + var propertyDetails = {}; + propertyDetails.icon = property.icon; + propertyDetails.title = property.name; + + $scope.model.itemDetails = propertyDetails; + } + + function hideDetailsOverlay() { + $scope.model.itemDetails = null; + } + + function pickParameterEditor(selectedParameterEditor) { + + console.log("pickParameterEditor", selectedParameterEditor); + console.log("$scope.model", $scope.model); + + $scope.model.parameter.editor = selectedParameterEditor.alias; + $scope.model.parameter.dataTypeName = selectedParameterEditor.name; + $scope.model.parameter.dataTypeIcon = selectedParameterEditor.icon; + + $scope.model.submit($scope.model); + } + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + init(); + } + + angular.module("umbraco").controller("Umbraco.Editors.MacroParameterPickerController", MacroParameterController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html new file mode 100644 index 0000000000..9f2b56401d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macroparameterpicker/macroparameterpicker.html @@ -0,0 +1,106 @@ +
    + + + +
    + + + + + + + + + +
    + +
    + + + +
    + +
    + + +
    +
    +
    +
    +
    {{result.group}}
    + +
    +
    +
    + + + + +
    + +
    +
    +
    + + + + + + + + +
    + +
    + +
    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 aa63f2d6d6..dfc77f786c 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 @@ -41,8 +41,6 @@ function MacroPickerController($scope, entityResource, macroResource, umbPropEdi macroResource.getMacroParameters($scope.model.selectedMacro.id) .then(function (data) { - - //go to next page if there are params otherwise we can just exit if (!angular.isArray(data) || data.length === 0) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html index 66c64657a9..fc1bec4ec1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/macropicker/macropicker.html @@ -32,10 +32,10 @@
    • - + - {{ availableItem.name }} + {{availableItem.name}}
    • 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 57a9c22b0b..ba103a2761 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 @@ -28,6 +28,7 @@ angular.module("umbraco") 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; $scope.onlyFolders = (dialogOptions.onlyFolders && dialogOptions.onlyFolders !== "0") ? true : false; $scope.showDetails = (dialogOptions.showDetails && dialogOptions.showDetails !== "0") ? true : false; @@ -37,7 +38,7 @@ angular.module("umbraco") $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); $scope.lockedFolder = true; $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; - + var userStartNodes = []; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; @@ -137,7 +138,8 @@ angular.module("umbraco") $scope.target = node; if (ensureWithinStartNode(node)) { selectMedia(node); - $scope.target.url = mediaHelper.resolveFile(node); + $scope.target.url = mediaHelper.resolveFileFromEntity(node); + $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); $scope.target.altText = altText; openDetailsDialog(); } @@ -226,7 +228,7 @@ angular.module("umbraco") } function clickHandler(media, event, index) { - + if (media.isFolder) { if ($scope.disableFolderSelect) { gotoFolder(media); @@ -333,22 +335,26 @@ angular.module("umbraco") } function openDetailsDialog() { + localizationService.localize("defaultdialogs_editSelectedMedia").then(function (data) { + vm.mediaPickerDetailsOverlay = { + show: true, + title: data, + disableFocalPoint: $scope.disableFocalPoint, + submit: function (model) { + $scope.model.selection.push($scope.target); + $scope.model.submit($scope.model); - vm.mediaPickerDetailsOverlay = { - show: true, - submit: function (model) { + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; + }, + close: function (oldModel) { + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; - $scope.model.selection.push($scope.target); - $scope.model.submit($scope.model); - - vm.mediaPickerDetailsOverlay.show = false; - vm.mediaPickerDetailsOverlay = null; - }, - close: function (oldModel) { - vm.mediaPickerDetailsOverlay.show = false; - vm.mediaPickerDetailsOverlay = null; - } - }; + close(); + } + }; + }); }; var debounceSearchMedia = _.debounce(function () { @@ -453,21 +459,25 @@ angular.module("umbraco") function getChildren(id) { vm.loading = true; return entityResource.getChildren(id, "Media", vm.searchOptions).then(function (data) { - - for (var i = 0; i < data.length; i++) { - if (data[i].metaData.MediaPath !== null) { - data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); - data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); - } + + var allowedTypes = dialogOptions.filter ? dialogOptions.filter.split(",") : null; + + for (var i = 0; i < data.length; i++) { + if (data[i].metaData.MediaPath !== null) { + data[i].thumbnail = mediaHelper.resolveFileFromEntity(data[i], true); + data[i].image = mediaHelper.resolveFileFromEntity(data[i], false); } - vm.searchOptions.filter = ""; - $scope.images = data ? data : []; + data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; + } - // set already selected medias to selected - preSelectMedia(); - vm.loading = false; - }); + vm.searchOptions.filter = ""; + $scope.images = data ? data : []; + + // set already selected medias to selected + preSelectMedia(); + vm.loading = false; + }); } function preSelectMedia() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index a08395143f..656c5f2ac1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -1,198 +1,224 @@
      - + - - + + - + -
      + -
      +
      -
      +
      - - + + - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index 737253feb2..fdd9b44a1e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -6,13 +6,39 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index ccc390252b..32f495e32b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -8,7 +8,8 @@ angular.module("umbraco") angularHelper, $element, eventsService, - editorService + editorService, + $interpolate ) { // Grid status variables @@ -661,7 +662,6 @@ angular.module("umbraco") return ((spans / $scope.model.config.items.columns) * 100).toFixed(8); }; - $scope.clearPrompt = function (scopedObject, e) { scopedObject.deletePrompt = false; e.preventDefault(); @@ -680,6 +680,10 @@ angular.module("umbraco") $scope.showRowConfigurations = !$scope.showRowConfigurations; }; + $scope.getTemplateName = function (control) { + if (control.editor.nameExp) return control.editor.nameExp(control) + return control.editor.name; + } // ********************************************* // Initialization @@ -923,6 +927,11 @@ angular.module("umbraco") localizationService.localize("grid_" + value.alias, undefined, value.name).then(function (v) { value.name = v; }); + // setup nametemplate + + value.nameExp = !!value.nameTemplate + ? $interpolate(value.nameTemplate) + : undefined; }); $scope.contentReady = true; 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 6b739a9b86..3995ed703d 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 @@ -182,7 +182,7 @@
      - {{control.editor.name}} + {{ getTemplateName(control) }}
      @@ -190,7 +190,7 @@
      - {{control.editor.name}} + {{ getTemplateName(control) }}
      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 7438fa04da..a0faefe690 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 @@ -5,6 +5,7 @@ + +
      + +
      + +
      +
      +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index 345c58f423..ba08aa2293 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -2,23 +2,24 @@
      - {{errorMsg}} +
      - +
      - - + + - + + @@ -27,26 +28,31 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html index f8ad0bb3f4..15b66d856d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html @@ -4,13 +4,13 @@
      - +
      - +
      @@ -29,7 +29,7 @@
      - +
      - Add layout +
      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 e50411fe8b..7febda9508 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,4 +1,4 @@ -function listViewController($scope, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, listViewHelper, navigationService, editorService, overlayService, languageResource, mediaHelper) { +function listViewController($scope, $interpolate, $routeParams, $injector, $timeout, currentUserResource, notificationsService, iconHelper, editorState, localizationService, appState, $location, listViewHelper, navigationService, editorService, overlayService, languageResource, mediaHelper) { //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content // that isn't created yet, if we continue this will use the parent id in the route params which isn't what @@ -168,6 +168,11 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete, cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture }; + _.each($scope.options.includeProperties, function (property) { + property.nameExp = !!property.nameTemplate + ? $interpolate(property.nameTemplate) + : undefined; + }); //watch for culture changes in the query strings and update accordingly $scope.$watch(function () { @@ -193,9 +198,14 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs _.each($scope.options.includeProperties, function (e, i) { e.allowSorting = true; - // Special case for members, only fields on the base table (cmsMember) can be used for sorting - if (e.isSystem && $scope.entityType == "member") { - e.allowSorting = e.alias == 'username' || e.alias == 'email'; + // Special case for members, only the configured system fields should be enabled sorting + // (see MemberRepository.ApplySystemOrdering) + if (e.isSystem && $scope.entityType === "member") { + e.allowSorting = e.alias === "username" || + e.alias === "email" || + e.alias === "updateDate" || + e.alias === "createDate" || + e.alias === "contentTypeAlias"; } if (e.isSystem) { @@ -262,10 +272,10 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs $scope.getContent = function (contentId) { - $scope.reloadView($scope.contentId); + $scope.reloadView($scope.contentId, true); } - $scope.reloadView = function (id) { + $scope.reloadView = function (id, reloadActiveNode) { $scope.viewLoaded = false; $scope.folders = []; @@ -297,9 +307,20 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs $scope.options.pageNumber = $scope.listViewResultSet.totalPages; //reload! - $scope.reloadView(id); + $scope.reloadView(id, reloadActiveNode); + } + // in the media section, the list view items are by default also shown in the tree, so we need + // to refresh the current tree node when changing the folder contents (adding and removing) + else if (reloadActiveNode && section === "media") { + var activeNode = appState.getTreeState("selectedNode"); + if (activeNode) { + if (activeNode.expanded) { + navigationService.reloadNode(activeNode); + } + } else { + navigationService.reloadSection(section); + } } - }); }; @@ -411,7 +432,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs var key = (total === 1 ? "bulk_deletedItem" : "bulk_deletedItems"); return localizationService.localize(key, [total]); }).then(function () { - $scope.reloadView($scope.contentId); + $scope.reloadView($scope.contentId, true); }); } @@ -489,6 +510,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs const dialog = { view: "views/propertyeditors/listview/overlays/listviewunpublish.html", submitButtonLabelKey: "actions_unpublish", + submitButtonStyle: "warning", submit: function (model) { // create a comma separated array of selected cultures @@ -536,7 +558,7 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs var key = (total === 1 ? "bulk_unpublishedItem" : "bulk_unpublishedItems"); return localizationService.localize(key, [total]); }).then(function () { - $scope.reloadView($scope.contentId); + $scope.reloadView($scope.contentId, true); }); } @@ -682,6 +704,13 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs value = value.substring(0, value.length - 3); } + if (e.nameExp) { + var newValue = e.nameExp({ value }); + if (newValue && (newValue = $.trim(newValue))) { + value = newValue; + } + } + // set what we've got on the result result[alias] = value; }); @@ -767,8 +796,11 @@ function listViewController($scope, $routeParams, $injector, $timeout, currentUs case "published": return "content_isPublished"; case "contentTypeAlias": - // TODO: Check for members - return $scope.entityType === "content" ? "content_documentType" : "content_mediatype"; + return $scope.entityType === "content" + ? "content_documentType" + : $scope.entityType === "media" + ? "content_mediatype" + : "content_membertype"; case "email": return "general_email"; case "username": diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html index bdca3d1fe1..83a905ccf7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/orderDirection.prevalues.html @@ -1,15 +1,9 @@ 
      - - - + + + - + Required diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html index ebdc1eb4c4..88dfb45d4a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewpublish.html @@ -1,41 +1,43 @@
      +

      -
      +
      + +
      + + +

      -
      - -
      +
      -
      +
      + +
      - + + +
      - - -
      + -
      - +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js index b6a236700e..46604227da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.controller.js @@ -56,7 +56,6 @@ } vm.loading = false; - } onInit(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html index a171fb1327..d7f33db58b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/overlays/listviewunpublish.html @@ -1,12 +1,16 @@
      - +

      +
      + +
      + -
      +

      @@ -14,26 +18,26 @@
      +
      + +
      +
      - + + +
      - - -
      + -
      - +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js deleted file mode 100644 index b258bf829a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.controller.js +++ /dev/null @@ -1,138 +0,0 @@ -//DO NOT DELETE THIS, this is in use... -angular.module('umbraco') -.controller("Umbraco.PropertyEditors.MacroContainerController", - - function($scope, macroService){ - - $scope.renderModel = []; - $scope.allowOpenButton = true; - $scope.allowRemoveButton = true; - $scope.sortableOptions = {}; - - if($scope.model.value){ - var macros = $scope.model.value.split('>'); - - angular.forEach(macros, function(syntax, key){ - if(syntax && syntax.length > 10){ - //re-add the char we split on - syntax = syntax + ">"; - var parsed = macroService.parseMacroSyntax(syntax); - if(!parsed){ - parsed = {}; - } - - parsed.syntax = syntax; - collectDetails(parsed); - $scope.renderModel.push(parsed); - setSortingState($scope.renderModel); - } - }); - } - - - function collectDetails(macro){ - macro.details = ""; - macro.icon = "icon-settings-alt"; - if(macro.macroParamsDictionary){ - angular.forEach((macro.macroParamsDictionary), function(value, key){ - macro.details += key + ": " + value + " "; - }); - } - } - - function openDialog(index){ - var dialogData = { - allowedMacros: $scope.model.config.allowed - }; - - if(index !== null && $scope.renderModel[index]) { - var macro = $scope.renderModel[index]; - dialogData["macroData"] = macro; - } - - $scope.macroPickerOverlay = {}; - $scope.macroPickerOverlay.view = "macropicker"; - $scope.macroPickerOverlay.dialogData = dialogData; - $scope.macroPickerOverlay.show = true; - - $scope.macroPickerOverlay.submit = function(model) { - - var macroObject = macroService.collectValueData(model.selectedMacro, model.macroParams, dialogData.renderingEngine); - collectDetails(macroObject); - - //update the raw syntax and the list... - if(index !== null && $scope.renderModel[index]) { - $scope.renderModel[index] = macroObject; - } else { - $scope.renderModel.push(macroObject); - } - - setSortingState($scope.renderModel); - - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - $scope.macroPickerOverlay.close = function(oldModel) { - $scope.macroPickerOverlay.show = false; - $scope.macroPickerOverlay = null; - }; - - } - - - - $scope.edit =function(index){ - openDialog(index); - }; - - $scope.add = function () { - - if ($scope.model.config.max && $scope.model.config.max > 0 && $scope.renderModel.length >= $scope.model.config.max) { - //cannot add more than the max - return; - } - - openDialog(); - }; - - $scope.remove =function(index){ - $scope.renderModel.splice(index, 1); - setSortingState($scope.renderModel); - }; - - $scope.clear = function() { - $scope.model.value = ""; - $scope.renderModel = []; - }; - - var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { - var syntax = []; - angular.forEach($scope.renderModel, function(value, key){ - syntax.push(value.syntax); - }); - - $scope.model.value = syntax.join(""); - }); - - //when the scope is destroyed we need to unsubscribe - $scope.$on('$destroy', function () { - unsubscribe(); - }); - - - function trim(str, chr) { - var rgxtrim = (!chr) ? new RegExp('^\\s+|\\s+$', 'g') : new RegExp('^'+chr+'+|'+chr+'+$', 'g'); - return str.replace(rgxtrim, ''); - } - - function setSortingState(items) { - // disable sorting if the list only consist of one item - if(items.length > 1) { - $scope.sortableOptions.disabled = false; - } else { - $scope.sortableOptions.disabled = true; - } - } - -}); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.html deleted file mode 100644 index 6d844e959b..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrocontainer.html +++ /dev/null @@ -1,33 +0,0 @@ -
      - -
      -
      - - -
      -
      - - - Add - - - - - -
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrolist.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrolist.prevalues.controller.js deleted file mode 100644 index ffede2abd4..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrolist.prevalues.controller.js +++ /dev/null @@ -1,15 +0,0 @@ -function MacroListController($scope, entityResource) { - - $scope.items = []; - - entityResource.getAll("Macro").then(function(items) { - _.each(items, function(i) { - $scope.items.push({ name: i.name, alias: i.alias }); - }); - - }); - - -} - -angular.module("umbraco").controller("Umbraco.PrevalueEditors.MacroList", MacroListController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrolist.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrolist.prevalues.html deleted file mode 100644 index d0081e7bc2..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/macrocontainer/macrolist.prevalues.html +++ /dev/null @@ -1,7 +0,0 @@ -
      - - - -
      \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index 29e3a970e2..c4f3342000 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -36,16 +36,16 @@
    • - +
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 536169778c..925ebf0e1f 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 @@ -2,8 +2,46 @@ "$scope", "Umbraco.PropertyEditors.NestedContent.Resources", + "overlayService", + "localizationService", + "iconHelper", + + function ($scope, ncResources, overlayService, localizationService, iconHelper) { + var selectElementTypeModalTitle = ""; + + $scope.elemTypeTabs = []; + + + init(); + + + function init() { + localizationService.localize("content_nestedContentSelectElementTypeModalTitle").then(function (value) { + selectElementTypeModalTitle = value; + }); + + ncResources.getContentTypes().then(function (elemTypes) { + $scope.model.elemTypes = elemTypes; + + // convert legacy icons + iconHelper.formatContentTypeIcons($scope.model.elemTypes); + + // Count doctype name occurrences + var elTypeNameOccurrences= _.countBy(elemTypes, 'name'); + + // Populate document type tab dictionary + // And append alias to name if multiple doctypes have the same name + elemTypes.forEach(function (value) { + $scope.elemTypeTabs[value.alias] = value.tabs; + + if (elTypeNameOccurrences[value.name] > 1) { + value.name += " (" + value.alias + ")"; + } + }); + }); + + } - function ($scope, ncResources) { $scope.add = function () { $scope.model.value.push({ @@ -57,42 +95,66 @@ } }; - $scope.docTypeTabs = {}; - ncResources.getContentTypes().then(function (docTypes) { - $scope.model.docTypes = docTypes; - - // Count doctype name occurrences - var docTypeNameOccurrences = _.countBy(docTypes, 'name'); - - // Populate document type tab dictionary - // And append alias to name if multiple doctypes have the same name - docTypes.forEach(function (value) { - $scope.docTypeTabs[value.alias] = value.tabs; - - value.displayName = value.name; - - if (docTypeNameOccurrences[value.name] > 1) { - value.displayName += " (" + value.alias + ")"; - } + $scope.placeholder = function (config) { + return _.find($scope.model.elemTypes, function (elType) { + return elType.alias === config.ncAlias; }); - }); + } - $scope.selectableDocTypesFor = function (config) { - // return all doctypes that are: + $scope.selectableElemTypesFor = function (config) { + // return all elemTypes that are: // 1. either already selected for this config, or // 2. not selected in any other config - return _.filter($scope.model.docTypes, function (docType) { - return docType.alias === config.ncAlias || !_.find($scope.model.value, function(c) { - return docType.alias === c.ncAlias; + return _.filter($scope.model.elemTypes, function (elType) { + return elType.alias === config.ncAlias || !_.find($scope.model.value, function (c) { + return elType.alias === c.ncAlias; }); }); } + $scope.canAdd = function () { + return !$scope.model.value || _.some($scope.model.elemTypes, function (elType) { + return !_.find($scope.model.value, function (c) { + return elType.alias === c.ncAlias; + }); + }); + } + + + $scope.openElemTypeModal = function ($event, config) { + + //we have to add the alias to the objects (they are stored as ncAlias) + var selectedItems = _.each($scope.model.value, function (obj) { + obj.alias = obj.ncAlias; + return obj; + }) + + var elemTypeSelectorOverlay = { + view: "itempicker", + title: selectElementTypeModalTitle, + availableItems: $scope.selectableElemTypesFor(config), + selectedItems: selectedItems, + position: "target", + event: $event, + submit: function (model) { + config.ncAlias = model.selectedItem.alias; + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(elemTypeSelectorOverlay); + } + + if (!$scope.model.value) { $scope.model.value = []; $scope.add(); } + } ]); @@ -141,7 +203,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.hasContentTypes = $scope.model.config.contentTypes.length > 0; $scope.labels = {}; - localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function(data) { + localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { $scope.labels.grid_addElement = data[0]; $scope.labels.content_createEmpty = data[1]; }); @@ -179,14 +241,14 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.overlayMenu.show = false; $scope.overlayMenu = null; }, - submit: function(model) { - if(model && model.selectedItem) { + submit: function (model) { + if (model && model.selectedItem) { $scope.addNode(model.selectedItem.alias); } $scope.overlayMenu.show = false; $scope.overlayMenu = null; }, - close: function() { + close: function () { $scope.overlayMenu.show = false; $scope.overlayMenu = null; } @@ -218,22 +280,22 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop icon: iconHelper.convertFromLegacyIcon(node.icon) }); }); - + $scope.overlayMenu.title = $scope.overlayMenu.pasteItems.length > 0 ? $scope.labels.grid_addElement : $scope.labels.content_createEmpty; - - $scope.overlayMenu.clickClearPaste = function($event) { + + $scope.overlayMenu.clickClearPaste = function ($event) { $event.stopPropagation(); $event.preventDefault(); clipboardService.clearEntriesOfType("elementType", contentTypeAliases); $scope.overlayMenu.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. }; - + if ($scope.overlayMenu.availableItems.length === 1 && $scope.overlayMenu.pasteItems.length === 0) { // only one scaffold type - no need to display the picker $scope.addNode($scope.scaffolds[0].contentTypeAlias); return; } - + $scope.overlayMenu.show = true; }; @@ -324,10 +386,10 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop if ($scope.nodes[idx].name !== name) { $scope.nodes[idx].name = name; } - + return name; }; - + $scope.getIcon = function (idx) { var scaffold = $scope.getScaffold($scope.model.value[idx].ncContentTypeAlias); return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder"; @@ -335,7 +397,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.sortableOptions = { axis: "y", cursor: "move", - handle:'.umb-nested-content__header-bar', + handle: '.umb-nested-content__header-bar', distance: 10, opacity: 0.7, tolerance: "pointer", @@ -377,15 +439,15 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop return contentType.ncAlias === alias; }); } - + $scope.showCopy = clipboardService.isSupported(); - + $scope.showPaste = false; - - $scope.clickCopy = function($event, node) { - + + $scope.clickCopy = function ($event, node) { + syncCurrentNode(); - + clipboardService.copy("elementType", node.contentTypeAlias, node); $event.stopPropagation(); } @@ -395,27 +457,27 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop if (newNode === undefined) { return; } - + // generate a new key. newNode.key = String.CreateGuid(); - + $scope.nodes.push(newNode); $scope.setDirty(); //updateModel();// done by setting current node... $scope.currentNode = newNode; } - + function checkAbilityToPasteContent() { $scope.showPaste = clipboardService.hasEntriesOfType("elementType", contentTypeAliases); } - + eventsService.on("clipboardService.storageUpdate", checkAbilityToPasteContent); - + var notSupported = [ - "Umbraco.Tags", - "Umbraco.UploadField", - "Umbraco.ImageCropper" + "Umbraco.Tags", + "Umbraco.UploadField", + "Umbraco.ImageCropper" ]; // Initialize @@ -495,24 +557,24 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop } $scope.inited = true; - + checkAbilityToPasteContent(); } } - + function createNode(scaffold, fromNcEntry) { var node = angular.copy(scaffold); - + node.key = fromNcEntry && fromNcEntry.key ? fromNcEntry.key : String.CreateGuid(); - + var variant = node.variants[0]; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; - + for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - + prop.propertyAlias = prop.alias; prop.alias = $scope.model.alias + "___" + prop.alias; // Force validation to occur server side as this is the @@ -522,7 +584,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop mandatory: false, pattern: "" }; - + if (fromNcEntry && fromNcEntry[prop.propertyAlias]) { prop.value = fromNcEntry[prop.propertyAlias]; } @@ -533,7 +595,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop return node; } - + function convertNodeIntoNCEntry(node) { var obj = { key: node.key, @@ -551,16 +613,16 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop } return obj; } - + function syncCurrentNode() { if ($scope.realCurrentNode) { $scope.$broadcast("ncSyncVal", { key: $scope.realCurrentNode.key }); } } - + function updateModel() { syncCurrentNode(); - + if ($scope.inited) { var newValues = []; for (var i = 0; i < $scope.nodes.length; i++) { @@ -582,7 +644,7 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop $scope.$on("$destroy", function () { unsubscribe(); }); - + } ]); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html index 516c23daad..f6bfdecb31 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html @@ -3,7 +3,7 @@
      AliasAlias HeaderTemplate
      - {{val.alias}} - - {{val.alias}} - - (system field) +
      + + + (system field) +
      - + Required - + {{val.alias}} - + + + + +
      - @@ -22,14 +22,22 @@
      + Element Type - + {{ph = placeholder(config);""}} +
      + + + + + {{ ph.name }} + + Add element type +
      +
      + ng-options="t for t in elemTypeTabs[config.ncAlias]" + ng-model="config.ncTabAlias" required> @@ -49,15 +57,15 @@ -
      +

      - Group:
      + Group:
      Select the group whose properties should be displayed. If left blank, the first group on the element type will be used.

      - Template:
      - Enter an angular expression to evaluate against each item for its name. Use {{$index}} to display the item index + Template:
      + Enter an angular expression to evaluate against each item for its name. Use {{$index}} to display the item index

      diff --git a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js index b4f396bb33..43339adf04 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/group.controller.js @@ -105,6 +105,7 @@ var sectionPicker = { selection: currentSelection, submit: function (model) { + vm.userGroup.sections = model.selection; editorService.close(); }, close: function () { @@ -168,7 +169,8 @@ angular.copy(vm.userGroup.users, currentSelection); var userPicker = { selection: currentSelection, - submit: function () { + submit: function (model) { + vm.userGroup.users = model.selection; editorService.close(); }, close: function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index 881d7a9c0d..e759be5e7a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -71,6 +71,7 @@ sections="userGroup.sections" content-start-node="userGroup.contentStartNode" media-start-node="userGroup.mediaStartNode" + allow-remove="true" on-remove="model.removeSelectedItem($index, model.user.userGroups)"> @@ -259,7 +260,7 @@
      Add + + diff --git a/src/Umbraco.Web.UI.Client/test/config/app.unit.js b/src/Umbraco.Web.UI.Client/test/config/app.unit.js index 32b05ab0d4..1f49d237e6 100644 --- a/src/Umbraco.Web.UI.Client/test/config/app.unit.js +++ b/src/Umbraco.Web.UI.Client/test/config/app.unit.js @@ -13,7 +13,7 @@ var app = angular.module('umbraco', [ 'ngSanitize', //'ngMessages', - 'tmh.dynamicLocale', + 'tmh.dynamicLocale' //'ngFileUpload', //'LocalStorageModule', //'chart.js' diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index 08223de092..37058d433b 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -15,6 +15,7 @@ module.exports = function (config) { 'node_modules/angular/angular.js', 'node_modules/angular-animate/angular-animate.js', 'node_modules/angular-cookies/angular-cookies.js', + 'node_modules/angular-aria/angular-aria.min.js', 'node_modules/angular-local-storage/dist/angular-local-storage.min.js', 'node_modules/angular-route/angular-route.js', 'node_modules/angular-sanitize/angular-sanitize.js', @@ -50,9 +51,23 @@ module.exports = function (config) { exclude: [], // use dolts reporter, as travis terminal does not support escaping sequences - // possible values: 'dots', 'progress', 'junit', 'teamcity' + // possible values: 'dots', 'progress', 'junit', 'spec' + // *** + // progress: Outputs a simple list like: "Executed 128 of 144 SUCCESS (0 secs / 0.814 secs)" + // spec: Outputs a more verbose report which is more useful for debugging if one of the tests fails. + // *** // CLI --reporters progress - reporters: ['progress', 'junit'], + + reporters: ['spec', 'junit'], + specReporter: { + maxLogLines: 5, // limit number of lines logged per test + suppressErrorSummary: true, // do not print error summary + suppressFailed: false, // do not print information about failed tests + suppressPassed: false, // do not print information about passed tests + suppressSkipped: true, // do not print information about skipped tests + showSpecTiming: false // print the time elapsed for each spec + }, + // web server port // CLI --port 9876 @@ -101,7 +116,9 @@ module.exports = function (config) { plugins: [ require('karma-jasmine'), require('karma-phantomjs-launcher'), - require('karma-junit-reporter') + require('karma-junit-reporter'), + require('karma-spec-reporter') + ], // the default configuration diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js index dba3373ec2..a82a926a6d 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/dropdown-controller.spec.js @@ -37,7 +37,7 @@ describe('Drop down controller tests', function () { $routeParams: routeParams }); - //this should be the expected format based on the changes made to the sortable prevalues + // this should be the expected format based on the changes made to the sortable prevalues expect(scope.model.config.items[0].value).toBe("value0"); expect(scope.model.config.items[1].value).toBe("value1"); expect(scope.model.config.items[2].value).toBe("value2"); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js new file mode 100644 index 0000000000..1e9ea2ea46 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js @@ -0,0 +1,69 @@ +(function () { + + describe('truncate filter', function() { + var $truncate; + + var testCases = [ + {input:'test', noOfChars:5, appendDots:true, expectedResult: 'test'}, + {input:'test ', noOfChars:4, appendDots:true, expectedResult: 'test…'}, + {input:'test a long text with space', noOfChars:5, appendDots:true, expectedResult: 'test …'}, + {input:'scenarios is a long word', noOfChars:5, appendDots:true, expectedResult: 'scena…'}, + {input:'scenarios is a long word', noOfChars:10, appendDots:true, expectedResult: 'scenarios …'}, + {input:'test', noOfChars:5, appendDots:false, expectedResult: 'test'}, + {input:'test a long text with space', noOfChars:5, appendDots:false, expectedResult: 'test '}, + {input:'scenarios is a long word', noOfChars:5, appendDots:false, expectedResult: 'scena'}, + {input:'scenarios is a long word', noOfChars:10, appendDots:false, expectedResult: 'scenarios '} + ]; + + var testCasesNew = [ + {value:'test', wordwise:false, max:20, tail:'...', expectedResult: 'test'}, + {value:'LoremIpsumLoremIpsumLoremIpsum',wordwise:false, max:20, tail:null, expectedResult: 'LoremIpsumLoremIpsum…'} + ]; + + beforeEach(module('umbraco')); + + beforeEach(inject(function($filter) { + $truncate = $filter('truncate'); + })); + + it('empty string as input is expected to give an empty string', function() { + expect($truncate('', 5, true)).toBe(''); + }); + + it('null as input is expected to give an empty string', function() { + expect($truncate(null, 5, true)).toBe(''); + }); + + it('undefined as input is expected to give an empty string', function() { + expect($truncate(undefined, 5, true)).toBe(''); + }); + + it('null as noOfChars to result in \'test\'', function() { + expect($truncate('test', null, true)).toBe('test'); + }); + + it('undefined as noOfChars to result in \'test\'', function() { + expect($truncate('test', undefined, true)).toBe('test'); + }); + + it('null as appendDots to behave as false', function() { + expect($truncate('test', 5, null)).toBe('test'); + }); + + testCases.forEach(function(test){ + + it('Expects \'' + test.input + '\' to be truncated as \''+ test.expectedResult + '\', when noOfChars=' + test.noOfChars + ', and appendDots=' + test.appendDots, function() { + console.log($truncate(test.input, test.noOfChars, test.appendDots)); + expect($truncate(test.input, test.noOfChars, test.appendDots)).toBe(test.expectedResult); + }); + }); + + testCasesNew.forEach(function(test){ + it('Expects \'' + test.value + '\' to be truncated as \''+ test.expectedResult + '\', when wordwise=' + test.wordwise + ', and max=' + test.max + ', and tail=' + test.tail, function() { + expect($truncate(test.value, test.wordwise, test.max, test.tail)).toBe(test.expectedResult); + }); + }); + + }); + +}()); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/filters/umbCmsJoinArray.filter.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/filters/umbCmsJoinArray.filter.spec.js new file mode 100644 index 0000000000..4a44b17bff --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/filters/umbCmsJoinArray.filter.spec.js @@ -0,0 +1,43 @@ +(function () { + + describe('umbCmsJoinArray filter', function() { + var $umbCmsJoinArray; + + var testCases = [ + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:', ', prop:'param' , expectedResult: 'a, b, c'}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:' ', prop:'param' , expectedResult: 'a b c'}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:'', prop:'param' , expectedResult: 'abc'}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:'', prop:'wrong' , expectedResult: ''}, + {input:[], separator:', ', prop:'param' , expectedResult: ''}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:', ', prop:null , expectedResult: ', , '}, + {input:[{param:'a'},{param:'b'},{param:'c'}], separator:null, prop:'param' , expectedResult: 'abc'}, + ]; + + var testCasesWithExpectedError = [ + {input:'test', separator:', ', prop:'param'}, + {input:null, separator:', ', prop:'param'}, + {input:undefined, separator:', ', prop:'param'}, + ]; + + beforeEach(module('umbraco')); + + beforeEach(inject(function($filter) { + $umbCmsJoinArray = $filter('umbCmsJoinArray'); + })); + + + testCases.forEach(function(test){ + it('Blackbox tests with expected result=\''+test.expectedResult+'\'', function() { + expect($umbCmsJoinArray(test.input, test.separator, test.prop)).toBe(test.expectedResult); + }); + }); + + testCasesWithExpectedError.forEach(function(test){ + it('Blackbox tests with expected error. Input=\''+test.input+'\'', function() { + expect(function() { $umbCmsJoinArray(test.input, test.separator, test.prop)}).toThrow(); + }); + }); + + }); + +}()); diff --git a/src/Umbraco.Web.UI.Docs/gulpfile.js b/src/Umbraco.Web.UI.Docs/gulpfile.js index ade7aa64c4..789e2b7845 100644 --- a/src/Umbraco.Web.UI.Docs/gulpfile.js +++ b/src/Umbraco.Web.UI.Docs/gulpfile.js @@ -53,4 +53,4 @@ gulp.task('open:docs', function (cb) { gulp.src(__filename) .pipe(open(options)); cb(); -}); +}); \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 6c20e8c765..f1ae2010db 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -94,10 +94,6 @@ - - all - - @@ -348,6 +344,9 @@ 8300 / http://localhost:8300/ + 8210 + / + http://localhost:8210/ False False diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml index a229898673..9f233a1f8c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/cs.xml @@ -225,7 +225,6 @@ Místní odkaz: Při používání místních odkazů vložte znak "#" před odkaz Otevřít v novém okně? - Nastavení makra Toto makro nemá žádné vlastnosti, které by bylo možno editovat Vložit Editovat oprávnění pro diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index ff299b8e7f..10035ef660 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -332,9 +332,11 @@ Vælg den dokumenttype, du vil oprette en indholdsskabelon til Angiv et navn for mappen Vælg en type og skriv en titel - "dokument typer".]]> + "dokumenttyper".]]> + Document Types within the Settings section.]]> Den valgte side i træet tillader ikke at sider oprettes under den. Rediger tilladelser for denne dokumenttype. + Opret en ny dokumenttype "media typer".]]> Det valgte medie i træet tillader ikke at medier oprettes under det. Rediger tilladelser for denne medietype. @@ -406,6 +408,7 @@ Klip Rediger ordbogsnøgle Rediger sprog + Rediger det valgte medie Indsæt lokalt link Indsæt tegn Indsæt grafisk overskrift @@ -419,7 +422,6 @@ Internt link: Ved lokalt link, indsæt da en "#" foran linket Åben i nyt vindue? - Makroindstillinger Denne makro har ingen egenskaber du kan redigere Indsæt tekst Rediger rettigheder for @@ -511,6 +513,7 @@ Søg i indekset og se resultaterne Værktøjer Værktøjer til at administrere indekset + felter Indtast dit brugernavn @@ -523,7 +526,7 @@ Label... Indtast beskrivelse Søg... - Filtrer... + Filtrér... Indtast nøgleord (tryk på Enter efter hvert nøgleord)... Indtast din e-mail Indtast en besked... @@ -564,6 +567,8 @@ Vælg den mappe, der skal flyttes til i træstrukturen nedenfor blev flyttet under + %0% fjernes egnskaber og egnskabernes data fra følgende elementer]]> + I understand this action will delete the properties and data based on this Data Type Dine data er blevet gemt, men før du kan udgive denne side er der nogle fejl der skal rettes: @@ -639,6 +644,7 @@ Felt Find Første + Fokuspunkt Generelt Grupper Gruppe @@ -933,7 +939,7 @@ Mange hilsner fra Umbraco robotten Sti til fil Absolut sti til fil (f.eks.: /bin/umbraco.bin) Installeret - Installeret pakker + Installerede pakker Installér lokal Afslut Denne pakke har ingen konfigurationsvisning @@ -1356,6 +1362,7 @@ Mange hilsner fra Umbraco robotten Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. 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 Genbrug Editor indstillinger @@ -1712,5 +1719,20 @@ Mange hilsner fra Umbraco robotten Åben backoffice søgning Åben/Luk backoffice hjælp Åben/Luk dine profil indstillinger + Aktivt sprog + Skift sprog til + Opret ny mappe + + Referencer + Denne Data Type har ingen referencer. + Brugt i Dokument Typer + Ingen referencer til Dokument Typer. + Brugt i Medie Typer + Ingen referencer til Medie Typer. + Brugt i Medlems Typer + Ingen referencer til Medlems Typer. + Brugt af + + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml index f529595f65..74f987a4cb 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/de.xml @@ -430,7 +430,6 @@ Anker: Wenn lokale Links verwendet werden, füge ein "#" vor den Link ein In einem neuen Fenster öffnen? - Macro Einstellungen Dieses Makro enthält keine einstellbaren Eigenschaften. Einfügen Berechtigungen bearbeiten für @@ -1590,7 +1589,7 @@ Neues Element Layout auswählen Neue Zeile - Neuer Inhalt + Inhalt hinzufügen Inhalt entfernen Einstellungen anwenden nicht zugelassen]]> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d7d743e7e9..b04a4d4513 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -275,6 +275,8 @@ Are you sure you want to delete this item? Property %0% uses editor %1% which is not supported by Nested Content. No content types are configured for this property. + Add element type + Select element type Add another text box Remove this text box Content root @@ -333,8 +335,11 @@ Enter a folder name Choose a type and a title Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> The selected page in the content tree doesn't allow for any pages to be created below it. Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> The selected media in the tree doesn't allow for any other media to be created below it. Edit permissions for this media type @@ -406,6 +411,7 @@ Cut Edit Dictionary Item Edit Language + Edit selected media Insert local link Insert character Insert graphic headline @@ -414,12 +420,12 @@ Click to add a Macro Insert table This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt Last Edited Link Internal link: When using local links, insert "#" in front of link Open in new window? - Macro Settings This macro does not contain any properties you can edit Paste Edit permissions for @@ -475,7 +481,7 @@ account Select editor Select snippet - This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead. + 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. There are no dictionary items. @@ -512,6 +518,7 @@ Search the index and view the results Tools Tools to manage the index + fields Enter your username @@ -640,6 +647,7 @@ Field Find First + Focal point General Groups Group @@ -1139,7 +1147,7 @@ To manage your website, simply open the Umbraco back office and start adding con Cancel and upload another package I accept terms of use - + Path to file Absolute path to file (ie: /bin/umbraco.bin) Installed @@ -1575,6 +1583,7 @@ To manage your website, simply open the Umbraco back office and start adding con Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. This content type is used in a composition, and therefore cannot be composed itself. There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. Create new Use existing Editor settings @@ -2149,5 +2158,20 @@ To manage your website, simply open the Umbraco back office and start adding con Open backoffice search Open/Close backoffice help Open/Close your profile options + Current language + Switch language to + Create new folder - + + References + This Data Type has no references. + Used in Document Types + No references to Document Types. + Used in Media Types + No references to Media Types. + Used in Member Types + No references to Member Types. + Used by + + + \ No newline at end of file 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 eff344d4f3..77d27ebbf5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -279,6 +279,8 @@ Are you sure you want to delete this item? Property %0% uses editor %1% which is not supported by Nested Content. No content types are configured for this property. + Add element type + Select element type Add another text box Remove this text box Content root @@ -338,8 +340,11 @@ Enter a folder name Choose a type and a title Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> The selected page in the content tree doesn't allow for any pages to be created below it. Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> The selected media in the tree doesn't allow for any other media to be created below it. Edit permissions for this media type Document Type without a template @@ -409,6 +414,7 @@ Cut Edit Dictionary Item Edit Language + Edit selected media Insert local link Insert character Insert graphic headline @@ -417,12 +423,12 @@ Click to add a Macro Insert table This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt Last Edited Link Internal link: When using local links, insert "#" in front of link Open in new window? - Macro Settings This macro does not contain any properties you can edit Paste Edit permissions for @@ -478,7 +484,7 @@ account Select editor Select snippet - This will delete the node and all its languages. If you only want to delete one language go and unpublish it instead. + 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. There are no dictionary items. @@ -515,6 +521,7 @@ Search the index and view the results Tools Tools to manage the index + fields Enter your username @@ -568,6 +575,8 @@ Select the folder to move to in the tree structure below was moved underneath + %0% will delete the properties and their data from the following items]]> + I understand this action will delete the properties and data based on this Data Type Your data has been saved, but before you can publish this page there are some errors you need to fix first: @@ -644,6 +653,7 @@ Field Find First + Focal point General Groups Group @@ -1587,6 +1597,7 @@ To manage your website, simply open the Umbraco back office and start adding con Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. This content type is used in a composition, and therefore cannot be composed itself. There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. Create new Use existing Editor settings @@ -2163,5 +2174,19 @@ To manage your website, simply open the Umbraco back office and start adding con Open backoffice search Open/Close backoffice help Open/Close your profile options + Current language + Switch language to + Create new folder + + + References + This Data Type has no references. + Used in Document Types + No references to Document Types. + Used in Media Types + No references to Media Types. + Used in Member Types + No references to Member Types. + Used by diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml index f8b31577e7..371e6de7a2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/es.xml @@ -311,7 +311,6 @@ Enlace interno Al usar enlaces locales, insertar "#" delante del enlace ¿Abrir en nueva ventana? - Ajustes para la Macro Esta macro no contiene ninguna propiedad que pueda editar Pegar Editar permisos para diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml index 83eb8ecc57..18a5fe9c5a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/fr.xml @@ -324,7 +324,6 @@ Lien interne : Si vous utilisez des ancres, insérez # au début du lien Ouvrir dans une nouvelle fenêtre? - Paramètres de macro Cette macro ne contient aucune propriété éditable Coller Editer les permissions pour diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml index ece1810ef5..9b816b4682 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/he.xml @@ -175,7 +175,6 @@ קישור פנימי: בעת שימוש בקישוריים פנימיים, הוסף "#" בתחילת הקישור לפתוח בחלון חדש? - הגדרות מאקרו המאקרו לא מכיל מאפיינים שניתן לערוך הדבק ערוך הרשאות עבור diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml index 3ae078252a..a7735fba79 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/it.xml @@ -180,7 +180,6 @@ - Impostazioni Macro Incolla Modifica il Permesso per @@ -324,8 +323,7 @@ Mostra la pagina inviata Dimensione Ordina - Submit - + Invia Tipo Su @@ -340,8 +338,8 @@ Benvenuto... Larghezza Si - Reorder - I am done reordering + Riordina + Ho finito di ordinare/key> Colore di sfondo @@ -619,8 +617,8 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i Tabs - Sort order - Creation date + Ordinamento + Data creazione @@ -854,6 +852,6 @@ Per gestire il tuo sito web, è sufficiente aprire il back office di Umbraco e i - Writer + Autore diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml index 303d0f0427..dd68ed45e5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ja.xml @@ -240,7 +240,6 @@ 内部リンク: 内部リンクを使うときは、リンクの前に "#" を挿入してください。 新規ウィンドウで開きますか? - マクロの設定 このマクロは編集できるプロパティがありません 貼り付け 許可の編集 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml index 512d192237..12f7c9ed50 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ko.xml @@ -174,7 +174,6 @@ 내부링크: 내부링크를 사용하실 때 링크앞에 "#"를 넣어주세요 새 창으로 여시겠습니까? - 매크로 세팅 이 매크로에는 편집할 수 있는 항목이 포함되어 있지 않습니다. 붙여넣기 권한 편집 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml index cd6878a196..78b0ebfb5a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nb.xml @@ -2,7 +2,7 @@ The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files Angi domene @@ -234,7 +234,6 @@ Intern link: Ved lokal link, sett inn "#" foran link Åpne i nytt vindu? - Makroinnstillinger Denne makroen har ingen egenskaper du kan endre Lim inn Endre rettigheter for @@ -437,7 +436,7 @@ Databasekonfigurasjon installer-knappen for å installere Umbraco %0% databasen]]> Neste for å fortsette.]]> - Databasen ble ikke funnet! Vennligst sjekk at informasjonen i "connection string" i "web.config"-filen er korrekt.

      For å fortsette, vennligst rediger "web.config"-filen (bruk Visual Studio eller din favoritteditor), rull ned til bunnen, og legg til tilkoblingsstrengen for din database i nøkkelen "umbracoDbDSN" og lagre filen.

      Klikk prøv på nytt når du er ferdig.
      Mer informasjon om redigering av web.config her.

      ]]>
      + Databasen ble ikke funnet! Vennligst sjekk at informasjonen i "connection string" i "web.config"-filen er korrekt.

      For å fortsette, vennligst rediger "web.config"-filen (bruk Visual Studio eller din favoritteditor), rull ned til bunnen, og legg til tilkoblingsstrengen for din database i nøkkelen "umbracoDbDSN" og lagre filen.

      Klikk prøv på nytt når du er ferdig.
      Mer informasjon om redigering av web.config her.

      ]]>
      Vennligst kontakt din ISP om nødvendig. Hvis du installerer på en lokal maskin eller server, må du kanskje skaffe informasjonen fra din systemadministrator.]]> Trykk på knappen oppgrader for å oppgradere databasen din til Umbraco %0%

      Ikke vær urolig - intet innhold vil bli slettet og alt vil fortsette å virke etterpå!

      ]]>
      Trykk Neste for å fortsette.]]> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml index 556a8c4d48..90f06fe7a6 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/nl.xml @@ -263,7 +263,6 @@ Interne link: Plaats een hekje (“#”) voor voor interne links. In nieuw venster openen? - Macro Settings Deze macro heeft geen eigenschappen die u kunt bewerken Plakken Bewerk rechten voor diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml index 055d9eddcf..de3e988118 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pl.xml @@ -312,7 +312,6 @@ Link wewnętrzny: Kiedy używasz odnośników lokalnych, wstaw znak "#" na początku linku Otworzyć w nowym oknie? - Ustawienia Makro To makro nie posiada żadnych właściwości, które można edytować Wklej Edytuj Uprawnienia dla diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml index 767617a2d5..e7afd04acd 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/pt.xml @@ -174,7 +174,6 @@ Link interno: Ao usar links locais insira "#" na frente do link Abrir em nova janela? - Configurações de Macro Este macro não contém nenhuma propriedade que possa ser editada Colar Editar Permissões para diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml index 5f138efc81..c52e17e829 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/ru.xml @@ -360,7 +360,6 @@ Внутренняя ссылка: Для того чтобы определить локальную ссылку, используйте "#" первым символом Открыть в новом окне? - Свойства макроса Этот макрос не имеет редактируемых свойств Заголовок ссылки Ни одной пиктограммы не найдено diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml index 9287856fb2..8ddf2fae26 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/sv.xml @@ -282,7 +282,6 @@ Intern länk: När du använder lokala länkar, lägg till "#" framför länken Öppna i nytt fönster? - Makroinställningar Detta makro innehåller inga egenskaper som du kan redigera Klistra in Redigera rättigheter för diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml index 347f205a1f..02069a53dc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/tr.xml @@ -203,7 +203,6 @@ İç Bağlantı: Yerel bağlantıları kullanırken, bağlantının önündeki "# " insert Yeni pencerede aç - Makro ayarları Düzenleyebileceğiniz Bu makro herhangi bir özellikleri içermiyor Yapıştır İzinleri düzenle diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml index 40a81413e5..a8dd8a8bef 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh.xml @@ -251,7 +251,6 @@ 内部链接: 本地链接请用“#”号开头 在新窗口中打开? - 宏设置 粘贴 编辑权限 diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml index a212826bbc..bac817ad20 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/zh_tw.xml @@ -2,7 +2,7 @@ The Umbraco community - http://our.umbraco.org/documentation/Extending-Umbraco/Language-Files + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files 管理主機名稱 @@ -166,7 +166,7 @@ 子項目 目標 預計發表的時間(伺服器端) - 這是什麼意思?]]> + 這是什麼意思?]]> 點選以便上傳 @@ -248,7 +248,6 @@ 內部連結: 本地連結請用“#”號開頭 在新視窗中打開? - 巨集設置 本巨集沒有包含您可以編輯的屬性 粘貼 編輯許可權 @@ -508,7 +507,7 @@

      請編輯檔案"web.config" (例如使用Visual Studio或您喜歡的編輯器),移動到檔案底部,並在名稱為"UmbracoDbDSN"的字串中設定資料庫連結資訊,並存檔。

      點選重試按鈕當上述步驟完成。
      - + 在此查詢更多編輯web.config的資訊。

      ]]>
      若需要時,請聯繫您的網路公司。如果您在本地機器或伺服器安裝的話,您也許需要聯絡系統管理者。]]> diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml index b7e293eed8..34bb744596 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Rte.cshtml @@ -1,5 +1,9 @@ @model dynamic @using Umbraco.Web.Composing @using Umbraco.Web.Templates - -@Html.Raw(TemplateUtilities.ParseInternalLinks(Model.value.ToString(), Current.UmbracoContext.UrlProvider)) +@{ + var value = TemplateUtilities.ParseInternalLinks(Model.value.ToString(), Current.UmbracoContext.UrlProvider); + value = TemplateUtilities.ResolveUrlsFromTextString(value); + value = TemplateUtilities.ResolveMediaFromTextString(value); +} +@Html.Raw(value) diff --git a/src/Umbraco.Web.UI/config/grid.editors.config.js b/src/Umbraco.Web.UI/config/grid.editors.config.js index 12fa726f21..49b843689b 100644 --- a/src/Umbraco.Web.UI/config/grid.editors.config.js +++ b/src/Umbraco.Web.UI/config/grid.editors.config.js @@ -7,12 +7,14 @@ }, { "name": "Image", + "nameTemplate": "{{ 'Image: ' + (value.udi | ncNodeName) }}", "alias": "media", "view": "media", "icon": "icon-picture" }, { "name": "Macro", + "nameTemplate": "{{ 'Macro: ' + value.macroAlias }}", "alias": "macro", "view": "macro", "icon": "icon-settings-alt" @@ -25,6 +27,7 @@ }, { "name": "Headline", + "nameTemplate": "{{ 'Headline: ' + value }}", "alias": "headline", "view": "textstring", "icon": "icon-coin", @@ -43,4 +46,4 @@ "markup": "
      #value#
      " } } -] \ No newline at end of file +] diff --git a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config index f647e1ae1e..00b556fd34 100644 --- a/src/Umbraco.Web.UI/config/umbracoSettings.Release.config +++ b/src/Umbraco.Web.UI/config/umbracoSettings.Release.config @@ -38,10 +38,9 @@ - In Preview Mode - click to end - ]]> - + Preview modeClick to end
      ]]> + + - In Preview Mode - click to end - ]]> + Preview modeClick to end]]> diff --git a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs index 423e0e3cea..d9a6518493 100644 --- a/src/Umbraco.Web/Cache/ContentCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentCacheRefresher.cs @@ -100,13 +100,7 @@ namespace Umbraco.Web.Cache //if (Suspendable.PageCacheRefresher.CanUpdateDocumentCache) // ... - _publishedSnapshotService.Notify(payloads, out _, out var publishedChanged); - - if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) - { - // when a public version changes - AppCaches.ClearPartialViewCache(); - } + NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); base.Refresh(payloads); } @@ -114,30 +108,35 @@ namespace Umbraco.Web.Cache // these events should never trigger // everything should be PAYLOAD/JSON - public override void RefreshAll() - { - throw new NotSupportedException(); - } + public override void RefreshAll() => throw new NotSupportedException(); - public override void Refresh(int id) - { - throw new NotSupportedException(); - } + public override void Refresh(int id) => throw new NotSupportedException(); - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public override void Remove(int id) - { - throw new NotSupportedException(); - } + public override void Remove(int id) => throw new NotSupportedException(); #endregion #region Json + /// + /// Refreshes the publish snapshot service and if there are published changes ensures that partial view caches are refreshed too + /// + /// + /// + /// + internal static void NotifyPublishedSnapshotService(IPublishedSnapshotService service, AppCaches appCaches, JsonPayload[] payloads) + { + service.Notify(payloads, out _, out var publishedChanged); + + if (payloads.Any(x => x.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) || publishedChanged) + { + // when a public version changes + appCaches.ClearPartialViewCache(); + } + } + public class JsonPayload { public JsonPayload(int id, Guid? key, TreeChangeTypes changeTypes) diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 65b29aabc6..b00a4818f6 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -266,13 +266,21 @@ namespace Umbraco.Web.Cache public static void RefreshLanguageCache(this DistributedCache dc, ILanguage language) { if (language == null) return; - dc.Refresh(LanguageCacheRefresher.UniqueId, language.Id); + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, + language.WasPropertyDirty(nameof(ILanguage.IsoCode)) + ? LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture + : LanguageCacheRefresher.JsonPayload.LanguageChangeType.Update); + + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); } public static void RemoveLanguageCache(this DistributedCache dc, ILanguage language) { if (language == null) return; - dc.Remove(LanguageCacheRefresher.UniqueId, language.Id); + + var payload = new LanguageCacheRefresher.JsonPayload(language.Id, language.IsoCode, LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + dc.RefreshByPayload(LanguageCacheRefresher.UniqueId, new[] { payload }); } #endregion diff --git a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs index 37b0a483a6..4ffe4e2717 100644 --- a/src/Umbraco.Web/Cache/DomainCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/DomainCacheRefresher.cs @@ -47,25 +47,13 @@ namespace Umbraco.Web.Cache // these events should never trigger // everything should be PAYLOAD/JSON - public override void RefreshAll() - { - throw new NotSupportedException(); - } + public override void RefreshAll() => throw new NotSupportedException(); - public override void Refresh(int id) - { - throw new NotSupportedException(); - } + public override void Refresh(int id) => throw new NotSupportedException(); - public override void Refresh(Guid id) - { - throw new NotSupportedException(); - } + public override void Refresh(Guid id) => throw new NotSupportedException(); - public override void Remove(int id) - { - throw new NotSupportedException(); - } + public override void Remove(int id) => throw new NotSupportedException(); #endregion diff --git a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs index 9093124609..8463acd6e0 100644 --- a/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/LanguageCacheRefresher.cs @@ -5,16 +5,18 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Services.Changes; using Umbraco.Web.PublishedCache; +using static Umbraco.Web.Cache.LanguageCacheRefresher.JsonPayload; namespace Umbraco.Web.Cache { - public sealed class LanguageCacheRefresher : CacheRefresherBase + public sealed class LanguageCacheRefresher : PayloadCacheRefresherBase + + //CacheRefresherBase { - public LanguageCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService, IDomainService domainService) + public LanguageCacheRefresher(AppCaches appCaches, IPublishedSnapshotService publishedSnapshotService) : base(appCaches) { _publishedSnapshotService = publishedSnapshotService; - _domainService = domainService; } #region Define @@ -33,41 +35,118 @@ namespace Umbraco.Web.Cache #region Refresher - public override void Refresh(int id) + public override void Refresh(JsonPayload[] payloads) { + if (payloads.Length == 0) return; + + var clearDictionary = false; + var clearContent = false; + + //clear all no matter what type of payload ClearAllIsolatedCacheByEntityType(); - RefreshDomains(id); - base.Refresh(id); + + foreach (var payload in payloads) + { + switch (payload.ChangeType) + { + case LanguageChangeType.Update: + clearDictionary = true; + break; + case LanguageChangeType.Remove: + case LanguageChangeType.ChangeCulture: + clearDictionary = true; + clearContent = true; + break; + } + } + + if (clearDictionary) + { + ClearAllIsolatedCacheByEntityType(); + } + + //if this flag is set, we will tell the published snapshot service to refresh ALL content and evict ALL IContent items + if (clearContent) + { + //clear all domain caches + RefreshDomains(); + ContentCacheRefresher.RefreshContentTypes(AppCaches); // we need to evict all IContent items + //now refresh all nucache + var clearContentPayload = new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }; + ContentCacheRefresher.NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, clearContentPayload); + } + + // then trigger event + base.Refresh(payloads); } - public override void Remove(int id) - { - ClearAllIsolatedCacheByEntityType(); - //if a language is removed, then all dictionary cache needs to be removed - ClearAllIsolatedCacheByEntityType(); - RefreshDomains(id); - base.Remove(id); - } + // these events should never trigger + // everything should be PAYLOAD/JSON + + public override void RefreshAll() => throw new NotSupportedException(); + + public override void Refresh(int id) => throw new NotSupportedException(); + + public override void Refresh(Guid id) => throw new NotSupportedException(); + + public override void Remove(int id) => throw new NotSupportedException(); #endregion - private void RefreshDomains(int langId) + /// + /// Clears all domain caches + /// + private void RefreshDomains() { - var assignedDomains = _domainService.GetAll(true).Where(x => x.LanguageId.HasValue && x.LanguageId.Value == langId).ToList(); + ClearAllIsolatedCacheByEntityType(); - if (assignedDomains.Count > 0) + // note: must do what's above FIRST else the repositories still have the old cached + // content and when the PublishedCachesService is notified of changes it does not see + // the new content... + + var payloads = new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }; + _publishedSnapshotService.Notify(payloads); + } + + #region Json + + public class JsonPayload + { + public JsonPayload(int id, string isoCode, LanguageChangeType changeType) { - // TODO: this is duplicating the logic in DomainCacheRefresher BUT we cannot inject that into this because it it not registered explicitly in the container, - // and we cannot inject the CacheRefresherCollection since that would be a circular reference, so what is the best way to call directly in to the - // DomainCacheRefresher? + Id = id; + IsoCode = isoCode; + ChangeType = changeType; + } - ClearAllIsolatedCacheByEntityType(); - // note: must do what's above FIRST else the repositories still have the old cached - // content and when the PublishedCachesService is notified of changes it does not see - // the new content... - // notify - _publishedSnapshotService.Notify(assignedDomains.Select(x => new DomainCacheRefresher.JsonPayload(x.Id, DomainChangeTypes.Remove)).ToArray()); + public int Id { get; } + public string IsoCode { get; } + public LanguageChangeType ChangeType { get; } + + public enum LanguageChangeType + { + /// + /// A new languages has been added + /// + Add = 0, + + /// + /// A language has been deleted + /// + Remove = 1, + + /// + /// A language has been updated - but it's culture remains the same + /// + Update = 2, + + /// + /// A language has been updated - it's culture has changed + /// + ChangeCulture = 3 } } + + #endregion } } diff --git a/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs b/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs index ab605c7014..ae6d324e84 100644 --- a/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs +++ b/src/Umbraco.Web/ContentApps/ListViewContentAppFactory.cs @@ -100,6 +100,15 @@ namespace Umbraco.Web.ContentApps if (configTabName != null && String.IsNullOrWhiteSpace(configTabName.ToString()) == false) contentApp.Name = configTabName.ToString(); } + + //Override Icon if icon is provided + if (listViewConfig.ContainsKey("icon")) + { + var configIcon = listViewConfig["icon"]; + if (configIcon != null && String.IsNullOrWhiteSpace(configIcon.ToString()) == false) + contentApp.Icon = configIcon.ToString(); + } + // if the list view is configured to show umbContent first, update the list view content app weight accordingly if(listViewConfig.ContainsKey("showContentFirst") && listViewConfig["showContentFirst"]?.ToString().TryConvertTo().Result == true) diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 9d5af028e3..d19c066861 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -1926,10 +1926,8 @@ namespace Umbraco.Web.Editors } if (model.ParentId < 0) { - //cannot move if the content item is not allowed at the root unless there are - //none allowed at root (in which case all should be allowed at root) - var contentTypeService = Services.ContentTypeService; - if (toMove.ContentType.AllowedAsRoot == false && contentTypeService.GetAll().Any(ct => ct.AllowedAsRoot)) + //cannot move if the content item is not allowed at the root + if (toMove.ContentType.AllowedAsRoot == false) { throw new HttpResponseException( Request.CreateNotificationValidationErrorResponse( diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 7c63061159..0f51c35a14 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -414,11 +414,7 @@ namespace Umbraco.Web.Editors IEnumerable types; if (contentId == Constants.System.Root) { - var allContentTypes = Services.ContentTypeService.GetAll().ToList(); - bool AllowedAsRoot(IContentType x) => x.AllowedAsRoot; - types = allContentTypes.Any(AllowedAsRoot) - ? allContentTypes.Where(AllowedAsRoot).ToList() - : allContentTypes; + types = Services.ContentTypeService.GetAll().Where(x => x.AllowedAsRoot).ToList(); } else { diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 75fa8476e7..eef0b5df93 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -140,7 +140,7 @@ namespace Umbraco.Web.Editors break; case "OUR": - urlPrefix = "https://our.umbraco.org/"; + urlPrefix = "https://our.umbraco.com/"; break; case "COM": diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 30c4162252..31e0d70a42 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -283,6 +283,60 @@ namespace Umbraco.Web.Editors : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } + /// + /// Returns the references (usages) for the data type + /// + /// + /// + public DataTypeReferences GetReferences(int id) + { + var result = new DataTypeReferences(); + var usages = Services.DataTypeService.GetReferences(id); + + foreach(var groupOfEntityType in usages.GroupBy(x => x.Key.EntityType)) + { + //get all the GUIDs for the content types to find + var guidsAndPropertyAliases = groupOfEntityType.ToDictionary(i => ((GuidUdi)i.Key).Guid, i => i.Value); + + if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.DocumentType)) + result.DocumentTypes = GetContentTypeUsages(Services.ContentTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MediaType)) + result.MediaTypes = GetContentTypeUsages(Services.MediaTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + else if (groupOfEntityType.Key == ObjectTypes.GetUdiType(UmbracoObjectTypes.MemberType)) + result.MemberTypes = GetContentTypeUsages(Services.MemberTypeService.GetAll(guidsAndPropertyAliases.Keys), guidsAndPropertyAliases); + } + + return result; + } + + /// + /// Maps the found content types and usages to the resulting model + /// + /// + /// + /// + private IEnumerable GetContentTypeUsages( + IEnumerable cts, + IReadOnlyDictionary> usages) + { + return cts.Select(x => new DataTypeReferences.ContentTypeReferences + { + Id = x.Id, + Key = x.Key, + Alias = x.Alias, + Icon = x.Icon, + Name = x.Name, + Udi = new GuidUdi(ObjectTypes.GetUdiType(UmbracoObjectTypes.DocumentType), x.Key), + //only select matching properties + Properties = x.PropertyTypes.Where(p => usages[x.Key].InvariantContains(p.Alias)) + .Select(p => new DataTypeReferences.ContentTypeReferences.PropertyTypeReferences + { + Alias = p.Alias, + Name = p.Name + }) + }); + } + #region ReadOnly actions to return basic data - allow access for: content ,media, members, settings, developer /// /// Gets the content json for all data types diff --git a/src/Umbraco.Web/Editors/LanguageController.cs b/src/Umbraco.Web/Editors/LanguageController.cs index 650dcea6e9..cb7fde23db 100644 --- a/src/Umbraco.Web/Editors/LanguageController.cs +++ b/src/Umbraco.Web/Editors/LanguageController.cs @@ -99,24 +99,28 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); // this is prone to race conditions but the service will not let us proceed anyways - var existing = Services.LocalizationService.GetLanguageByIsoCode(language.IsoCode); + var existingByCulture = Services.LocalizationService.GetLanguageByIsoCode(language.IsoCode); // the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK") // - we need to handle that explicitly - if (existing?.IsoCode != language.IsoCode) + if (existingByCulture?.IsoCode != language.IsoCode) { - existing = null; + existingByCulture = null; } - if (existing != null && language.Id != existing.Id) + if (existingByCulture != null && language.Id != existingByCulture.Id) { //someone is trying to create a language that already exist ModelState.AddModelError("IsoCode", "The language " + language.IsoCode + " already exists"); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - if (existing == null) + var existingById = language.Id != default ? Services.LocalizationService.GetLanguageById(language.Id) : null; + + if (existingById == null) { + //Creating a new lang... + CultureInfo culture; try { @@ -141,38 +145,39 @@ namespace Umbraco.Web.Editors return Mapper.Map(newLang); } - existing.IsMandatory = language.IsMandatory; + existingById.IsMandatory = language.IsMandatory; // note that the service will prevent the default language from being "un-defaulted" // but does not hurt to test here - though the UI should prevent it too - if (existing.IsDefault && !language.IsDefault) + if (existingById.IsDefault && !language.IsDefault) { ModelState.AddModelError("IsDefault", "Cannot un-default the default language."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - existing.IsDefault = language.IsDefault; - existing.FallbackLanguageId = language.FallbackLanguageId; + existingById.IsDefault = language.IsDefault; + existingById.FallbackLanguageId = language.FallbackLanguageId; + existingById.IsoCode = language.IsoCode; // modifying an existing language can create a fallback, verify // note that the service will check again, dealing with race conditions - if (existing.FallbackLanguageId.HasValue) + if (existingById.FallbackLanguageId.HasValue) { var languages = Services.LocalizationService.GetAllLanguages().ToDictionary(x => x.Id, x => x); - if (!languages.ContainsKey(existing.FallbackLanguageId.Value)) + if (!languages.ContainsKey(existingById.FallbackLanguageId.Value)) { ModelState.AddModelError("FallbackLanguage", "The selected fall back language does not exist."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } - if (CreatesCycle(existing, languages)) + if (CreatesCycle(existingById, languages)) { - ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existing.FallbackLanguageId.Value].IsoCode} would create a circular path."); + ModelState.AddModelError("FallbackLanguage", $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path."); throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState)); } } - Services.LocalizationService.Save(existing); - return Mapper.Map(existing); + Services.LocalizationService.Save(existingById); + return Mapper.Map(existingById); } // see LocalizationService diff --git a/src/Umbraco.Web/Editors/MacrosController.cs b/src/Umbraco.Web/Editors/MacrosController.cs index 429e8b6190..3cb161e547 100644 --- a/src/Umbraco.Web/Editors/MacrosController.cs +++ b/src/Umbraco.Web/Editors/MacrosController.cs @@ -229,6 +229,39 @@ namespace Umbraco.Web.Editors return this.Request.CreateResponse(HttpStatusCode.OK, Current.ParameterEditors); } + /// + /// Gets the available parameter editors grouped by their group. + /// + /// + /// The . + /// + public HttpResponseMessage GetGroupedParameterEditors() + { + var parameterEditors = Current.ParameterEditors.ToArray(); + + var grouped = parameterEditors + .GroupBy(x => x.Group.IsNullOrWhiteSpace() ? "" : x.Group.ToLower()) + .OrderBy(x => x.Key) + .ToDictionary(group => group.Key, group => group.OrderBy(d => d.Name).AsEnumerable()); + + return this.Request.CreateResponse(HttpStatusCode.OK, grouped); + } + + /// + /// Get parameter editor by alias. + /// + /// + /// The . + /// + public HttpResponseMessage GetParameterEditorByAlias(string alias) + { + var parameterEditors = Current.ParameterEditors.ToArray(); + + var parameterEditor = parameterEditors.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); + + return this.Request.CreateResponse(HttpStatusCode.OK, parameterEditor); + } + /// /// Returns a error response and optionally logs it /// diff --git a/src/Umbraco.Web/Editors/UsersController.cs b/src/Umbraco.Web/Editors/UsersController.cs index a26bf4fe70..15e5b9404f 100644 --- a/src/Umbraco.Web/Editors/UsersController.cs +++ b/src/Umbraco.Web/Editors/UsersController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Runtime.Serialization; +using System.Security.Cryptography; using System.Threading.Tasks; using System.Web; using System.Web.Http; @@ -105,7 +106,7 @@ namespace Umbraco.Web.Editors if (Current.Configs.Settings().Content.DisallowedUploadFiles.Contains(ext) == false) { //generate a path of known data, we don't want this path to be guessable - user.Avatar = "UserAvatars/" + (user.Id + safeFileName).ToSHA1() + "." + ext; + user.Avatar = "UserAvatars/" + (user.Id + safeFileName).GenerateHash() + "." + ext; using (var fs = System.IO.File.OpenRead(file.LocalFileName)) { diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs index 7d644f8cf1..baf490ce97 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/BaseHttpHeaderCheck.cs @@ -115,7 +115,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security private bool DoHttpHeadersContainHeader(WebResponse response) { - return response.Headers.AllKeys.Contains(_header); + return response.Headers.AllKeys.Contains(_header, StringComparer.InvariantCultureIgnoreCase); } private bool DoMetaTagsContainKeyForHeader(WebResponse response) @@ -186,24 +186,22 @@ namespace Umbraco.Web.HealthCheck.Checks.Security } var removeHeaderElement = customHeadersElement.Elements("remove") - .SingleOrDefault(x => x.Attribute("name") != null && - x.Attribute("name")?.Value == _value); + .SingleOrDefault(x => x.Attribute("name")?.Value.Equals(_value, StringComparison.InvariantCultureIgnoreCase) == true); if (removeHeaderElement == null) { - removeHeaderElement = new XElement("remove"); - removeHeaderElement.Add(new XAttribute("name", _header)); - customHeadersElement.Add(removeHeaderElement); + customHeadersElement.Add( + new XElement("remove", + new XAttribute("name", _header))); } var addHeaderElement = customHeadersElement.Elements("add") - .SingleOrDefault(x => x.Attribute("name") != null && - x.Attribute("name").Value == _header); + .SingleOrDefault(x => x.Attribute("name")?.Value.Equals(_header, StringComparison.InvariantCultureIgnoreCase) == true); if (addHeaderElement == null) { - addHeaderElement = new XElement("add"); - addHeaderElement.Add(new XAttribute("name", _header)); - addHeaderElement.Add(new XAttribute("value", _value)); - customHeadersElement.Add(addHeaderElement); + customHeadersElement.Add( + new XElement("add", + new XAttribute("name", _header), + new XAttribute("value", _value))); } doc.Save(configFile); diff --git a/src/Umbraco.Web/JavaScript/JsInitialize.js b/src/Umbraco.Web/JavaScript/JsInitialize.js index 1ef6e74dfe..4b0a457b64 100644 --- a/src/Umbraco.Web/JavaScript/JsInitialize.js +++ b/src/Umbraco.Web/JavaScript/JsInitialize.js @@ -13,6 +13,7 @@ 'lib/angular-route/angular-route.js', 'lib/angular-cookies/angular-cookies.js', + 'lib/angular-aria/angular-aria.min.js', 'lib/angular-touch/angular-touch.js', 'lib/angular-sanitize/angular-sanitize.js', 'lib/angular-animate/angular-animate.js', diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 706ebf2825..00b17781e0 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -73,7 +73,7 @@ namespace Umbraco.Web } /// - /// Returns a list of cultures that have property validation errors errors + /// Returns a list of cultures that have property validation errors /// /// /// diff --git a/src/Umbraco.Web/Models/ContentEditing/DataTypeReferences.cs b/src/Umbraco.Web/Models/ContentEditing/DataTypeReferences.cs new file mode 100644 index 0000000000..d9cd93e7c8 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DataTypeReferences.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "dataTypeReferences", Namespace = "")] + public class DataTypeReferences + { + [DataMember(Name = "documentTypes")] + public IEnumerable DocumentTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "mediaTypes")] + public IEnumerable MediaTypes { get; set; } = Enumerable.Empty(); + + [DataMember(Name = "memberTypes")] + public IEnumerable MemberTypes { get; set; } = Enumerable.Empty(); + + [DataContract(Name = "contentType", Namespace = "")] + public class ContentTypeReferences : EntityBasic + { + [DataMember(Name = "properties")] + public object Properties { get; set; } + + [DataContract(Name = "property", Namespace = "")] + public class PropertyTypeReferences + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "alias")] + public string Alias { get; set; } + } + } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs index eb474e4cbe..2e44f1327b 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapDefinition.cs @@ -42,9 +42,6 @@ namespace Umbraco.Web.Models.Mapping target.Trashed = source.Trashed; target.Udi = Udi.Create(ObjectTypes.GetUdiType(source.NodeObjectType), source.Key); - if (source.NodeObjectType == Constants.ObjectTypes.Member && target.Icon.IsNullOrWhiteSpace()) - target.Icon = Constants.Icons.Member; - if (source is IContentEntitySlim contentSlim) { source.AdditionalData["ContentTypeAlias"] = contentSlim.ContentTypeAlias; @@ -226,7 +223,18 @@ namespace Umbraco.Web.Models.Mapping } private static string MapContentTypeIcon(IEntitySlim entity) - => entity is ContentEntitySlim contentEntity ? contentEntity.ContentTypeIcon : null; + { + switch (entity) + { + case ContentEntitySlim contentEntity: + // NOTE: this case covers both content and media entities + return contentEntity.ContentTypeIcon; + case MemberEntitySlim memberEntity: + return memberEntity.ContentTypeIcon.IfNullOrWhiteSpace(Constants.Icons.Member); + } + + return null; + } private static string MapName(IEntitySlim source, MapperContext context) { diff --git a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs index 24a41519f7..4a78d201ca 100644 --- a/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs +++ b/src/Umbraco.Web/Mvc/StatusCodeFilterAttribute.cs @@ -1,5 +1,7 @@ using System.Net; using System.Web.Mvc; +using Umbraco.Core; +using Umbraco.Web.Composing; namespace Umbraco.Web.Mvc { @@ -20,6 +22,7 @@ namespace Umbraco.Web.Mvc base.OnActionExecuted(filterContext); filterContext.HttpContext.Response.StatusCode = (int)_statusCode; + filterContext.HttpContext.Response.TrySkipIisCustomErrors = Current.Configs.Settings().WebRouting.TrySkipIisCustomErrors; } } } diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs index f782f09289..24e2fc29a5 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyEditor.cs @@ -89,7 +89,7 @@ namespace Umbraco.Web.PropertyEditors var grid = DeserializeGridValue(rawJson, out var rtes); - var userId = _umbracoContextAccessor.UmbracoContext?.Security.CurrentUser.Id ?? Constants.Security.SuperUserId; + var userId = _umbracoContextAccessor.UmbracoContext?.Security?.CurrentUser?.Id ?? Constants.Security.SuperUserId; // Process the rte values foreach (var rte in rtes) diff --git a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs index 4f854a7556..ce41628ec1 100644 --- a/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/ListViewConfiguration.cs @@ -60,6 +60,9 @@ namespace Umbraco.Web.PropertyEditors Description = "The bulk actions that are allowed from the list view")] public BulkActionPermissionSettings BulkActionPermissions { get; set; } = new BulkActionPermissionSettings(); // TODO: managing defaults? + [ConfigurationField("icon", "Content app icon", "views/propertyeditors/listview/icon.prevalues.html", Description = "The icon of the listview content app")] + public string Icon { get; set; } + [ConfigurationField("tabName", "Content app name", "textstring", Description = "The name of the listview content app (default if empty: 'Child Items')")] public string TabName { get; set; } @@ -74,6 +77,9 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("header")] public string Header { get; set; } + [JsonProperty("nameTemplate")] + public string Template { get; set; } + [JsonProperty("isSystem")] public int IsSystem { get; set; } // TODO: bool } diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerConfiguration.cs deleted file mode 100644 index 1490c4d2a8..0000000000 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerConfiguration.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - /// - /// Represents the configuration for the macro container value editor. - /// - public class MacroContainerConfiguration - { - [ConfigurationField("max", "Max items", "number", Description = "The maximum number of macros that are allowed in the container")] - public int MaxItems { get; set; } - - [ConfigurationField("allowed", "Allowed items", "views/propertyeditors/macrocontainer/macrolist.prevalues.html", Description = "The macro types allowed, if none are selected all macros will be allowed")] - public object AllowedItems { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerConfigurationEditor.cs deleted file mode 100644 index 1ac4f5c874..0000000000 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerConfigurationEditor.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - /// - /// Represents the configuration editor for the macro container value editor. - /// - public class MacroContainerConfigurationEditor : ConfigurationEditor - { } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs deleted file mode 100644 index ff2c08bf62..0000000000 --- a/src/Umbraco.Web/PropertyEditors/MacroContainerPropertyEditor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Web.PropertyEditors -{ - // TODO: MacroContainerPropertyEditor is deprecated, but what's the alternative? - [DataEditor( - Constants.PropertyEditors.Aliases.MacroContainer, - "(Obsolete) Macro Picker", - "macrocontainer", - ValueType = ValueTypes.Text, - Group = Constants.PropertyEditors.Groups.RichContent, - Icon = Constants.Icons.Macro, - IsDeprecated = true)] - public class MacroContainerPropertyEditor : DataEditor - { - public MacroContainerPropertyEditor(ILogger logger) - : base(logger) - { } - - protected override IConfigurationEditor CreateConfigurationEditor() => new MacroContainerConfigurationEditor(); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 7e91a3af79..f511a97cac 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -40,14 +40,6 @@ namespace Umbraco.Web.PropertyEditors // has to be lazy else circular dep in ctor private PropertyEditorCollection PropertyEditors => _propertyEditors.Value; - private static IContentType GetElementType(JObject item) - { - var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject(); - return string.IsNullOrEmpty(contentTypeAlias) - ? null - : Current.Services.ContentTypeService.Get(contentTypeAlias); - } - #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => new NestedContentConfigurationEditor(); @@ -62,11 +54,22 @@ namespace Umbraco.Web.PropertyEditors { private readonly PropertyEditorCollection _propertyEditors; + private readonly Lazy> _contentTypes = new Lazy>(() => + Current.Services.ContentTypeService.GetAll().ToDictionary(c => c.Alias) + ); + public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors) : base(attribute) { _propertyEditors = propertyEditors; - Validators.Add(new NestedContentValidator(propertyEditors)); + Validators.Add(new NestedContentValidator(propertyEditors, GetElementType)); + } + + + private IContentType GetElementType(JObject item) + { + var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject() ?? string.Empty; + return _contentTypes.Value.ContainsKey(contentTypeAlias) ? _contentTypes.Value[contentTypeAlias] : null; } internal ServiceContext Services => Current.Services; @@ -122,6 +125,10 @@ namespace Umbraco.Web.PropertyEditors { // convert the value, and store the converted value var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + if (propEditor == null) + { + continue; + } var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; var valEditor = propEditor.GetValueEditor(tempConfig); var convValue = valEditor.ConvertDbToString(propType, propValues[propAlias]?.ToString(), dataTypeService); @@ -185,6 +192,11 @@ namespace Umbraco.Web.PropertyEditors // convert that temp property, and store the converted value var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + if(propEditor == null) + { + propValues[propAlias] = tempProp.GetValue()?.ToString(); + continue; + } var tempConfig = dataTypeService.GetDataType(propType.DataTypeId).Configuration; var valEditor = propEditor.GetValueEditor(tempConfig); var convValue = valEditor.ToEditor(tempProp, dataTypeService); @@ -249,6 +261,10 @@ namespace Umbraco.Web.PropertyEditors // Lookup the property editor var propEditor = _propertyEditors[propType.PropertyEditorAlias]; + if (propEditor == null) + { + continue; + } // Create a fake content property data object var contentPropData = new ContentPropertyData(propValues[propKey], propConfiguration); @@ -272,10 +288,12 @@ namespace Umbraco.Web.PropertyEditors internal class NestedContentValidator : IValueValidator { private readonly PropertyEditorCollection _propertyEditors; + private readonly Func _getElementType; - public NestedContentValidator(PropertyEditorCollection propertyEditors) + public NestedContentValidator(PropertyEditorCollection propertyEditors, Func getElementType) { _propertyEditors = propertyEditors; + _getElementType = getElementType; } public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) @@ -293,7 +311,7 @@ namespace Umbraco.Web.PropertyEditors var o = value[i]; var propValues = (JObject) o; - var contentType = GetElementType(propValues); + var contentType = _getElementType(propValues); if (contentType == null) continue; var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray(); @@ -306,6 +324,11 @@ namespace Umbraco.Web.PropertyEditors var config = dataTypeService.GetDataType(propType.DataTypeId).Configuration; var propertyEditor = _propertyEditors[propType.PropertyEditorAlias]; + if (propertyEditor == null) + { + continue; + } + foreach (var validator in propertyEditor.GetValueEditor().Validators) { foreach (var result in validator.Validate(propValues[propKey], propertyEditor.GetValueEditor().ValueType, config)) diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 3eed40c8bf..5743e9c1d5 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -114,7 +114,7 @@ namespace Umbraco.Web.PropertyEditors if (editorValue.Value == null) return null; - var userId = _umbracoContextAccessor.UmbracoContext?.Security.CurrentUser.Id ?? Constants.Security.SuperUserId; + var userId = _umbracoContextAccessor.UmbracoContext?.Security?.CurrentUser?.Id ?? Constants.Security.SuperUserId; var config = editorValue.DataTypeConfiguration as RichTextConfiguration; var mediaParent = config?.MediaParentId; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs deleted file mode 100644 index ef356e3604..0000000000 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MacroContainerValueConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Web; -using Umbraco.Core; -using Umbraco.Core.Cache; -using Umbraco.Core.Macros; -using Umbraco.Core.Models.PublishedContent; -using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Web.Macros; - -namespace Umbraco.Web.PropertyEditors.ValueConverters -{ - /// - /// Ensures macro syntax is parsed for the macro container which will work when getting the field - /// values in any way (i.e. dynamically, using Field(), or IPublishedContent) - /// - [DefaultPropertyValueConverter] - public class MacroContainerValueConverter : PropertyValueConverterBase - { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IMacroRenderer _macroRenderer; - - public MacroContainerValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer) - { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _macroRenderer = macroRenderer ?? throw new ArgumentNullException(nameof(macroRenderer)); - } - - public override bool IsConverter(IPublishedPropertyType propertyType) - => propertyType.EditorAlias == Constants.PropertyEditors.Aliases.MacroContainer; - - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IHtmlString); - - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; - - // NOT thread-safe over a request because it modifies the - // global UmbracoContext.Current.InPreviewMode status. So it - // should never execute in // over the same UmbracoContext with - // different preview modes. - string RenderMacros(string source, bool preview) - { - var umbracoContext = _umbracoContextAccessor.UmbracoContext; - using (umbracoContext.ForcedPreview(preview)) // force for macro rendering - { - var sb = new StringBuilder(); - - MacroTagParser.ParseMacros( - source, - //callback for when text block is found - textBlock => sb.Append(textBlock), - //callback for when macro syntax is found - (macroAlias, macroAttributes) => sb.Append(_macroRenderer.Render( - macroAlias, - umbracoContext.PublishedRequest?.PublishedContent, - //needs to be explicitly casted to Dictionary - macroAttributes.ConvertTo(x => (string)x, x => x)).ToString())); - - return sb.ToString(); - } - } - - public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) - { - if (source == null) return null; - var sourceString = source.ToString(); - - // ensure string is parsed for macros and macros are executed correctly - sourceString = RenderMacros(sourceString, preview); - - return new HtmlString(sourceString); - } - } -} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs index 5f8e81fd31..3b4859432d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentNode.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Umbraco.Core.Models.PublishedContent; using Umbraco.Web.PublishedCache.NuCache.DataSource; @@ -6,6 +7,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { // represents a content "node" ie a pair of draft + published versions // internal, never exposed, to be accessed from ContentStore (only!) + [DebuggerDisplay("Id: {Id}, Path: {Path}")] internal class ContentNode { // special ctor for root pseudo node diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index f0d695f090..f71abd6aa7 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -549,7 +549,10 @@ namespace Umbraco.Web.PublishedCache.NuCache // manage children if (existing != null) + { kit.Node.FirstChildContentId = existing.FirstChildContentId; + kit.Node.LastChildContentId = existing.LastChildContentId; + } // set SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); @@ -571,6 +574,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { // replacing existing, handle siblings kit.Node.NextSiblingContentId = existing.NextSiblingContentId; + kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; } _xmap[kit.Node.Uid] = kit.Node.Id; @@ -729,7 +733,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // clear if (existing != null) { + //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? RemoveTreeNodeLocked(existing); } @@ -1002,9 +1008,9 @@ namespace Umbraco.Web.PublishedCache.NuCache } // else it's going somewhere in the middle, - // and this is bad, perfs-wise - we only do it when moving - // inserting in linked list is slow, optimizing would require trees - // but... that should not happen very often - and not on large amount of data + // TODO: There was a note about performance when this occurs and that this only happens when moving and not very often, but that is not true, + // this also happens anytime a middle node is unpublished or republished (which causes a branch update), i'm unsure if this has perf impacts, + // i think this used to but it doesn't seem bad anymore that I can see... while (child.NextSiblingContentId > 0) { // get next child diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 629f5694bc..e76b526492 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -235,6 +235,8 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentTypeService.ScopedRefreshedEntity += OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity += OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity += OnMemberTypeRefreshedEntity; + + LocalizationService.SavedLanguage += OnLanguageSaved; } private void TearDownRepositoryEvents() @@ -252,6 +254,8 @@ namespace Umbraco.Web.PublishedCache.NuCache ContentTypeService.ScopedRefreshedEntity -= OnContentTypeRefreshedEntity; MediaTypeService.ScopedRefreshedEntity -= OnMediaTypeRefreshedEntity; MemberTypeService.ScopedRefreshedEntity -= OnMemberTypeRefreshedEntity; + + LocalizationService.SavedLanguage -= OnLanguageSaved; } public override void Dispose() @@ -1260,6 +1264,21 @@ namespace Umbraco.Web.PublishedCache.NuCache RebuildMemberDbCache(contentTypeIds: memberTypeIds); } + /// + /// If a is ever saved with a different culture, we need to rebuild all of the content nucache table + /// + /// + /// + private void OnLanguageSaved(ILocalizationService sender, Core.Events.SaveEventArgs e) + { + //culture changed on an existing language + var cultureChanged = e.SavedEntities.Any(x => !x.WasPropertyDirty(nameof(ILanguage.Id)) && x.WasPropertyDirty(nameof(ILanguage.IsoCode))); + if(cultureChanged) + { + RebuildContentDbCache(); + } + } + private ContentNuDto GetDto(IContentBase content, bool published) { // should inject these in ctor diff --git a/src/Umbraco.Web/PublishedContentExtensions.cs b/src/Umbraco.Web/PublishedContentExtensions.cs index d15d5817f6..75eb6adbcb 100644 --- a/src/Umbraco.Web/PublishedContentExtensions.cs +++ b/src/Umbraco.Web/PublishedContentExtensions.cs @@ -38,7 +38,7 @@ namespace Umbraco.Web /// A value indicating whether the content is of a content type composed of a content type identified by the alias. public static bool IsComposedOf(this IPublishedContent content, string alias) { - return content.ContentType.CompositionAliases.Contains(alias); + return content.ContentType.CompositionAliases.InvariantContains(alias); } #endregion @@ -421,7 +421,7 @@ namespace Umbraco.Web /// Does not consider the content itself. Returns all ancestors, of the specified content type. public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(false, n => n.ContentType.Alias == contentTypeAlias); + return content.AncestorsOrSelf(false, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -486,7 +486,7 @@ namespace Umbraco.Web /// May or may not begin with the content itself, depending on its content type. public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.AncestorsOrSelf(true, n => n.ContentType.Alias == contentTypeAlias); + return content.AncestorsOrSelf(true, n => n.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -549,7 +549,7 @@ namespace Umbraco.Web /// Does not consider the content itself. May return null. public static IPublishedContent Ancestor(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateAncestors(false).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -612,7 +612,7 @@ namespace Umbraco.Web /// May or may not return the content itself depending on its content type. May return null. public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) { - return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateAncestors(true).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } /// @@ -727,7 +727,7 @@ namespace Umbraco.Web public static IEnumerable DescendantsOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.DescendantsOrSelf(false, p => p.ContentType.Alias == contentTypeAlias, culture); + return content.DescendantsOrSelf(false, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); } public static IEnumerable Descendants(this IPublishedContent content, string culture = null) @@ -754,7 +754,7 @@ namespace Umbraco.Web public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.DescendantsOrSelf(true, p => p.ContentType.Alias == contentTypeAlias, culture); + return content.DescendantsOrSelf(true, p => p.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); } public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string culture = null) @@ -781,7 +781,7 @@ namespace Umbraco.Web public static IPublishedContent DescendantOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.EnumerateDescendants(false, culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateDescendants(false, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } public static T Descendant(this IPublishedContent content, string culture = null) @@ -808,7 +808,7 @@ namespace Umbraco.Web public static IPublishedContent DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.EnumerateDescendants(true, culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAlias); + return content.EnumerateDescendants(true, culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias)); } public static T DescendantOrSelf(this IPublishedContent content, string culture = null) @@ -892,7 +892,7 @@ namespace Umbraco.Web /// The children of the content, of any of the specified types. public static IEnumerable ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string culture = null) { - return content.Children(x => contentTypeAlias.InvariantContains(x.ContentType.Alias), culture); + return content.Children(x => x.ContentType.Alias.InvariantEquals(contentTypeAlias), culture); } /// @@ -973,7 +973,7 @@ namespace Umbraco.Web ? content.Children(culture).Any() ? content.Children(culture).ElementAt(0) : null - : content.Children(culture).FirstOrDefault(x => x.ContentType.Alias == contentTypeAliasFilter); + : content.Children(culture).FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); if (firstNode == null) return new DataTable(); //no children found @@ -993,7 +993,7 @@ namespace Umbraco.Web { if (contentTypeAliasFilter.IsNullOrWhiteSpace() == false) { - if (n.ContentType.Alias != contentTypeAliasFilter) + if (n.ContentType.Alias.InvariantEquals(contentTypeAliasFilter) == false) continue; //skip this one, it doesn't match the filter } diff --git a/src/Umbraco.Web/PublishedElementExtensions.cs b/src/Umbraco.Web/PublishedElementExtensions.cs index c35c85c606..f28581eb42 100644 --- a/src/Umbraco.Web/PublishedElementExtensions.cs +++ b/src/Umbraco.Web/PublishedElementExtensions.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web /// A value indicating whether the content is of a content type composed of a content type identified by the alias. public static bool IsComposedOf(this IPublishedElement content, string alias) { - return content.ContentType.CompositionAliases.Contains(alias); + return content.ContentType.CompositionAliases.InvariantContains(alias); } #endregion @@ -165,8 +165,9 @@ namespace Umbraco.Web public static IEnumerable OfTypes(this IEnumerable contents, params string[] types) where T : IPublishedElement { - types = types.Select(x => x.ToLowerInvariant()).ToArray(); - return contents.Where(x => types.Contains(x.ContentType.Alias.ToLowerInvariant())); + if (types == null || types.Length == 0) return Enumerable.Empty(); + + return contents.Where(x => types.InvariantContains(x.ContentType.Alias)); } #endregion diff --git a/src/Umbraco.Web/Search/ExamineComponent.cs b/src/Umbraco.Web/Search/ExamineComponent.cs index f34b1f862b..149b4d1436 100644 --- a/src/Umbraco.Web/Search/ExamineComponent.cs +++ b/src/Umbraco.Web/Search/ExamineComponent.cs @@ -27,6 +27,7 @@ namespace Umbraco.Web.Search private readonly IPublishedContentValueSetBuilder _publishedContentValueSetBuilder; private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly IValueSetBuilder _memberValueSetBuilder; + private readonly BackgroundIndexRebuilder _backgroundIndexRebuilder; private static object _isConfiguredLocker = new object(); private readonly IScopeProvider _scopeProvider; private readonly ServiceContext _services; @@ -47,7 +48,8 @@ namespace Umbraco.Web.Search IContentValueSetBuilder contentValueSetBuilder, IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, - IValueSetBuilder memberValueSetBuilder) + IValueSetBuilder memberValueSetBuilder, + BackgroundIndexRebuilder backgroundIndexRebuilder) { _services = services; _scopeProvider = scopeProvider; @@ -56,7 +58,7 @@ namespace Umbraco.Web.Search _publishedContentValueSetBuilder = publishedContentValueSetBuilder; _mediaValueSetBuilder = mediaValueSetBuilder; _memberValueSetBuilder = memberValueSetBuilder; - + _backgroundIndexRebuilder = backgroundIndexRebuilder; _mainDom = mainDom; _logger = profilingLogger; _indexCreator = indexCreator; @@ -111,6 +113,7 @@ namespace Umbraco.Web.Search ContentTypeCacheRefresher.CacheUpdated += ContentTypeCacheRefresherUpdated; MediaCacheRefresher.CacheUpdated += MediaCacheRefresherUpdated; MemberCacheRefresher.CacheUpdated += MemberCacheRefresherUpdated; + LanguageCacheRefresher.CacheUpdated += LanguageCacheRefresherUpdated; } public void Terminate() @@ -320,6 +323,25 @@ namespace Umbraco.Web.Search } } + private void LanguageCacheRefresherUpdated(LanguageCacheRefresher sender, CacheRefresherEventArgs e) + { + if (!(e.MessageObject is LanguageCacheRefresher.JsonPayload[] payloads)) + return; + + if (payloads.Length == 0) return; + + var removedOrCultureChanged = payloads.Any(x => + x.ChangeType == LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture + || x.ChangeType == LanguageCacheRefresher.JsonPayload.LanguageChangeType.Remove); + + if (removedOrCultureChanged) + { + //if a lang is removed or it's culture has changed, we need to rebuild the indexes since + //field names and values in the index have a string culture value. + _backgroundIndexRebuilder.RebuildIndexes(false); + } + } + /// /// Updates indexes based on content type changes /// diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index 0ade432d70..b30f0cbe03 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -29,6 +29,7 @@ namespace Umbraco.Web.Search composition.Register(Lifetime.Singleton); composition.Register(Lifetime.Singleton); + composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(factory => new ContentValueSetBuilder( diff --git a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs index 5ac5529be5..ec536b9d75 100644 --- a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs +++ b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Logging; using Umbraco.Core.Services; using Umbraco.Examine; @@ -19,18 +20,20 @@ namespace Umbraco.Web.Search public UmbracoIndexesCreator(IProfilingLogger profilingLogger, ILocalizationService languageService, IPublicAccessService publicAccessService, - IMemberService memberService) + IMemberService memberService, IUmbracoIndexConfig umbracoIndexConfig) { ProfilingLogger = profilingLogger ?? throw new System.ArgumentNullException(nameof(profilingLogger)); LanguageService = languageService ?? throw new System.ArgumentNullException(nameof(languageService)); PublicAccessService = publicAccessService ?? throw new System.ArgumentNullException(nameof(publicAccessService)); MemberService = memberService ?? throw new System.ArgumentNullException(nameof(memberService)); + UmbracoIndexConfig = umbracoIndexConfig; } protected IProfilingLogger ProfilingLogger { get; } protected ILocalizationService LanguageService { get; } protected IPublicAccessService PublicAccessService { get; } protected IMemberService MemberService { get; } + protected IUmbracoIndexConfig UmbracoIndexConfig { get; } /// /// Creates the Umbraco indexes @@ -55,7 +58,8 @@ namespace Umbraco.Web.Search new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, LanguageService, - GetContentValueSetValidator()); + GetContentValueSetValidator() + ); return index; } @@ -80,28 +84,31 @@ namespace Umbraco.Web.Search CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.MembersIndexPath), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, - GetMemberValueSetValidator()); + GetMemberValueSetValidator() + ); return index; } - + [Obsolete("This method should not be used and will be removed in future versions. GetContentValueSetValidator was moved to IUmbracoIndexConfig")] public virtual IContentValueSetValidator GetContentValueSetValidator() { - return new ContentValueSetValidator(false, true, PublicAccessService); + return UmbracoIndexConfig.GetContentValueSetValidator(); } - + [Obsolete("This method should not be used and will be removed in future versions. GetPublishedContentValueSetValidator was moved to IUmbracoIndexConfig")] public virtual IContentValueSetValidator GetPublishedContentValueSetValidator() { - return new ContentValueSetValidator(true, false, PublicAccessService); + return UmbracoIndexConfig.GetPublishedContentValueSetValidator(); } /// /// Returns the for the member indexer /// /// + [Obsolete("This method should not be used and will be removed in future versions. GetMemberValueSetValidator was moved to IUmbracoIndexConfig")] public virtual IValueSetValidator GetMemberValueSetValidator() { - return new MemberValueSetValidator(); + return UmbracoIndexConfig.GetMemberValueSetValidator(); } + } } diff --git a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs index c3c3dc75ce..b351cca972 100644 --- a/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs +++ b/src/Umbraco.Web/Search/UmbracoTreeSearcher.cs @@ -7,6 +7,8 @@ using Examine; using Umbraco.Core; using Umbraco.Core.Mapping; using Umbraco.Core.Models; +using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; using Umbraco.Core.Services; using Umbraco.Examine; using Umbraco.Web.Models.ContentEditing; @@ -14,6 +16,7 @@ using Umbraco.Web.Trees; namespace Umbraco.Web.Search { + /// /// Used for internal Umbraco implementations of /// @@ -24,22 +27,25 @@ namespace Umbraco.Web.Search private readonly ILocalizationService _languageService; private readonly IEntityService _entityService; private readonly UmbracoMapper _mapper; + private readonly ISqlContext _sqlContext; public UmbracoTreeSearcher(IExamineManager examineManager, UmbracoContext umbracoContext, ILocalizationService languageService, IEntityService entityService, - UmbracoMapper mapper) + UmbracoMapper mapper, + ISqlContext sqlContext) { _examineManager = examineManager ?? throw new ArgumentNullException(nameof(examineManager)); _umbracoContext = umbracoContext; _languageService = languageService; _entityService = entityService; _mapper = mapper; + _sqlContext = sqlContext; } /// - /// Searches for results based on the entity type + /// Searches Examine for results based on the entity type /// /// /// @@ -61,7 +67,7 @@ namespace Umbraco.Web.Search string type; var indexName = Constants.UmbracoIndexes.InternalIndexName; - var fields = new[] { "id", "__NodeId" }; + var fields = new[] { "id", "__NodeId", "__Key" }; // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string // manipulation for things like start paths, member types, etc... @@ -70,12 +76,18 @@ namespace Umbraco.Web.Search //} + //special GUID check since if a user searches on one specifically we need to escape it + if (Guid.TryParse(query, out var g)) + { + query = "\"" + g.ToString() + "\""; + } + switch (entityType) { case UmbracoEntityTypes.Member: indexName = Constants.UmbracoIndexes.MembersIndexName; type = "member"; - fields = new[] { "id", "__NodeId", "email", "loginName" }; + fields = new[] { "id", "__NodeId", "__Key", "email", "loginName" }; if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1") { sb.Append("+__NodeTypeAlias:"); @@ -129,6 +141,26 @@ namespace Umbraco.Web.Search } } + /// + /// Searches with the for results based on the entity type + /// + /// + /// + /// + /// + /// + /// + /// + public IEnumerable EntitySearch(UmbracoObjectTypes objectType, string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) + { + //if it's a GUID, match it + Guid.TryParse(query, out var g); + + var results = _entityService.GetPagedDescendants(objectType, pageIndex, pageSize, out totalFound, + filter: _sqlContext.Query().Where(x => x.Name.Contains(query) || x.Key == g)); + return _mapper.MapEnumerable(results); + } + private bool BuildQuery(StringBuilder sb, string query, string searchFrom, string[] fields, string type) { //build a lucene query: diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 970191e510..3373a6fa8b 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -15,6 +15,10 @@ using Umbraco.Web.WebApi.Filters; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; namespace Umbraco.Web.Trees { @@ -36,20 +40,21 @@ namespace Umbraco.Web.Trees private readonly UmbracoTreeSearcher _treeSearcher; private readonly ActionCollection _actions; - public ContentTreeController(UmbracoTreeSearcher treeSearcher, ActionCollection actions) - { - _treeSearcher = treeSearcher; - _actions = actions; - } - protected override int RecycleBinId => Constants.System.RecycleBinContent; protected override bool RecycleBinSmells => Services.ContentService.RecycleBinSmells(); private int[] _userStartNodes; + protected override int[] UserStartNodes => _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService)); + public ContentTreeController(UmbracoTreeSearcher treeSearcher, ActionCollection actions, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + _actions = actions; + } + /// protected override TreeNode GetSingleTreeNode(IEntitySlim entity, string parentId, FormDataCollection queryStrings) { diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 015c91cb81..c3458f93a2 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -18,11 +18,23 @@ using System.Web.Http.ModelBinding; using Umbraco.Web.Actions; using Umbraco.Web.Composing; using Umbraco.Core.Security; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence; namespace Umbraco.Web.Trees { public abstract class ContentTreeControllerBase : TreeController { + + protected ContentTreeControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + } + + protected ContentTreeControllerBase() + { + } + #region Actions /// @@ -529,6 +541,8 @@ namespace Umbraco.Web.Trees private bool? _ignoreUserStartNodes; + + /// /// If the request should allows a user to choose nodes that they normally don't have access to /// diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index bead6aa141..e8f8fb9f75 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -3,11 +3,17 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; +using Umbraco.Web.Search; using Umbraco.Web.WebApi.Filters; namespace Umbraco.Web.Trees @@ -18,6 +24,13 @@ namespace Umbraco.Web.Trees [CoreTree] public class ContentTypeTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public ContentTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + } + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var root = base.CreateRootNode(queryStrings); @@ -49,8 +62,10 @@ namespace Umbraco.Web.Trees //if the request is for folders only then just return if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + var children = Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DocumentType).ToArray(); + var contentTypes = Services.ContentTypeService.GetAll(children.Select(c => c.Id).ToArray()).ToDictionary(c => c.Id); nodes.AddRange( - Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DocumentType) + children .OrderBy(entity => entity.Name) .Select(dt => { @@ -60,6 +75,12 @@ namespace Umbraco.Web.Trees var node = CreateTreeNode(dt, Constants.ObjectTypes.DocumentType, id, queryStrings, Constants.Icons.ContentType, hasChildren); node.Path = dt.Path; + + // enrich the result with content type data that's not available in the entity service output + var contentType = contentTypes[dt.Id]; + node.Alias = contentType.Alias; + node.AdditionalData["isElement"] = contentType.IsElement; + return node; })); @@ -135,10 +156,7 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.DocumentType, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.DocumentType, query, pageSize, pageIndex, out totalFound, searchFrom); + } } diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 2465b4d45a..4e5b1df631 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -12,6 +12,11 @@ using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; using Constants = Umbraco.Core.Constants; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Web.Search; namespace Umbraco.Web.Trees { @@ -21,6 +26,13 @@ namespace Umbraco.Web.Trees [CoreTree] public class DataTypeTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public DataTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + } + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var intId = id.TryConvertTo(); @@ -148,10 +160,6 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.DataType, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.DataType, query, pageSize, pageIndex, out totalFound, searchFrom); } } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index f4f373f9a4..df44a809c9 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -16,6 +16,10 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Search; using Constants = Umbraco.Core.Constants; using Umbraco.Core.Services.Implement; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; namespace Umbraco.Web.Trees { @@ -35,7 +39,7 @@ namespace Umbraco.Web.Trees { private readonly UmbracoTreeSearcher _treeSearcher; - public MediaTreeController(UmbracoTreeSearcher treeSearcher) + public MediaTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { _treeSearcher = treeSearcher; } diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 53e30a4ee5..f85aefcace 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -10,6 +10,11 @@ using Umbraco.Web.WebApi.Filters; using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence; +using Umbraco.Web.Search; namespace Umbraco.Web.Trees { @@ -19,6 +24,13 @@ namespace Umbraco.Web.Trees [CoreTree] public class MediaTypeTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public MediaTypeTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + } + protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings) { var intId = id.TryConvertTo(); @@ -117,10 +129,7 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.MediaType, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.MediaType, query, pageSize, pageIndex, out totalFound, searchFrom); + } } diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index 2657f13255..c0a9d15cfa 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -131,7 +131,7 @@ namespace Umbraco.Web.Trees { nodes.AddRange(Services.MemberTypeService.GetAll() .Select(memberType => - CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, Constants.Icons.MemberType, true, + CreateTreeNode(memberType.Alias, id, queryStrings, memberType.Name, memberType.Icon.IfNullOrWhiteSpace(Constants.Icons.Member), true, queryStrings.GetRequiredValue("application") + TreeAlias.EnsureStartsWith('/') + "/list/" + memberType.Alias))); } } diff --git a/src/Umbraco.Web/Trees/TemplatesTreeController.cs b/src/Umbraco.Web/Trees/TemplatesTreeController.cs index 8c37718c9b..d0ca8d1b2e 100644 --- a/src/Umbraco.Web/Trees/TemplatesTreeController.cs +++ b/src/Umbraco.Web/Trees/TemplatesTreeController.cs @@ -3,12 +3,18 @@ using System.Globalization; using System.Linq; using System.Net.Http.Formatting; using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; using Umbraco.Web.Actions; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; using Umbraco.Web.Mvc; +using Umbraco.Web.Search; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; @@ -20,6 +26,13 @@ namespace Umbraco.Web.Trees [CoreTree] public class TemplatesTreeController : TreeController, ISearchableTree { + private readonly UmbracoTreeSearcher _treeSearcher; + + public TemplatesTreeController(UmbracoTreeSearcher treeSearcher, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) + { + _treeSearcher = treeSearcher; + } + protected override TreeNode CreateRootNode(FormDataCollection queryStrings) { var root = base.CreateRootNode(queryStrings); @@ -119,10 +132,6 @@ namespace Umbraco.Web.Trees } public IEnumerable Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null) - { - var results = Services.EntityService.GetPagedDescendants(UmbracoObjectTypes.Template, pageIndex, pageSize, out totalFound, - filter: SqlContext.Query().Where(x => x.Name.Contains(query))); - return Mapper.MapEnumerable(results); - } + => _treeSearcher.EntitySearch(UmbracoObjectTypes.Template, query, pageSize, pageIndex, out totalFound, searchFrom); } } diff --git a/src/Umbraco.Web/Trees/TreeControllerBase.cs b/src/Umbraco.Web/Trees/TreeControllerBase.cs index 0f9b61469e..77e5ebef62 100644 --- a/src/Umbraco.Web/Trees/TreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/TreeControllerBase.cs @@ -265,6 +265,7 @@ namespace Umbraco.Web.Trees treeNode.Path = entity.Path; treeNode.Udi = Udi.Create(ObjectTypes.GetUdiType(entityObjectType), entity.Key); treeNode.HasChildren = hasChildren; + treeNode.Trashed = entity.Trashed; return treeNode; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a0ce64943a..e04adc9e84 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -212,6 +212,7 @@ + @@ -486,8 +487,6 @@ - - @@ -810,7 +809,6 @@ - @@ -848,7 +846,6 @@ - diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 9065341ab9..2ec4141f5a 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -1,7 +1,6 @@ using System; using System.Web; using Umbraco.Core; -using Umbraco.Core.Collections; using Umbraco.Core.Logging; using Umbraco.Web.Composing; using Umbraco.Web.Routing; @@ -102,6 +101,5 @@ namespace Umbraco.Web return end; } - } } diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index 8a2e54ca02..4600c02ac1 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -1,8 +1,10 @@ -using Umbraco.Core; +using System; +using Umbraco.Core; using Umbraco.Core.Cache; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Mapping; using Umbraco.Core.Persistence; using Umbraco.Core.Services; @@ -17,9 +19,15 @@ namespace Umbraco.Web.WebApi { } + [Obsolete("This constructor is obsolete since it doesn't inject the UmbracoMapper. The UmbracoMapper will be resolved from the service locator Current.Mapper, which is not good for testability. Inject the UmbracoMapper using full constructor injection instead.")] protected UmbracoApiController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper) { } + + protected UmbracoApiController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, UmbracoMapper umbracoMapper) + : base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper, umbracoMapper) + { + } } } diff --git a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs index 89a630fb5d..a1cd7708e2 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiControllerBase.cs @@ -40,13 +40,28 @@ namespace Umbraco.Web.WebApi Current.Factory.GetInstance(), Current.Factory.GetInstance(), Current.Factory.GetInstance(), - Current.Factory.GetInstance() + Current.Factory.GetInstance(), + Current.Factory.GetInstance() ) { } /// /// Initializes a new instance of the class with all its dependencies. /// + protected UmbracoApiControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper, UmbracoMapper umbracoMapper) + { + UmbracoContextAccessor = umbracoContextAccessor; + GlobalSettings = globalSettings; + SqlContext = sqlContext; + Services = services; + AppCaches = appCaches; + Logger = logger; + RuntimeState = runtimeState; + Umbraco = umbracoHelper; + Mapper = umbracoMapper; + } + + [Obsolete("This constructor is obsolete since it doesn't inject the UmbracoMapper. The UmbracoMapper will be resolved from the service locator Current.Mapper, which is not good for testability. Inject the UmbracoMapper using full constructor injection instead.")] protected UmbracoApiControllerBase(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper) { UmbracoContextAccessor = umbracoContextAccessor; @@ -58,7 +73,7 @@ namespace Umbraco.Web.WebApi RuntimeState = runtimeState; Umbraco = umbracoHelper; - // fixme - can we break all ctors? + // not good for testability, hence the obsolete. Mapper = Current.Mapper; }