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..2679eaa411 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,15 +2,15 @@ 👍🎉 First off, thanks for taking the time to contribute! 🎉👍 -The following is a set of guidelines for contributing to Umbraco CMS. +The following is a set of guidelines, for contributing to Umbraco CMS. -These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. +These are mostly guidelines, not rules. Use your best judgement, and feel free to propose changes to this document in a pull request. Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you 💖. **Code of conduct** -This project and everyone participating in it is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). +This project and everyone participating in it, is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). **Table of contents** @@ -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,30 +146,34 @@ 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! ### Keeping your Umbraco fork in sync with the main repository -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. +We recommend you to 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 have 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 new file mode 100644 index 0000000000..0ac35e6897 --- /dev/null +++ b/.github/CONTRIBUTION_GUIDELINES.md @@ -0,0 +1,35 @@ +# Contributing to Umbraco CMS + +When you’re considering creating a pull request for Umbraco CMS, we will categorize them in two different sizes, small and large. + +The process for both sizes is very similar, as [explained in the contribution document](CONTRIBUTING.md#how-do-i-begin). + +## Small PRs +Bug fixes and small improvements - can be recognized by seeing a small number of changes and possibly a small number of new files. + +We’re usually able to handle small PRs pretty quickly. A community volunteer will do the initial review and flag it for Umbraco HQ as “community tested”. If everything looks good, it will be merged pretty quickly [as per the described process](REVIEW_PROCESS.md). + +### Up for grabs + +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 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.). + +We would love to follow the same process for larger PRs but this is not always possible due to time limitations and priorities that need to be aligned. We don’t want to put up any barriers, but this document should set the correct expectations. + +Please make sure to describe your idea in an issue, it helps to put in mockup screenshots or videos. + +If the change makes sense for HQ to include in Umbraco CMS we will leave you some feedback on how we’d like to see it being implemented. + +If a larger pull request is encouraged by Umbraco HQ, the process will be similar to what is described in the [small PRs process](#small-prs) above, we strive to feedback within 14 days. Finalizing and merging the PR might take longer though as it will likely need to be picked up by the development team to make sure everything is in order. We’ll keep you posted on the progress. + +It is highly recommended that you speak to the HQ before making large, complex changes. + +### 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 fix bugs. + +Eventually, a package could "graduate" to be included in the CMS. diff --git a/.github/ISSUE_TEMPLATE/1_Bug.md b/.github/ISSUE_TEMPLATE/1_Bug.md index 619452f700..d388af0d39 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug.md +++ b/.github/ISSUE_TEMPLATE/1_Bug.md @@ -7,15 +7,15 @@ A brief description of the issue goes here. +## Umbraco version + +I am seeing this issue on Umbraco version: Reproduction diff --git a/.github/README.md b/.github/README.md index bdf9ef9f67..d6d978c3d6 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. @@ -21,7 +21,7 @@ Please also see our [Code of Conduct](CODE_OF_CONDUCT.md). [Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet, with full support for all your custom .NET code and integrations. You're up and running in less than a minute, and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14-day trial, no credit card needed. -If you want to DIY, you can [download Umbraco]((https://our.umbraco.com/download)) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you. +If you want to DIY, then you can [download Umbraco]((https://our.umbraco.com/download)) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you. ## Documentation @@ -29,7 +29,7 @@ The documentation for Umbraco CMS can be found [on Our Umbraco](https://our.umbr ## Join the Umbraco community -Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources. +Our friendly community is available 24/7 at the community hub, we call ["Our Umbraco"](https://our.umbraco.com/). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources. Besides "Our", we all support each other also via Twitter: [Umbraco HQ](https://twitter.com/umbraco), [Release Updates](https://twitter.com/umbracoproject), [#umbraco](https://twitter.com/hashtag/umbraco) diff --git a/.github/REVIEW_PROCESS.md b/.github/REVIEW_PROCESS.md new file mode 100644 index 0000000000..917d25b090 --- /dev/null +++ b/.github/REVIEW_PROCESS.md @@ -0,0 +1,25 @@ +# Review process + +You're an awesome person and have sent us your contribution in the form of a pull request! It's now time to relax for a bit and wait for our response. + +In order to set some expectations, here's what happens next. + +## Review process + +You will get an initial reply within 48 hours (workdays) to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. + +You will get feedback within at most 14 days after opening the PR. You’ll most likely get feedback sooner though. Then there are a few possible outcomes: + +- Your proposed change is awesome! We merge it in and it will be included in the next minor release of Umbraco +- If the change is a high priority bug fix, we will cherry-pick it into the next patch release as well so that we can release it as soon as possible +- Your proposed change is awesome but needs a bit more work, we’ll give you feedback on the changes we’d like to see +- Your proposed change is awesome but.. not something we’re looking to include at this point. We’ll close your PR and the related issue (we’ll be nice about it!) + +## Are you still available? + +We understand you have other things to do and can't just drop everything to help us out. +So if we’re asking for your help to improve the PR we’ll wait for two weeks to give you a fair chance to make changes. We’ll ask for an update if we don’t hear back from you after that time. + +If we don’t hear back from you for 4 weeks, we’ll close the PR so that it doesn’t just hang around forever. You’re very welcome to re-open it once you have some more time to spend on it. + +There will be times that we really like your proposed changes and we’ll finish the final improvements we’d like to see ourselves. You still get the credits and your commits will live on in the git repository. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 585bd855b7..a0ff4d5b27 100644 --- a/.gitignore +++ b/.gitignore @@ -132,7 +132,8 @@ src/Umbraco.Web.UI.Client/bower_components/* preserve.belle #Ignore Rule for output of generated documentation files from Grunt docserve -src/Umbraco.Web.UI.Client/docs/api +src/Umbraco.Web.UI.Docs/api +src/Umbraco.Web.UI.Docs/package-lock.json src/*.boltdata/ src/umbraco.sln.ide/* src/.vs/ diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index 4aa354eba2..c8374bc2f7 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -25,10 +25,10 @@ not want this to happen as the alpha of the next major is, really, the next major already. --> - - + + - + 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/Readme.txt b/build/NuSpecs/tools/Readme.txt index e40b0dbc7e..b0583a2b4d 100644 --- a/build/NuSpecs/tools/Readme.txt +++ b/build/NuSpecs/tools/Readme.txt @@ -1,11 +1,11 @@ - 888 - 888 -888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. -888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b -888 888 888 888 888 888 888 888 .d888888 888 888 888 -Y88b 888 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P - "Y88888 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" + 888 + 888 +888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. +888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b +888 888 888 888 888 888 888 888 .d888888 888 888 888 +Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P + "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" ------------------------------------------------------------------ diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index df364c64ed..ba88f808c0 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -1,12 +1,13 @@ - _ _ __ __ ____ _____ _____ ____ - | | | | \/ | _ \| __ \ /\ / ____/ __ \ - | | | | \ / | |_) | |__) | / \ | | | | | | - | | | | |\/| | _ <| _ / / /\ \| | | | | | - | |__| | | | | |_) | | \ \ / ____ | |___| |__| | - \____/|_| |_|____/|_| \_/_/ \_\_____\____/ + 888 + 888 +888 888 88888b.d88b. 88888b. 888d888 8888b. .d8888b .d88b. +888 888 888 "888 "88b 888 "88b 888P" "88b d88P" d88""88b +888 888 888 888 888 888 888 888 .d888888 888 888 888 +Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P + "Y888P" 888 888 888 88888P" 888 "Y888888 "Y8888P "Y88P" ----------------------------------------------------- +------------------------------------------------------------------ Don't forget to build! @@ -25,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 14778e0f10..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 @@ - + @@ -128,6 +128,7 @@ + diff --git a/build/build-docs.ps1 b/build/build-docs.ps1 deleted file mode 100644 index 8cd3f090c7..0000000000 --- a/build/build-docs.ps1 +++ /dev/null @@ -1,44 +0,0 @@ -$uenv=build/build.ps1 -get - -$src = "$($uenv.SolutionRoot)\src" -$tmp = $uenv.BuildTemp -$out = $uenv.BuildOutput -$DocFxJson = "$src\ApiDocs\docfx.json" -$DocFxSiteOutput = "$tmp\_site\*.*" - -################ Do the UI docs -$uenv.CompileBelle() - -"Moving to Umbraco.Web.UI.Client folder" -cd .\src\Umbraco.Web.UI.Client - -"Generating the docs and waiting before executing the next commands" -& gulp docs | Out-Null - -# change baseUrl -$BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/" -$IndexPath = "./docs/api/index.html" -(Get-Content $IndexPath).replace('location.href.replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath - -# zip it -& $uenv.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Client\docs\api\*.*" - - -################ Do the c# docs - -# Build the solution in debug mode -$SolutionPath = Join-Path -Path $src -ChildPath "umbraco.sln" -#$uenv.CompileUmbraco() - -#restore nuget packages -$uenv.RestoreNuGet() - -# run DocFx -$DocFx = $uenv.BuildEnv.DocFx - -Write-Host "$DocFxJson" -& $DocFx metadata $DocFxJson -& $DocFx build $DocFxJson - -# zip it -& $uenv.BuildEnv.Zip a -tzip -r "$out\csharp-docs.zip" $DocFxSiteOutput diff --git a/build/build.ps1 b/build/build.ps1 index e4994d2c4a..892654d3cd 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -456,25 +456,24 @@ $ubuild.DefineMethod("PrepareAngularDocs", { Write-Host "Prepare Angular Documentation" - + $src = "$($this.SolutionRoot)\src" - $out = $this.BuildOutput - - $this.CompileBelle() - - "Moving to Umbraco.Web.UI.Client folder" - cd .\src\Umbraco.Web.UI.Client + $out = $this.BuildOutput + + "Moving to Umbraco.Web.UI.Docs folder" + cd ..\src\Umbraco.Web.UI.Docs "Generating the docs and waiting before executing the next commands" - & gulp docs | Out-Null + & npm install + & npx gulp docs # change baseUrl $BaseUrl = "https://our.umbraco.com/apidocs/v8/ui/" - $IndexPath = "./docs/api/index.html" + $IndexPath = "./api/index.html" (Get-Content $IndexPath).replace('location.href.replace(rUrl, indexFile)', "`'" + $BaseUrl + "`'") | Set-Content $IndexPath # zip it - & $this.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Client\docs\api\*.*" + & $this.BuildEnv.Zip a -tzip -r "$out\ui-docs.zip" "$src\Umbraco.Web.UI.Docs\api\*.*" }) $ubuild.DefineMethod("Build", diff --git a/src/ApiDocs/umbracotemplate/styles/main.css b/src/ApiDocs/umbracotemplate/styles/main.css index d74d51b150..802d42bc0a 100644 --- a/src/ApiDocs/umbracotemplate/styles/main.css +++ b/src/ApiDocs/umbracotemplate/styles/main.css @@ -1,65 +1,11 @@ body { - color: rgba(0,0,0,.8); -} -.navbar-inverse { - background: #a3db78; -} -.navbar-inverse .navbar-nav>li>a, .navbar-inverse .navbar-text { - color: rgba(0,0,0,.8); -} - -.navbar-inverse { - border-color: transparent; -} - -.sidetoc { - background-color: #f5fbf1; -} -body .toc { - background-color: #f5fbf1; -} -.sidefilter { - background-color: #daf0c9; -} -.subnav { - background-color: #f5fbf1; -} - -.navbar-inverse .navbar-nav>.active>a { - color: rgba(0,0,0,.8); - background-color: #daf0c9; -} - -.navbar-inverse .navbar-nav>.active>a:focus, .navbar-inverse .navbar-nav>.active>a:hover { - color: rgba(0,0,0,.8); - background-color: #daf0c9; -} - -.btn-primary { - color: rgba(0,0,0,.8); - background-color: #fff; - border-color: rgba(0,0,0,.8); -} -.btn-primary:hover { - background-color: #daf0c9; - color: rgba(0,0,0,.8); - border-color: rgba(0,0,0,.8); -} - -.toc .nav > li > a { - color: rgba(0,0,0,.8); -} - -button, a { - color: #f36f21; -} - -button:hover, -button:focus, -a:hover, -a:focus { - color: #143653; - text-decoration: none; + color: #34393e; + font-family: 'Roboto', sans-serif; + line-height: 1.5; + font-size: 16px; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + word-wrap: break-word } .navbar-header .navbar-brand { @@ -68,6 +14,202 @@ a:focus { width:50px; } -.toc .nav > li.active > a { - color: #f36f21; +#_content>a { + margin-top: 5px; } +/* HEADINGS */ + +h1 { + font-weight: 600; + font-size: 32px; +} + +h2 { + font-weight: 600; + font-size: 24px; + line-height: 1.8; +} + +h3 { + font-weight: 600; + font-size: 20px; + line-height: 1.8; +} + +h5 { + font-size: 14px; + padding: 10px 0px; +} + +article h1, +article h2, +article h3, +article h4 { + margin-top: 35px; + margin-bottom: 15px; +} + +article h4 { + padding-bottom: 8px; + border-bottom: 2px solid #ddd; +} + +/* NAVBAR */ + +.navbar-brand>img { + color: #fff; +} + +.navbar { + border: none; + /* Both navbars use box-shadow */ + -webkit-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + -moz-box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); + box-shadow: 0px 1px 3px 0px rgba(100, 100, 100, 0.5); +} + +.subnav { + border-top: 1px solid #ddd; + background-color: #fff; +} + +.navbar-inverse { + background-color: #303ea1; + z-index: 100; +} + +.navbar-inverse .navbar-nav>li>a, +.navbar-inverse .navbar-text { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid transparent; + padding-bottom: 12px; +} + +.navbar-inverse .navbar-nav>li>a:focus, +.navbar-inverse .navbar-nav>li>a:hover { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid white; +} + +.navbar-inverse .navbar-nav>.active>a, +.navbar-inverse .navbar-nav>.active>a:focus, +.navbar-inverse .navbar-nav>.active>a:hover { + color: #fff; + background-color: #303ea1; + border-bottom: 3px solid white; +} + +.navbar-form .form-control { + border: none; + border-radius: 20px; +} + +/* SIDEBAR */ + +.toc .level1>li { + font-weight: 400; +} + +.toc .nav>li>a { + color: #34393e; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.sidefilter { + background-color: #fff; + border-left: none; + border-right: none; +} + +.toc-filter { + padding: 10px; + margin: 0; +} + +.toc-filter>input { + border: 2px solid #ddd; + border-radius: 20px; +} + +.toc-filter>.filter-icon { + display: none; +} + +.sidetoc>.toc { + background-color: #fff; + overflow-x: hidden; +} + +.sidetoc { + background-color: #fff; + border: none; +} + +/* ALERTS */ + +.alert { + padding: 0px 0px 5px 0px; + color: inherit; + background-color: inherit; + border: none; + box-shadow: 0px 2px 2px 0px rgba(100, 100, 100, 0.4); +} + +.alert>p { + margin-bottom: 0; + padding: 5px 10px; +} + +.alert>ul { + margin-bottom: 0; + padding: 5px 40px; +} + +.alert>h5 { + padding: 10px 15px; + margin-top: 0; + text-transform: uppercase; + font-weight: bold; + border-radius: 4px 4px 0 0; +} + +.alert-info>h5 { + color: #1976d2; + border-bottom: 4px solid #1976d2; + background-color: #e3f2fd; +} + +.alert-warning>h5 { + color: #f57f17; + border-bottom: 4px solid #f57f17; + background-color: #fff3e0; +} + +.alert-danger>h5 { + color: #d32f2f; + border-bottom: 4px solid #d32f2f; + background-color: #ffebee; +} + +/* CODE HIGHLIGHT */ +pre { + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + word-break: break-all; + word-wrap: break-word; + background-color: #fffaef; + border-radius: 4px; + box-shadow: 0px 1px 4px 1px rgba(100, 100, 100, 0.4); +} + +.sideaffix { + overflow: visible; +} \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index a4e859988e..bf3a271d32 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.2.0")] -[assembly: AssemblyInformationalVersion("8.2.0")] +[assembly: AssemblyFileVersion("8.3.0")] +[assembly: AssemblyInformationalVersion("8.3.0")] diff --git a/src/Umbraco.Core/AsyncLock.cs b/src/Umbraco.Core/AsyncLock.cs index 158b132f26..6dd866705e 100644 --- a/src/Umbraco.Core/AsyncLock.cs +++ b/src/Umbraco.Core/AsyncLock.cs @@ -67,31 +67,34 @@ namespace Umbraco.Core : new NamedSemaphoreReleaser(_semaphore2); } - public Task LockAsync() - { - var wait = _semaphore != null - ? _semaphore.WaitAsync() - : _semaphore2.WaitOneAsync(); + //NOTE: We don't use the "Async" part of this lock at all + //TODO: Remove this and rename this class something like SystemWideLock, then we can re-instate this logic if we ever need an Async lock again - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + //public Task LockAsync() + //{ + // var wait = _semaphore != null + // ? _semaphore.WaitAsync() + // : _semaphore2.WaitOneAsync(); - public Task LockAsync(int millisecondsTimeout) - { - var wait = _semaphore != null - ? _semaphore.WaitAsync(millisecondsTimeout) - : _semaphore2.WaitOneAsync(millisecondsTimeout); + // return wait.IsCompleted + // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named + // : wait.ContinueWith((_, state) => (((AsyncLock) state).CreateReleaser()), + // this, CancellationToken.None, + // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + //} - return wait.IsCompleted - ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named - : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), - this, CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); - } + //public Task LockAsync(int millisecondsTimeout) + //{ + // var wait = _semaphore != null + // ? _semaphore.WaitAsync(millisecondsTimeout) + // : _semaphore2.WaitOneAsync(millisecondsTimeout); + + // return wait.IsCompleted + // ? _releaserTask ?? Task.FromResult(CreateReleaser()) // anonymous vs named + // : wait.ContinueWith((_, state) => (((AsyncLock)state).CreateReleaser()), + // this, CancellationToken.None, + // TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + //} public IDisposable Lock() { diff --git a/src/Umbraco.Core/Cache/CacheKeys.cs b/src/Umbraco.Core/Cache/CacheKeys.cs index e8f93d636a..b8ee0e97c4 100644 --- a/src/Umbraco.Core/Cache/CacheKeys.cs +++ b/src/Umbraco.Core/Cache/CacheKeys.cs @@ -11,5 +11,6 @@ public const string TemplateFrontEndCacheKey = "template"; public const string MacroContentCacheKey = "macroContent_"; // used in MacroRenderers + public const string MacroFromAliasCacheKey = "macroFromAlias_"; } } diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs b/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs index 078a505be9..8518d907b5 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/FileSystems.cs @@ -90,7 +90,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions // register the IFileSystem supporting the IMediaFileSystem // THIS IS THE ONLY THING THAT NEEDS TO CHANGE, IN ORDER TO REPLACE THE UNDERLYING FILESYSTEM // and, SupportingFileSystem.For() returns the underlying filesystem - composition.SetMediaFileSystem(() => new PhysicalFileSystem("~/media")); + composition.SetMediaFileSystem(() => new PhysicalFileSystem(SystemDirectories.Media)); return composition; } diff --git a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs index 0baefe104b..d252c58730 100644 --- a/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs +++ b/src/Umbraco.Core/Composing/CompositionExtensions/Services.cs @@ -96,7 +96,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions var pluginLangFolders = appPlugins.Exists == false ? Enumerable.Empty() : appPlugins.GetDirectories() - .SelectMany(x => x.GetDirectories("Lang")) + .SelectMany(x => x.GetDirectories("Lang", SearchOption.AllDirectories)) .SelectMany(x => x.GetFiles("*.xml", SearchOption.TopDirectoryOnly)) .Select(x => new LocalizedTextServiceSupplementaryFileSource(x, false)); diff --git a/src/Umbraco.Core/Composing/Lifetime.cs b/src/Umbraco.Core/Composing/Lifetime.cs index 1a2cc3119a..012555be5e 100644 --- a/src/Umbraco.Core/Composing/Lifetime.cs +++ b/src/Umbraco.Core/Composing/Lifetime.cs @@ -12,31 +12,62 @@ /// or MS.DI, PerDependency in Autofac. Transient, + // TODO: We need to fix this up, currently LightInject is the only one that behaves differently from all other containers. + // ... the simple fix would be to map this to PerScopeLifetime in LI but need to wait on a response here https://github.com/seesharper/LightInject/issues/494#issuecomment-518942625 + // + // we use it for controllers, httpContextBase and other request scoped objects: MembershpHelper, TagQuery, UmbracoTreeSearcher and ISearchableTree + // - so that they are automatically disposed at the end of the scope (ie request) + // - not sure they should not be simply 'scoped'? + /// /// One unique instance per request. /// - // TODO: review lifetimes for LightInject vs other containers - // currently, corresponds to 'Request' in LightInject which is 'Transient + disposed by Scope' - // but NOT (in LightInject) a per-web-request lifetime, more a TransientScoped - // - // we use it for controllers, httpContextBase and umbracoContext - // - so that they are automatically disposed at the end of the scope (ie request) - // - not sure they should not be simply 'scoped'? - // - // Castle has an extra PerWebRequest something, and others use scope - // what about Request before first request ie during application startup? - // see http://blog.ploeh.dk/2009/11/17/UsingCastleWindsor'sPerWebRequestlifestylewithASP.NETMVConIIS7/ - // Castle ends up requiring a special scope manager too - // see https://groups.google.com/forum/#!topic/castle-project-users/1E2W9LVIYR4 - // - // but maybe also - why are we requiring scoped services at startup? + /// + /// + /// Any instance created with this lifetime will be disposed at the end of a request. + /// + /// Corresponds to + /// + /// PerRequestLifeTime in LightInject - means transient but disposed at the end of the current web request. + /// see: https://github.com/seesharper/LightInject/issues/494#issuecomment-518493262 + /// + /// + /// Scoped in MS.DI - means one per web request. + /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes + /// + /// InstancePerRequest in Autofac - means one per web request. + /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request + /// But "Behind the scenes, though, it’s still just instance per matching lifetime scope." + /// + /// + /// LifestylePerWebRequest in Castle Windsor - means one per web request. + /// see https://github.com/castleproject/Windsor/blob/master/docs/mvc-tutorial-part-7-lifestyles.md#the-perwebrequest-lifestyle + /// + /// Request, /// - /// One unique instance per container scope. + /// One unique instance per scope. /// - /// Corresponds to Scope in LightInject, Scoped in MS.DI - /// or Castle Windsor, PerLifetimeScope in Autofac. + /// + /// + /// Any instance created with this lifetime will be disposed at the end of the current scope. + /// + /// Corresponds to + /// PerScopeLifetime in LightInject (when in a request, means one per web request) + /// + /// Scoped in MS.DI (when in a request, means one per web request) + /// see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes + /// + /// InstancePerLifetimeScope in Autofac (when in a request, means one per web request) + /// see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-lifetime-scope + /// Also note that Autofac's InstancePerRequest is the same as this, see https://autofaccn.readthedocs.io/en/latest/lifetime/instance-scope.html#instance-per-request + /// it says "Behind the scenes, though, it’s still just instance per matching lifetime scope." + /// + /// + /// LifestyleScoped in Castle Windsor + /// + /// Scope, /// diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 49f4481a59..a888e3c42b 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -33,7 +33,6 @@ namespace Umbraco.Core.Configuration /// private static void ResetInternal() { - GlobalSettingsExtensions.Reset(); _reservedPaths = null; _reservedUrls = null; HasSmtpServer = null; diff --git a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs index 6bfb7ea904..bc76caacee 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Web; using System.Web.Routing; using Umbraco.Core.IO; @@ -9,22 +11,9 @@ namespace Umbraco.Core.Configuration { public static class GlobalSettingsExtensions { - /// - /// Used in unit testing to reset all config items, this is automatically called by GlobalSettings.Reset() - /// - internal static void Reset() - { - _reservedUrlsCache = null; - _mvcArea = null; - } - - private static readonly object Locker = new object(); - //make this volatile so that we can ensure thread safety with a double check lock - private static volatile string _reservedUrlsCache; - private static string _reservedPathsCache; - private static HashSet _reservedList = new HashSet(); private static string _mvcArea; + /// /// This returns the string of the MVC Area route. /// @@ -40,6 +29,13 @@ namespace Umbraco.Core.Configuration { if (_mvcArea != null) return _mvcArea; + _mvcArea = GetUmbracoMvcAreaNoCache(globalSettings); + + return _mvcArea; + } + + internal static string GetUmbracoMvcAreaNoCache(this IGlobalSettings globalSettings) + { if (globalSettings.Path.IsNullOrWhiteSpace()) { throw new InvalidOperationException("Cannot create an MVC Area path without the umbracoPath specified"); @@ -48,95 +44,8 @@ namespace Umbraco.Core.Configuration var path = globalSettings.Path; if (path.StartsWith(SystemDirectories.Root)) // beware of TrimStart, see U4-2518 path = path.Substring(SystemDirectories.Root.Length); - _mvcArea = path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower(); - return _mvcArea; + return path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower(); } - /// - /// Determines whether the specified URL is reserved or is inside a reserved path. - /// - /// - /// The URL to check. - /// - /// true if the specified URL is reserved; otherwise, false. - /// - internal static bool IsReservedPathOrUrl(this IGlobalSettings globalSettings, string url) - { - if (_reservedUrlsCache == null) - { - lock (Locker) - { - if (_reservedUrlsCache == null) - { - // store references to strings to determine changes - _reservedPathsCache = globalSettings.ReservedPaths; - _reservedUrlsCache = globalSettings.ReservedUrls; - - // add URLs and paths to a new list - var newReservedList = new HashSet(); - foreach (var reservedUrlTrimmed in _reservedUrlsCache - .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim().ToLowerInvariant()) - .Where(x => x.IsNullOrWhiteSpace() == false) - .Select(reservedUrl => IOHelper.ResolveUrl(reservedUrl).Trim().EnsureStartsWith("/")) - .Where(reservedUrlTrimmed => reservedUrlTrimmed.IsNullOrWhiteSpace() == false)) - { - newReservedList.Add(reservedUrlTrimmed); - } - - foreach (var reservedPathTrimmed in _reservedPathsCache - .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) - .Select(x => x.Trim().ToLowerInvariant()) - .Where(x => x.IsNullOrWhiteSpace() == false) - .Select(reservedPath => IOHelper.ResolveUrl(reservedPath).Trim().EnsureStartsWith("/").EnsureEndsWith("/")) - .Where(reservedPathTrimmed => reservedPathTrimmed.IsNullOrWhiteSpace() == false)) - { - newReservedList.Add(reservedPathTrimmed); - } - - // use the new list from now on - _reservedList = newReservedList; - } - } - } - - //The url should be cleaned up before checking: - // * If it doesn't contain an '.' in the path then we assume it is a path based URL, if that is the case we should add an trailing '/' because all of our reservedPaths use a trailing '/' - // * We shouldn't be comparing the query at all - var pathPart = url.Split(new[] {'?'}, StringSplitOptions.RemoveEmptyEntries)[0].ToLowerInvariant(); - if (pathPart.Contains(".") == false) - { - pathPart = pathPart.EnsureEndsWith('/'); - } - - // return true if url starts with an element of the reserved list - return _reservedList.Any(x => pathPart.InvariantStartsWith(x)); - } - - /// - /// Determines whether the current request is reserved based on the route table and - /// whether the specified URL is reserved or is inside a reserved path. - /// - /// - /// - /// - /// The route collection to lookup the request in - /// - internal static bool IsReservedPathOrUrl(this IGlobalSettings globalSettings, string url, HttpContextBase httpContext, RouteCollection routes) - { - if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); - if (routes == null) throw new ArgumentNullException(nameof(routes)); - - //check if the current request matches a route, if so then it is reserved. - //TODO: This value should be cached! Else this is doing double routing in MVC every request! - var route = routes.GetRouteData(httpContext); - if (route != null) - return true; - - //continue with the standard ignore routine - return globalSettings.IsReservedPathOrUrl(url); - } - - } } 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 0c2e246721..eb2b3525a7 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -14,6 +14,23 @@ namespace Umbraco.Core /// public const string InternalGenericPropertiesPrefix = "_umb_"; + public static class Legacy + { + public static class Aliases + { + public const string Textbox = "Umbraco.Textbox"; + public const string Date = "Umbraco.Date"; + public const string ContentPicker2 = "Umbraco.ContentPicker2"; + public const string MediaPicker2 = "Umbraco.MediaPicker2"; + public const string MemberPicker2 = "Umbraco.MemberPicker2"; + public const string MultiNodeTreePicker2 = "Umbraco.MultiNodeTreePicker2"; + public const string TextboxMultiple = "Umbraco.TextboxMultiple"; + public const string RelatedLinks2 = "Umbraco.RelatedLinks2"; + public const string RelatedLinks = "Umbraco.RelatedLinks"; + + } + } + /// /// Defines Umbraco built-in property editor aliases. /// @@ -68,17 +85,17 @@ namespace Umbraco.Core /// ListView. /// public const string ListView = "Umbraco.ListView"; - - /// - /// Macro Container. - /// - public const string MacroContainer = "Umbraco.MacroContainer"; - + /// /// Media Picker. /// public const string MediaPicker = "Umbraco.MediaPicker"; + /// + /// Multiple Media Picker. + /// + public const string MultipleMediaPicker = "Umbraco.MultipleMediaPicker"; + /// /// Member Picker. /// @@ -186,6 +203,24 @@ namespace Umbraco.Core /// Must be a valid value. public const string DataValueType = "umbracoDataValueType"; } + + /// + /// Defines Umbraco's built-in property editor groups. + /// + public static class Groups + { + public const string Common = "Common"; + + public const string Lists = "Lists"; + + public const string Media = "Media"; + + public const string People = "People"; + + public const string Pickers = "Pickers"; + + public const string RichContent = "Rich Content"; + } } } } diff --git a/src/Umbraco.Core/ContentVariationExtensions.cs b/src/Umbraco.Core/ContentVariationExtensions.cs index 9fdc5f0b90..5b157307ab 100644 --- a/src/Umbraco.Core/ContentVariationExtensions.cs +++ b/src/Umbraco.Core/ContentVariationExtensions.cs @@ -150,39 +150,46 @@ namespace Umbraco.Core culture = culture.NullOrWhiteSpaceAsNull(); segment = segment.NullOrWhiteSpaceAsNull(); - bool Validate(bool variesBy, string value) - { - if (variesBy) - { - // varies by - // in exact mode, the value cannot be null (but it can be a wildcard) - // in !wildcards mode, the value cannot be a wildcard (but it can be null) - if ((exact && value == null) || (!wildcards && value == "*")) - return false; - } - else - { - // does not vary by value - // the value cannot have a value - // unless wildcards and it's "*" - if (value != null && (!wildcards || value != "*")) - return false; - } - - return true; - } - - if (!Validate(variation.VariesByCulture(), culture)) + // if wildcards are disabled, do not allow "*" + if (!wildcards && (culture == "*" || segment == "*")) { if (throwIfInvalid) - throw new NotSupportedException($"Culture value \"{culture ?? ""}\" is invalid."); + throw new NotSupportedException($"Variation wildcards are not supported."); return false; } - if (!Validate(variation.VariesBySegment(), segment)) + if (variation.VariesByCulture()) + { + // varies by culture + // in exact mode, the culture cannot be null + if (exact && culture == null) + { + if (throwIfInvalid) + throw new NotSupportedException($"Culture may not be null because culture variation is enabled."); + return false; + } + } + else + { + // does not vary by culture + // the culture cannot have a value + // unless wildcards and it's "*" + if (culture != null && !(wildcards && culture == "*")) + { + if (throwIfInvalid) + throw new NotSupportedException($"Culture \"{culture}\" is invalid because culture variation is disabled."); + return false; + } + } + + // if it does not vary by segment + // the segment cannot have a value + // segment may always be null, even when the ContentVariation.Segment flag is set for this variation, + // therefore the exact parameter is not used in segment validation. + if (!variation.VariesBySegment() && segment != null && !(wildcards && segment == "*")) { if (throwIfInvalid) - throw new NotSupportedException($"Segment value \"{segment ?? ""}\" is invalid."); + throw new NotSupportedException($"Segment \"{segment}\" is invalid because segment variation is disabled."); return false; } diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs index 66b788f9c8..f665aaa8ed 100644 --- a/src/Umbraco.Core/DateTimeExtensions.cs +++ b/src/Umbraco.Core/DateTimeExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; @@ -15,7 +16,7 @@ namespace Umbraco.Core /// public static string ToIsoString(this DateTime dt) { - return dt.ToString("yyyy-MM-dd HH:mm:ss"); + return dt.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); } public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo) diff --git a/src/Umbraco.Core/Exceptions/PanicException.cs b/src/Umbraco.Core/Exceptions/PanicException.cs new file mode 100644 index 0000000000..c3564d2834 --- /dev/null +++ b/src/Umbraco.Core/Exceptions/PanicException.cs @@ -0,0 +1,45 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Exceptions +{ + /// + /// Represents an internal exception that in theory should never been thrown, it is only thrown in circumstances that should never happen. + /// + /// + [Serializable] + internal class PanicException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public PanicException() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public PanicException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference ( in Visual Basic) if no inner exception is specified. + public PanicException(string message, Exception innerException) + : base(message, innerException) + { } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + protected PanicException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } + } +} diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs index 2f33d82bdc..d6fb63b0a1 100644 --- a/src/Umbraco.Core/IO/SystemDirectories.cs +++ b/src/Umbraco.Core/IO/SystemDirectories.cs @@ -15,6 +15,8 @@ namespace Umbraco.Core.IO public static string TempFileUploads => TempData + "/FileUploads"; + public static string TempImageUploads => TempFileUploads + "/rte"; + public static string Install => "~/install"; public static string AppCode => "~/App_Code"; diff --git a/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs b/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs index b39a3f38df..dbdd7842ba 100644 --- a/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs +++ b/src/Umbraco.Core/Logging/Viewer/ILogViewer.cs @@ -42,6 +42,12 @@ namespace Umbraco.Core.Logging.Viewer bool CheckCanOpenLogs(LogTimePeriod logTimePeriod); + /// + /// Gets the current Serilog minimum log level + /// + /// + string GetLogLevel(); + /// /// Returns the collection of logs /// diff --git a/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs b/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs index acb2f5dbf9..607c20e601 100644 --- a/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs +++ b/src/Umbraco.Core/Logging/Viewer/LogViewerSourceBase.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Xml; using Newtonsoft.Json; +using Serilog; using Serilog.Events; using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Formatting = Newtonsoft.Json.Formatting; namespace Umbraco.Core.Logging.Viewer { @@ -89,6 +92,16 @@ namespace Umbraco.Core.Logging.Viewer return errorCounter.Count; } + /// + /// Get the Serilog minimum-level value from the config file. + /// + /// + public string GetLogLevel() + { + var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast().Where(Log.Logger.IsEnabled)?.Min() ?? null; + return logLevel?.ToString() ?? ""; + } + public LogLevelCounts GetLogLevelCounts(LogTimePeriod logTimePeriod) { var counter = new CountingFilter(); diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index ca875c2167..5da1062275 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Cryptography; using System.Threading; using System.Web.Hosting; using Umbraco.Core.Logging; @@ -64,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); @@ -113,7 +114,7 @@ namespace Umbraco.Core lock (_locko) { - _logger.Debug("Signaled {Signaled} ({SignalSource})", _signaled ? "(again)" : string.Empty, source); + _logger.Debug("Signaled ({Signaled}) ({SignalSource})", _signaled ? "again" : "first", source); if (_signaled) return; if (_isMainDom == false) return; // probably not needed _signaled = true; @@ -171,6 +172,7 @@ namespace Umbraco.Core // if more than 1 instance reach that point, one will get the lock // and the other one will timeout, which is accepted + //TODO: This can throw a TimeoutException - in which case should this be in a try/finally to ensure the signal is always reset? _asyncLocker = _asyncLock.Lock(LockTimeoutMilliseconds); _isMainDom = true; @@ -181,6 +183,9 @@ namespace Umbraco.Core // which is accepted _signal.Reset(); + + //WaitOneAsync (ext method) will wait for a signal without blocking the main thread, the waiting is done on a background thread + _signal.WaitOneAsync() .ContinueWith(_ => OnSignal("signal")); diff --git a/src/Umbraco.Core/Mapping/UmbracoMapper.cs b/src/Umbraco.Core/Mapping/UmbracoMapper.cs index 8915ebcf74..e62825101c 100644 --- a/src/Umbraco.Core/Mapping/UmbracoMapper.cs +++ b/src/Umbraco.Core/Mapping/UmbracoMapper.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Mapping { @@ -231,10 +232,10 @@ namespace Umbraco.Core.Mapping if (ctor != null && map != null) { // register (for next time) and do it now (for this time) - object NCtor(object s, MapperContext c) => MapEnumerableInternal((IEnumerable) s, targetGenericArg, ctor, map, c); + object NCtor(object s, MapperContext c) => MapEnumerableInternal((IEnumerable)s, targetGenericArg, ctor, map, c); DefineCtors(sourceType)[targetType] = NCtor; DefineMaps(sourceType)[targetType] = Identity; - return (TTarget) NCtor(source, context); + return (TTarget)NCtor(source, context); } throw new InvalidOperationException($"Don't know how to map {sourceGenericArg.FullName} to {targetGenericArg.FullName}, so don't know how to map {sourceType.FullName} to {targetType.FullName}."); @@ -259,13 +260,13 @@ namespace Umbraco.Core.Mapping if (typeof(TTarget).IsArray) { var elementType = typeof(TTarget).GetElementType(); - if (elementType == null) throw new Exception("panic"); + if (elementType == null) throw new PanicException("elementType == null which should never occur"); var targetArray = Array.CreateInstance(elementType, targetList.Count); targetList.CopyTo(targetArray, 0); target = targetArray; } - return (TTarget) target; + return (TTarget)target; } /// @@ -342,7 +343,21 @@ namespace Umbraco.Core.Mapping if (ctor == null) return null; - _ctors[sourceType] = sourceCtor; + _ctors.AddOrUpdate(sourceType, sourceCtor, (k, v) => + { + // Add missing constructors + foreach (var c in sourceCtor) + { + if (!v.ContainsKey(c.Key)) + { + v.Add(c.Key, c.Value); + } + } + + return v; + }); + + return ctor; } @@ -367,7 +382,17 @@ namespace Umbraco.Core.Mapping if (map == null) return null; - _maps[sourceType] = sourceMap; + if (_maps.ContainsKey(sourceType)) + { + foreach (var m in sourceMap) + { + if (!_maps[sourceType].TryGetValue(m.Key, out _)) + _maps[sourceType].Add(m.Key, m.Value); + } + } + else + _maps[sourceType] = sourceMap; + return map; } @@ -382,7 +407,7 @@ namespace Umbraco.Core.Mapping { if (type.IsArray) return type.GetElementType(); if (type.IsGenericType) return type.GenericTypeArguments[0]; - throw new Exception("panic"); + throw new PanicException($"Could not get enumerable or array type from {type}"); } /// diff --git a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs index 9b13457b76..df74bf7c87 100644 --- a/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs +++ b/src/Umbraco.Core/Migrations/Expressions/Delete/KeysAndIndexes/DeleteKeysAndIndexesBuilder.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; using NPoco; +using Umbraco.Core; using Umbraco.Core.Migrations.Expressions.Common; using Umbraco.Core.Persistence.SqlSyntax; @@ -27,31 +29,57 @@ namespace Umbraco.Core.Migrations.Expressions.Delete.KeysAndIndexes { _context.BuildingExpression = false; + //get a list of all constraints - this will include all PK, FK and unique constraints + var tableConstraints = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); + + //get a list of defined indexes - this will include all indexes, unique indexes and unique constraint indexes + var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); + + var uniqueConstraintNames = tableConstraints.Where(x => !x.Item2.InvariantStartsWith("PK_") && !x.Item2.InvariantStartsWith("FK_")).Select(x => x.Item2); + var indexNames = indexes.Select(x => x.IndexName).ToList(); + // drop keys if (DeleteLocal || DeleteForeign) { // table, constraint - var tableKeys = _context.SqlContext.SqlSyntax.GetConstraintsPerTable(_context.Database).DistinctBy(x => x.Item2).ToList(); + if (DeleteForeign) { - foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("FK_"))) + //In some cases not all FK's are prefixed with "FK" :/ mostly with old upgraded databases so we need to check if it's either: + // * starts with FK OR + // * doesn't start with PK_ and doesn't exist in the list of indexes + + foreach (var key in tableConstraints.Where(x => x.Item1 == TableName + && (x.Item2.InvariantStartsWith("FK_") || (!x.Item2.InvariantStartsWith("PK_") && !indexNames.InvariantContains(x.Item2))))) + { Delete.ForeignKey(key.Item2).OnTable(key.Item1).Do(); + } + } if (DeleteLocal) { - foreach (var key in tableKeys.Where(x => x.Item1 == TableName && x.Item2.StartsWith("PK_"))) + foreach (var key in tableConstraints.Where(x => x.Item1 == TableName && x.Item2.InvariantStartsWith("PK_"))) Delete.PrimaryKey(key.Item2).FromTable(key.Item1).Do(); - // note: we do *not* delete the DEFAULT constraints + // note: we do *not* delete the DEFAULT constraints and if we wanted to we'd have to deal with that in interesting ways + // since SQL server has a specific way to handle that, see SqlServerSyntaxProvider.GetDefaultConstraintsPerColumn } } // drop indexes if (DeleteLocal) - { - var indexes = _context.SqlContext.SqlSyntax.GetDefinedIndexesDefinitions(_context.Database).DistinctBy(x => x.IndexName).ToList(); + { foreach (var index in indexes.Where(x => x.TableName == TableName)) - Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + { + //if this is a unique constraint we need to drop the constraint, else drop the index + //to figure this out, the index must be tagged as unique and it must exist in the tableConstraints + + if (index.IsUnique && uniqueConstraintNames.InvariantContains(index.IndexName)) + Delete.UniqueConstraint(index.IndexName).FromTable(index.TableName).Do(); + else + Delete.Index(index.IndexName).OnTable(index.TableName).Do(); + } + } } diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs index dd5c17713a..94d8cfbc62 100644 --- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs @@ -190,6 +190,7 @@ namespace Umbraco.Core.Migrations.Install _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Settings }); _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Users }); _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Forms }); + _database.Insert(new UserGroup2AppDto { UserGroupId = 1, AppAlias = Constants.Applications.Translation }); _database.Insert(new UserGroup2AppDto { UserGroupId = 2, AppAlias = Constants.Applications.Content }); @@ -226,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/AddTypedLabels.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs index 2f20f01728..309f8acbc3 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/AddTypedLabels.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using System.Linq; using NPoco; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -71,10 +72,21 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 // flip known property types - var intPropertyTypes = new[] { 7, 8, 29 }; - var bigintPropertyTypes = new[] { 9, 26 }; - var dtPropertyTypes = new[] { 32, 33, 34 }; + var labelPropertyTypes = Database.Fetch(Sql() + .Select(x => x.Id, x => x.Alias) + .From() + .Where(x => x.DataTypeId == Constants.DataTypes.LabelString) + ); + var intPropertyAliases = new[] { Constants.Conventions.Media.Width, Constants.Conventions.Media.Height, Constants.Conventions.Member.FailedPasswordAttempts }; + var bigintPropertyAliases = new[] { Constants.Conventions.Media.Bytes }; + var dtPropertyAliases = new[] { Constants.Conventions.Member.LastLockoutDate, Constants.Conventions.Member.LastLoginDate, Constants.Conventions.Member.LastPasswordChangeDate }; + + var intPropertyTypes = labelPropertyTypes.Where(pt => intPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); + var bigintPropertyTypes = labelPropertyTypes.Where(pt => bigintPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); + var dtPropertyTypes = labelPropertyTypes.Where(pt => dtPropertyAliases.Contains(pt.Alias)).Select(pt => pt.Id).ToArray(); + + Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelInt)).WhereIn(x => x.Id, intPropertyTypes)); Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelBigint)).WhereIn(x => x.Id, bigintPropertyTypes)); Database.Execute(Sql().Update(u => u.Set(x => x.DataTypeId, Constants.DataTypes.LabelDateTime)).WhereIn(x => x.Id, dtPropertyTypes)); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs index 44f7affe8e..1956876402 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/ConvertRelatedLinksToMultiUrlPicker.cs @@ -19,8 +19,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 var sqlDataTypes = Sql() .Select() .From() - .Where(x => x.EditorAlias == "Umbraco.RelatedLinks" - || x.EditorAlias == "Umbraco.RelatedLinks2"); + .Where(x => x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks + || x.EditorAlias == Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2); var dataTypes = Database.Fetch(sqlDataTypes); var dataTypeIds = dataTypes.Select(x => x.NodeId).ToList(); @@ -72,6 +72,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { var sqlNodeData = Sql() .Select() + .From() .Where(x => x.NodeId == intId); var node = Database.Fetch(sqlNodeData).FirstOrDefault(); 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 438b02385b..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 @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Umbraco.Core.Composing; @@ -18,6 +19,18 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 private readonly PropertyEditorCollection _propertyEditors; private readonly ILogger _logger; + private static readonly ISet LegacyAliases = new HashSet() + { + Constants.PropertyEditors.Legacy.Aliases.Date, + Constants.PropertyEditors.Legacy.Aliases.Textbox, + Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, + Constants.PropertyEditors.Legacy.Aliases.RelatedLinks2, + Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, + Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, + }; + public DataTypeMigration(IMigrationContext context, PreValueMigratorCollection preValueMigrators, PropertyEditorCollection propertyEditors, ILogger logger) : base(context) { @@ -61,25 +74,41 @@ 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 var newAlias = migrator.GetNewAlias(dataType.EditorAlias); if (newAlias == null) { - _logger.Warn("Skipping validation of configuration for data type {NodeId} : {EditorAlias}." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, dataType.EditorAlias); + if (!LegacyAliases.Contains(dataType.EditorAlias)) + { + _logger.Warn( + "Skipping validation of configuration for data type {NodeId} : {EditorAlias}." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, dataType.EditorAlias); + } } else if (!_propertyEditors.TryGet(newAlias, out var propertyEditor)) { - _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" - + " because no property editor with that alias was found." - + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", - dataType.NodeId, newAlias, dataType.EditorAlias); + if (!LegacyAliases.Contains(newAlias)) + { + _logger.Warn("Skipping validation of configuration for data type {NodeId} : {NewEditorAlias} (was: {EditorAlias})" + + " because no property editor with that alias was found." + + " Please ensure that the configuration is valid. The site may fail to start and / or load data types and run.", + dataType.NodeId, newAlias, dataType.EditorAlias); + } } else { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs index 2e341ad091..f445742aa9 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ContentPickerPreValueMigrator.cs @@ -3,7 +3,7 @@ class ContentPickerPreValueMigrator : DefaultPreValueMigrator { public override bool CanMigrate(string editorAlias) - => editorAlias == "Umbraco.ContentPicker2"; + => editorAlias == Constants.PropertyEditors.Legacy.Aliases.ContentPicker2; public override string GetNewAlias(string editorAlias) => null; 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/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs new file mode 100644 index 0000000000..87a1fd6504 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MarkdownEditorPreValueMigrator.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes +{ + class MarkdownEditorPreValueMigrator : DefaultPreValueMigrator //PreValueMigratorBase + { + public override bool CanMigrate(string editorAlias) + => editorAlias == Constants.PropertyEditors.Aliases.MarkdownEditor; + + protected override object GetPreValueValue(PreValueDto preValue) + { + if (preValue.Alias == "preview") + return preValue.Value == "1"; + + return base.GetPreValueValue(preValue); + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs index a46b1eefb7..922d886595 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/MediaPickerPreValueMigrator.cs @@ -6,15 +6,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes { private readonly string[] _editors = { - "Umbraco.MediaPicker2", - "Umbraco.MediaPicker" + Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, + Constants.PropertyEditors.Aliases.MediaPicker }; public override bool CanMigrate(string editorAlias) => _editors.Contains(editorAlias); public override string GetNewAlias(string editorAlias) - => "Umbraco.MediaPicker"; + => Constants.PropertyEditors.Aliases.MediaPicker; // you wish - but MediaPickerConfiguration lives in Umbraco.Web /* diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs index 8dfa464508..db9021d653 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/PreValueMigratorComposer.cs @@ -20,7 +20,8 @@ public class PreValueMigratorComposer : ICoreComposer .Append() .Append() .Append() - .Append(); + .Append() + .Append(); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs index c04e7c8fda..89a71fdaf4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/RenamingPreValueMigrator.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes { @@ -20,7 +21,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes case "Umbraco.NoEdit": return Constants.PropertyEditors.Aliases.Label; default: - throw new Exception("panic"); + throw new PanicException($"The alias {editorAlias} is not supported"); } } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs index 7249ebd6ec..07fefc8e85 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypes/ValueListPreValueMigrator.cs @@ -9,6 +9,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0.DataTypes private readonly string[] _editors = { "Umbraco.RadioButtonList", + "Umbraco.CheckBoxList", "Umbraco.DropDown", "Umbraco.DropdownlistPublishingKeys", "Umbraco.DropDownMultiple", diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs index a434b9f1c1..0d451e8460 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/MergeDateAndDateTimePropertyEditor.cs @@ -16,7 +16,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - var dataTypes = GetDataTypes("Umbraco.Date"); + var dataTypes = GetDataTypes(Constants.PropertyEditors.Legacy.Aliases.Date); foreach (var dataType in dataTypes) { @@ -25,6 +25,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { config = (DateTimeConfiguration) new CustomDateTimeConfigurationEditor().FromDatabase( dataType.Configuration); + + // If the Umbraco.Date type is the default from V7 and it has never been updated, then the + // configuration is empty, and the format stuff is handled by in JS by moment.js. - We can't do that + // after the migration, so we force the format to the default from V7. + if (string.IsNullOrEmpty(dataType.Configuration)) + { + config.Format = "YYYY-MM-DD"; + }; } catch (Exception ex) { diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs index dac62abb75..89a8f010ec 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs @@ -12,12 +12,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 public override void Migrate() { - RenameDataType(Constants.PropertyEditors.Aliases.ContentPicker + "2", Constants.PropertyEditors.Aliases.ContentPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MediaPicker + "2", Constants.PropertyEditors.Aliases.MediaPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MemberPicker + "2", Constants.PropertyEditors.Aliases.MemberPicker); - RenameDataType(Constants.PropertyEditors.Aliases.MultiNodeTreePicker + "2", Constants.PropertyEditors.Aliases.MultiNodeTreePicker); - RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea, false); - RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.ContentPicker2, Constants.PropertyEditors.Aliases.ContentPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MediaPicker2, Constants.PropertyEditors.Aliases.MediaPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MemberPicker2, Constants.PropertyEditors.Aliases.MemberPicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.MultiNodeTreePicker2, Constants.PropertyEditors.Aliases.MultiNodeTreePicker); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.TextboxMultiple, Constants.PropertyEditors.Aliases.TextArea, false); + RenameDataType(Constants.PropertyEditors.Legacy.Aliases.Textbox, Constants.PropertyEditors.Aliases.TextBox, false); } private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true) @@ -43,7 +43,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 $"Property Editors. Before upgrading to v8, any Data Types using property editors that are named with the prefix '(Obsolete)' must be migrated " + $"to the non-obsolete v7 property editors of the same type."); } - + } Database.Execute(Sql() diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs index bd08b53877..58ec0e30c2 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorMacroColumns.cs @@ -15,21 +15,17 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0 { //special trick to add the column without constraints and return the sql to add them later AddColumn("macroType", out var sqls1); - //now we need to update the new column with some values because this column doesn't allow NULL values - Update.Table(Constants.DatabaseSchema.Tables.Macro).Set(new { macroType = (int)MacroTypes.Unknown}).AllRows().Do(); - //now apply constraints (NOT NULL) to new table - foreach (var sql in sqls1) Execute.Sql(sql).Do(); - - //special trick to add the column without constraints and return the sql to add them later AddColumn("macroSource", out var sqls2); - //populate the new macroSource column with legacy data - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType IS NOT NULL").Do(); - Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython IS NOT NULL").Do(); + //populate the new columns with legacy data + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = '', macroType = {(int)MacroTypes.Unknown}").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroXSLT, macroType = {(int)MacroTypes.Unknown} WHERE macroXSLT != '' AND macroXSLT IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptAssembly, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptAssembly != '' AND macroScriptAssembly IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroScriptType, macroType = {(int)MacroTypes.Unknown} WHERE macroScriptType != '' AND macroScriptType IS NOT NULL").Do(); + Execute.Sql($"UPDATE {Constants.DatabaseSchema.Tables.Macro} SET macroSource = macroPython, macroType = {(int)MacroTypes.PartialView} WHERE macroPython != '' AND macroPython IS NOT NULL").Do(); //now apply constraints (NOT NULL) to new table + foreach (var sql in sqls1) Execute.Sql(sql).Do(); foreach (var sql in sqls2) Execute.Sql(sql).Do(); //now remove these old columns diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs index 64ac20d175..9026f15fc1 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/SuperZero.cs @@ -30,6 +30,7 @@ Database.Execute("set identity_insert umbracoUser off;"); Database.Execute("update umbracoUser2UserGroup set userId=-1 where userId=0;"); + Database.Execute("update umbracoUser2NodeNotify set userId=-1 where userId=0;"); Database.Execute("update umbracoNode set nodeUser=-1 where nodeUser=0;"); Database.Execute("update umbracoUserLogin set userId=-1 where userId=0;"); Database.Execute($"update {Constants.DatabaseSchema.Tables.ContentVersion} set userId=-1 where userId=0;"); diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs index bf048bf2bd..b68aa23d78 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_1_0/ConvertTinyMceAndGridMediaUrlsToLocalLink.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Newtonsoft.Json; @@ -36,6 +37,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 var properties = Database.Fetch(sqlPropertyData); + var exceptions = new List(); foreach (var property in properties) { var value = property.TextValue; @@ -43,19 +45,34 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 if (property.PropertyTypeDto.DataTypeDto.EditorAlias == Constants.PropertyEditors.Aliases.Grid) { - var obj = JsonConvert.DeserializeObject(value); - var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); - - foreach (var control in allControls.SelectMany(c => c)) + try { - var controlValue = control["value"]; - if (controlValue.Type == JTokenType.String) + var obj = JsonConvert.DeserializeObject(value); + var allControls = obj.SelectTokens("$.sections..rows..areas..controls"); + + foreach (var control in allControls.SelectMany(c => c).OfType()) { - control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + var controlValue = control["value"]; + if (controlValue?.Type == JTokenType.String) + { + control["value"] = UpdateMediaUrls(mediaLinkPattern, controlValue.Value()); + } } + + property.TextValue = JsonConvert.SerializeObject(obj); + } + catch (JsonException e) + { + exceptions.Add(new InvalidOperationException( + "Cannot deserialize the value as json. This can be because the property editor " + + "type is changed from another type into a grid. Old versions of the value in this " + + "property can have the structure from the old property editor type. This needs to be " + + "changed manually before updating the database.\n" + + $"Property info: Id = {property.Id}, LanguageId = {property.LanguageId}, VersionId = {property.VersionId}, Value = {property.Value}" + , e)); + continue; } - property.TextValue = JsonConvert.SerializeObject(obj); } else { @@ -65,6 +82,12 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_1_0 Database.Update(property); } + + if (exceptions.Any()) + { + throw new AggregateException("One or more errors related to unexpected data in grid values occurred.", exceptions); + } + Context.AddPostMigration(); } 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/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 644e60a961..af8c781072 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -221,7 +221,13 @@ namespace Umbraco.Core.Models return true; } - public static void UnpublishCulture(this IContent content, string culture = "*") + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool UnpublishCulture(this IContent content, string culture = "*") { culture = culture.NullOrWhiteSpaceAsNull(); @@ -229,16 +235,31 @@ namespace Umbraco.Core.Models if (!content.ContentType.SupportsPropertyVariation(culture, "*", true)) throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\"."); - if (culture == "*") // all cultures + + var keepProcessing = true; + + if (culture == "*") + { + // all cultures content.ClearPublishInfos(); - else // one single culture - content.ClearPublishInfo(culture); + } + else + { + // one single culture + keepProcessing = content.ClearPublishInfo(culture); + } + - // property.PublishValues only publishes what is valid, variation-wise - foreach (var property in content.Properties) - property.UnpublishValues(culture); + if (keepProcessing) + { + // property.PublishValues only publishes what is valid, variation-wise + foreach (var property in content.Properties) + property.UnpublishValues(culture); - content.PublishedState = PublishedState.Publishing; + content.PublishedState = PublishedState.Publishing; + } + + return keepProcessing; } public static void ClearPublishInfos(this IContent content) @@ -246,15 +267,24 @@ namespace Umbraco.Core.Models content.PublishCultureInfos = null; } - public static void ClearPublishInfo(this IContent content, string culture) + /// + /// Returns false if the culture is already unpublished + /// + /// + /// + /// + public static bool ClearPublishInfo(this IContent content, string culture) { if (culture == null) throw new ArgumentNullException(nameof(culture)); if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(culture)); - content.PublishCultureInfos.Remove(culture); - - // set the culture to be dirty - it's been modified - content.TouchCulture(culture); + var removed = content.PublishCultureInfos.Remove(culture); + if (removed) + { + // set the culture to be dirty - it's been modified + content.TouchCulture(culture); + } + return removed; } /// 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/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs index 5f1fe6ed49..ed87c5f320 100644 --- a/src/Umbraco.Core/Models/IContentTypeBase.cs +++ b/src/Umbraco.Core/Models/IContentTypeBase.cs @@ -101,7 +101,7 @@ namespace Umbraco.Core.Models PropertyGroupCollection PropertyGroups { get; set; } /// - /// Gets all local property types belonging to a group, across all local property groups. + /// Gets all local property types all local property groups or ungrouped. /// IEnumerable PropertyTypes { get; } diff --git a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs index 3903fe405b..b1d0189c56 100644 --- a/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/UserGroupExtensions.cs @@ -15,6 +15,12 @@ namespace Umbraco.Core.Models.Membership return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, group.StartContentId, group.StartMediaId, group.Alias, group.AllowedSections, group.Permissions); } + public static bool IsSystemUserGroup(this IUserGroup group) => + IsSystemUserGroup(group.Alias); + + public static bool IsSystemUserGroup(this IReadOnlyUserGroup group) => + IsSystemUserGroup(group.Alias); + public static IReadOnlyUserGroup ToReadOnlyGroup(this UserGroupDto group) { return new ReadOnlyUserGroup(group.Id, group.Name, group.Icon, @@ -22,5 +28,12 @@ namespace Umbraco.Core.Models.Membership group.UserGroup2AppDtos.Select(x => x.AppAlias).ToArray(), group.DefaultPermissions == null ? Enumerable.Empty() : group.DefaultPermissions.ToCharArray().Select(x => x.ToString())); } + + private static bool IsSystemUserGroup(this string groupAlias) + { + return groupAlias == Constants.Security.AdminGroupAlias + || groupAlias == Constants.Security.SensitiveDataGroupAlias + || groupAlias == Constants.Security.TranslatorGroupAlias; + } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index 816bfdbb01..89009ac7b8 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -30,6 +30,16 @@ /// Is used by constructor to create special property types. IPublishedPropertyType CreatePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// + /// Creates a core (non-user) published property type. + /// + /// The published content type owning the property. + /// The property type alias. + /// The datatype identifier. + /// The variations. + /// Is used by constructor to create special property types. + IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations); + /// /// Gets a published datatype. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs index 6f42715f16..dd60eb9beb 100644 --- a/src/Umbraco.Core/Models/PublishedContent/ModelType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/ModelType.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; +using Umbraco.Core.Exceptions; namespace Umbraco.Core.Models.PublishedContent { @@ -76,7 +77,7 @@ namespace Umbraco.Core.Models.PublishedContent return type; var def = type.GetGenericTypeDefinition(); if (def == null) - throw new InvalidOperationException("panic"); + throw new PanicException($"The type {type} has not generic type definition"); var args = type.GetGenericArguments().Select(x => Map(x, modelTypes, true)).ToArray(); return def.MakeGenericType(args); @@ -115,7 +116,7 @@ namespace Umbraco.Core.Models.PublishedContent return type.FullName; var def = type.GetGenericTypeDefinition(); if (def == null) - throw new InvalidOperationException("panic"); + throw new PanicException($"The type {type} has not generic type definition"); var args = type.GetGenericArguments().Select(x => MapToName(x, map, true)).ToArray(); var defFullName = def.FullName.Substring(0, def.FullName.IndexOf('`')); diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 3b03cfc9ea..b11e991118 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -92,26 +92,26 @@ namespace Umbraco.Core.Models.PublishedContent { var aliases = new HashSet(propertyTypes.Select(x => x.Alias), StringComparer.OrdinalIgnoreCase); - foreach ((var alias, (var dataTypeId, var editorAlias)) in BuiltinMemberProperties) + foreach (var (alias, dataTypeId) in BuiltinMemberProperties) { if (aliases.Contains(alias)) continue; - propertyTypes.Add(factory.CreatePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); + propertyTypes.Add(factory.CreateCorePropertyType(this, alias, dataTypeId, ContentVariation.Nothing)); } } // TODO: this list somehow also exists in constants, see memberTypeRepository => remove duplicate! - private static readonly Dictionary BuiltinMemberProperties = new Dictionary + private static readonly Dictionary BuiltinMemberProperties = new Dictionary { - { "Email", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "Username", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "PasswordQuestion", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "Comments", (Constants.DataTypes.Textbox, Constants.PropertyEditors.Aliases.TextBox) }, - { "IsApproved", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) }, - { "IsLockedOut", (Constants.DataTypes.Boolean, Constants.PropertyEditors.Aliases.Boolean) }, - { "LastLockoutDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, - { "CreateDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, - { "LastLoginDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, - { "LastPasswordChangeDate", (Constants.DataTypes.DateTime, Constants.PropertyEditors.Aliases.DateTime) }, + { "Email", Constants.DataTypes.Textbox }, + { "Username", Constants.DataTypes.Textbox }, + { "PasswordQuestion", Constants.DataTypes.Textbox }, + { "Comments", Constants.DataTypes.Textbox }, + { "IsApproved", Constants.DataTypes.Boolean }, + { "IsLockedOut", Constants.DataTypes.Boolean }, + { "LastLockoutDate", Constants.DataTypes.DateTime }, + { "CreateDate", Constants.DataTypes.DateTime }, + { "LastLoginDate", Constants.DataTypes.DateTime }, + { "LastPasswordChangeDate", Constants.DataTypes.DateTime }, }; #region Content type diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 17a15a2536..34094508c3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -61,6 +61,12 @@ namespace Umbraco.Core.Models.PublishedContent return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, true, variations, _propertyValueConverters, _publishedModelFactory, this); } + /// + public IPublishedPropertyType CreateCorePropertyType(IPublishedContentType contentType, string propertyTypeAlias, int dataTypeId, ContentVariation variations = ContentVariation.Nothing) + { + return new PublishedPropertyType(contentType, propertyTypeAlias, dataTypeId, false, variations, _propertyValueConverters, _publishedModelFactory, this); + } + /// /// This method is for tests and is not intended to be used directly from application code. /// 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/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs index 68bc9c923d..78ad60f763 100644 --- a/src/Umbraco.Core/ObjectExtensions.cs +++ b/src/Umbraco.Core/ObjectExtensions.cs @@ -542,7 +542,7 @@ namespace Umbraco.Core { return "\"{0}\"".InvariantFormat(obj); } - if (obj is int || obj is Int16 || obj is Int64 || obj is float || obj is double || obj is bool || obj is int? || obj is Int16? || obj is Int64? || obj is float? || obj is double? || obj is bool?) + if (obj is int || obj is short || obj is long || obj is float || obj is double || obj is bool || obj is int? || obj is float? || obj is double? || obj is bool?) { return "{0}".InvariantFormat(obj); } @@ -723,7 +723,7 @@ namespace Umbraco.Core { return typeConverter; } - + var converter = TypeDescriptor.GetConverter(target); if (converter.CanConvertFrom(source)) { @@ -788,6 +788,6 @@ namespace Umbraco.Core return BoolConvertCache[type] = false; } - + } } diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs index c811f484bc..281cc2c396 100644 --- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Text.RegularExpressions; using System.Web; using System.Xml.Linq; using System.Xml.XPath; @@ -12,7 +11,9 @@ using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Models.Packaging; +using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; @@ -26,13 +27,14 @@ namespace Umbraco.Core.Packaging private readonly ILocalizationService _localizationService; private readonly IDataTypeService _dataTypeService; private readonly PropertyEditorCollection _propertyEditors; + private readonly IScopeProvider _scopeProvider; private readonly IEntityService _entityService; private readonly IContentTypeService _contentTypeService; private readonly IContentService _contentService; public PackageDataInstallation(ILogger logger, IFileService fileService, IMacroService macroService, ILocalizationService localizationService, IDataTypeService dataTypeService, IEntityService entityService, IContentTypeService contentTypeService, - IContentService contentService, PropertyEditorCollection propertyEditors) + IContentService contentService, PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider) { _logger = logger; _fileService = fileService; @@ -40,12 +42,13 @@ namespace Umbraco.Core.Packaging _localizationService = localizationService; _dataTypeService = dataTypeService; _propertyEditors = propertyEditors; + _scopeProvider = scopeProvider; _entityService = entityService; _contentTypeService = contentTypeService; _contentService = contentService; } - #region Uninstall + #region Install/Uninstall public UninstallationSummary UninstallPackageData(PackageDefinition package, int userId) { @@ -58,93 +61,97 @@ namespace Umbraco.Core.Packaging var removedDataTypes = new List(); var removedLanguages = new List(); - - //Uninstall templates - foreach (var item in package.Templates.ToArray()) + using (var scope = _scopeProvider.CreateScope()) { - if (int.TryParse(item, out var nId) == false) continue; - var found = _fileService.GetTemplate(nId); - if (found != null) + //Uninstall templates + foreach (var item in package.Templates.ToArray()) { - removedTemplates.Add(found); - _fileService.DeleteTemplate(found.Alias, userId); + if (int.TryParse(item, out var nId) == false) continue; + var found = _fileService.GetTemplate(nId); + if (found != null) + { + removedTemplates.Add(found); + _fileService.DeleteTemplate(found.Alias, userId); + } + package.Templates.Remove(nId.ToString()); } - package.Templates.Remove(nId.ToString()); - } - //Uninstall macros - foreach (var item in package.Macros.ToArray()) - { - if (int.TryParse(item, out var nId) == false) continue; - var macro = _macroService.GetById(nId); - if (macro != null) + //Uninstall macros + foreach (var item in package.Macros.ToArray()) { - removedMacros.Add(macro); - _macroService.Delete(macro, userId); + if (int.TryParse(item, out var nId) == false) continue; + var macro = _macroService.GetById(nId); + if (macro != null) + { + removedMacros.Add(macro); + _macroService.Delete(macro, userId); + } + package.Macros.Remove(nId.ToString()); } - package.Macros.Remove(nId.ToString()); - } - //Remove Document Types - var contentTypes = new List(); - var contentTypeService = _contentTypeService; - foreach (var item in package.DocumentTypes.ToArray()) - { - if (int.TryParse(item, out var nId) == false) continue; - var contentType = contentTypeService.Get(nId); - if (contentType == null) continue; - contentTypes.Add(contentType); - package.DocumentTypes.Remove(nId.ToString(CultureInfo.InvariantCulture)); - } - - //Order the DocumentTypes before removing them - if (contentTypes.Any()) - { - // TODO: I don't think this ordering is necessary - var orderedTypes = (from contentType in contentTypes - orderby contentType.ParentId descending, contentType.Id descending - select contentType).ToList(); - removedContentTypes.AddRange(orderedTypes); - contentTypeService.Delete(orderedTypes, userId); - } - - //Remove Dictionary items - foreach (var item in package.DictionaryItems.ToArray()) - { - if (int.TryParse(item, out var nId) == false) continue; - var di = _localizationService.GetDictionaryItemById(nId); - if (di != null) + //Remove Document Types + var contentTypes = new List(); + var contentTypeService = _contentTypeService; + foreach (var item in package.DocumentTypes.ToArray()) { - removedDictionaryItems.Add(di); - _localizationService.Delete(di, userId); + if (int.TryParse(item, out var nId) == false) continue; + var contentType = contentTypeService.Get(nId); + if (contentType == null) continue; + contentTypes.Add(contentType); + package.DocumentTypes.Remove(nId.ToString(CultureInfo.InvariantCulture)); } - package.DictionaryItems.Remove(nId.ToString()); - } - //Remove Data types - foreach (var item in package.DataTypes.ToArray()) - { - if (int.TryParse(item, out var nId) == false) continue; - var dtd = _dataTypeService.GetDataType(nId); - if (dtd != null) + //Order the DocumentTypes before removing them + if (contentTypes.Any()) { - removedDataTypes.Add(dtd); - _dataTypeService.Delete(dtd, userId); + // TODO: I don't think this ordering is necessary + var orderedTypes = (from contentType in contentTypes + orderby contentType.ParentId descending, contentType.Id descending + select contentType).ToList(); + removedContentTypes.AddRange(orderedTypes); + contentTypeService.Delete(orderedTypes, userId); } - package.DataTypes.Remove(nId.ToString()); - } - //Remove Langs - foreach (var item in package.Languages.ToArray()) - { - if (int.TryParse(item, out var nId) == false) continue; - var lang = _localizationService.GetLanguageById(nId); - if (lang != null) + //Remove Dictionary items + foreach (var item in package.DictionaryItems.ToArray()) { - removedLanguages.Add(lang); - _localizationService.Delete(lang, userId); + if (int.TryParse(item, out var nId) == false) continue; + var di = _localizationService.GetDictionaryItemById(nId); + if (di != null) + { + removedDictionaryItems.Add(di); + _localizationService.Delete(di, userId); + } + package.DictionaryItems.Remove(nId.ToString()); } - package.Languages.Remove(nId.ToString()); + + //Remove Data types + foreach (var item in package.DataTypes.ToArray()) + { + if (int.TryParse(item, out var nId) == false) continue; + var dtd = _dataTypeService.GetDataType(nId); + if (dtd != null) + { + removedDataTypes.Add(dtd); + _dataTypeService.Delete(dtd, userId); + } + package.DataTypes.Remove(nId.ToString()); + } + + //Remove Langs + foreach (var item in package.Languages.ToArray()) + { + if (int.TryParse(item, out var nId) == false) continue; + var lang = _localizationService.GetLanguageById(nId); + if (lang != null) + { + removedLanguages.Add(lang); + _localizationService.Delete(lang, userId); + } + package.Languages.Remove(nId.ToString()); + } + + scope.Complete(); } // create a summary of what was actually removed, for PackagingService.UninstalledPackage @@ -165,14 +172,40 @@ namespace Umbraco.Core.Packaging } + public InstallationSummary InstallPackageData(CompiledPackage compiledPackage, int userId) + { + using (var scope = _scopeProvider.CreateScope()) + { + var installationSummary = new InstallationSummary + { + DataTypesInstalled = ImportDataTypes(compiledPackage.DataTypes.ToList(), userId), + LanguagesInstalled = ImportLanguages(compiledPackage.Languages, userId), + DictionaryItemsInstalled = ImportDictionaryItems(compiledPackage.DictionaryItems, userId), + MacrosInstalled = ImportMacros(compiledPackage.Macros, userId), + TemplatesInstalled = ImportTemplates(compiledPackage.Templates.ToList(), userId), + DocumentTypesInstalled = ImportDocumentTypes(compiledPackage.DocumentTypes, userId) + }; + + //we need a reference to the imported doc types to continue + var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x); + + installationSummary.StylesheetsInstalled = ImportStylesheets(compiledPackage.Stylesheets, userId); + installationSummary.ContentInstalled = ImportContent(compiledPackage.Documents, importedDocTypes, userId); + + scope.Complete(); + + return installationSummary; + } + } + #endregion #region Content - public IEnumerable ImportContent(IEnumerable docs, IDictionary importedDocumentTypes, int userId) + public IReadOnlyList ImportContent(IEnumerable docs, IDictionary importedDocumentTypes, int userId) { - return docs.SelectMany(x => ImportContent(x, -1, importedDocumentTypes, userId)); + return docs.SelectMany(x => ImportContent(x, -1, importedDocumentTypes, userId)).ToList(); } /// @@ -353,7 +386,7 @@ namespace Umbraco.Core.Packaging #region DocumentTypes - public IEnumerable ImportDocumentType(XElement docTypeElement, int userId) + public IReadOnlyList ImportDocumentType(XElement docTypeElement, int userId) { return ImportDocumentTypes(new[] { docTypeElement }, userId); } @@ -364,7 +397,7 @@ namespace Umbraco.Core.Packaging /// Xml to import /// Optional id of the User performing the operation. Default is zero (admin). /// An enumerable list of generated ContentTypes - public IEnumerable ImportDocumentTypes(IEnumerable docTypeElements, int userId) + public IReadOnlyList ImportDocumentTypes(IEnumerable docTypeElements, int userId) { return ImportDocumentTypes(docTypeElements.ToList(), true, userId); } @@ -376,7 +409,7 @@ namespace Umbraco.Core.Packaging /// Boolean indicating whether or not to import the /// Optional id of the User performing the operation. Default is zero (admin). /// An enumerable list of generated ContentTypes - public IEnumerable ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId) + public IReadOnlyList ImportDocumentTypes(IReadOnlyCollection unsortedDocumentTypes, bool importStructure, int userId) { var importedContentTypes = new Dictionary(); @@ -575,12 +608,11 @@ namespace Umbraco.Core.Packaging contentType.Thumbnail = infoElement.Element("Thumbnail").Value; contentType.Description = infoElement.Element("Description").Value; - //NOTE AllowAtRoot is a new property in the package xml so we need to verify it exists before using it. + //NOTE AllowAtRoot, IsListView, IsElement and Variations are new properties in the package xml so we need to verify it exists before using it. var allowAtRoot = infoElement.Element("AllowAtRoot"); if (allowAtRoot != null) contentType.AllowedAsRoot = allowAtRoot.Value.InvariantEquals("true"); - //NOTE IsListView is a new property in the package xml so we need to verify it exists before using it. var isListView = infoElement.Element("IsListView"); if (isListView != null) contentType.IsContainer = isListView.Value.InvariantEquals("true"); @@ -589,6 +621,10 @@ namespace Umbraco.Core.Packaging if (isElement != null) contentType.IsElement = isElement.Value.InvariantEquals("true"); + var variationsElement = infoElement.Element("Variations"); + if (variationsElement != null) + contentType.Variations = (ContentVariation)Enum.Parse(typeof(ContentVariation), variationsElement.Value); + //Name of the master corresponds to the parent and we need to ensure that the Parent Id is set var masterElement = infoElement.Element("Master"); if (masterElement != null) @@ -614,7 +650,7 @@ namespace Umbraco.Core.Packaging var compositionContentType = importedContentTypes.ContainsKey(compositionAlias) ? importedContentTypes[compositionAlias] : _contentTypeService.Get(compositionAlias); - var added = contentType.AddContentType(compositionContentType); + contentType.AddContentType(compositionContentType); } } } @@ -748,9 +784,14 @@ namespace Umbraco.Core.Packaging { Name = property.Element("Name").Value, Description = (string)property.Element("Description"), - Mandatory = property.Element("Mandatory") != null ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") : false, + Mandatory = property.Element("Mandatory") != null + ? property.Element("Mandatory").Value.ToLowerInvariant().Equals("true") + : false, ValidationRegExp = (string)property.Element("Validation"), - SortOrder = sortOrder + SortOrder = sortOrder, + Variations = property.Element("Variations") != null + ? (ContentVariation)Enum.Parse(typeof(ContentVariation), property.Element("Variations").Value) + : ContentVariation.Nothing }; var tab = (string)property.Element("Tab"); @@ -817,7 +858,7 @@ namespace Umbraco.Core.Packaging /// Xml to import /// Optional id of the user /// An enumerable list of generated DataTypeDefinitions - public IEnumerable ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId) + public IReadOnlyList ImportDataTypes(IReadOnlyCollection dataTypeElements, int userId) { var dataTypes = new List(); @@ -946,13 +987,13 @@ namespace Umbraco.Core.Packaging /// Xml to import /// /// An enumerable list of dictionary items - public IEnumerable ImportDictionaryItems(IEnumerable dictionaryItemElementList, int userId) + public IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, int userId) { var languages = _localizationService.GetAllLanguages().ToList(); return ImportDictionaryItems(dictionaryItemElementList, languages, null, userId); } - private IEnumerable ImportDictionaryItems(IEnumerable dictionaryItemElementList, List languages, Guid? parentId, int userId) + private IReadOnlyList ImportDictionaryItems(IEnumerable dictionaryItemElementList, List languages, Guid? parentId, int userId) { var items = new List(); foreach (var dictionaryItemElement in dictionaryItemElementList) @@ -1029,7 +1070,7 @@ namespace Umbraco.Core.Packaging /// Xml to import /// Optional id of the User performing the operation /// An enumerable list of generated languages - public IEnumerable ImportLanguages(IEnumerable languageElements, int userId) + public IReadOnlyList ImportLanguages(IEnumerable languageElements, int userId) { var list = new List(); foreach (var languageElement in languageElements) @@ -1058,7 +1099,7 @@ namespace Umbraco.Core.Packaging /// Xml to import /// Optional id of the User performing the operation /// - public IEnumerable ImportMacros(IEnumerable macroElements, int userId) + public IReadOnlyList ImportMacros(IEnumerable macroElements, int userId) { var macros = macroElements.Select(ParseMacroElement).ToList(); @@ -1148,7 +1189,7 @@ namespace Umbraco.Core.Packaging #region Stylesheets - public IEnumerable ImportStylesheets(IEnumerable stylesheetElements, int userId) + public IReadOnlyList ImportStylesheets(IEnumerable stylesheetElements, int userId) { var result = new List(); @@ -1216,7 +1257,7 @@ namespace Umbraco.Core.Packaging /// Xml to import /// Optional user id /// An enumerable list of generated Templates - public IEnumerable ImportTemplates(IReadOnlyCollection templateElements, int userId) + public IReadOnlyList ImportTemplates(IReadOnlyCollection templateElements, int userId) { var templates = new List(); diff --git a/src/Umbraco.Core/Packaging/PackageInstallation.cs b/src/Umbraco.Core/Packaging/PackageInstallation.cs index d791295b38..a42ee1aeb2 100644 --- a/src/Umbraco.Core/Packaging/PackageInstallation.cs +++ b/src/Umbraco.Core/Packaging/PackageInstallation.cs @@ -90,21 +90,8 @@ namespace Umbraco.Core.Packaging public InstallationSummary InstallPackageData(PackageDefinition packageDefinition, CompiledPackage compiledPackage, int userId) { - var installationSummary = new InstallationSummary - { - DataTypesInstalled = _packageDataInstallation.ImportDataTypes(compiledPackage.DataTypes.ToList(), userId), - LanguagesInstalled = _packageDataInstallation.ImportLanguages(compiledPackage.Languages, userId), - DictionaryItemsInstalled = _packageDataInstallation.ImportDictionaryItems(compiledPackage.DictionaryItems, userId), - MacrosInstalled = _packageDataInstallation.ImportMacros(compiledPackage.Macros, userId), - TemplatesInstalled = _packageDataInstallation.ImportTemplates(compiledPackage.Templates.ToList(), userId), - DocumentTypesInstalled = _packageDataInstallation.ImportDocumentTypes(compiledPackage.DocumentTypes, userId) - }; + var installationSummary = _packageDataInstallation.InstallPackageData(compiledPackage, userId); - //we need a reference to the imported doc types to continue - var importedDocTypes = installationSummary.DocumentTypesInstalled.ToDictionary(x => x.Alias, x => x); - - installationSummary.StylesheetsInstalled = _packageDataInstallation.ImportStylesheets(compiledPackage.Stylesheets, userId); - installationSummary.ContentInstalled = _packageDataInstallation.ImportContent(compiledPackage.Documents, importedDocTypes, userId); installationSummary.Actions = CompiledPackageXmlParser.GetPackageActions(XElement.Parse(compiledPackage.Actions), compiledPackage.Name); installationSummary.MetaData = compiledPackage; installationSummary.FilesInstalled = packageDefinition.Files; diff --git a/src/Umbraco.Core/Persistence/BulkDataReader.cs b/src/Umbraco.Core/Persistence/BulkDataReader.cs index 1eaa88ee88..7dbe74922a 100644 --- a/src/Umbraco.Core/Persistence/BulkDataReader.cs +++ b/src/Umbraco.Core/Persistence/BulkDataReader.cs @@ -470,7 +470,7 @@ namespace Umbraco.Core.Persistence break; case SqlDbType.SmallInt: - dataType = typeof(Int16); + dataType = typeof(short); dataTypeName = "smallint"; break; @@ -688,34 +688,34 @@ namespace Umbraco.Core.Persistence DataColumnCollection columns = _schemaTable.Columns; - columns.Add(SchemaTableColumn.ColumnName, typeof(System.String)); - columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(System.Int32)); - columns.Add(SchemaTableColumn.ColumnSize, typeof(System.Int32)); - columns.Add(SchemaTableColumn.NumericPrecision, typeof(System.Int16)); - columns.Add(SchemaTableColumn.NumericScale, typeof(System.Int16)); - columns.Add(SchemaTableColumn.IsUnique, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.IsKey, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(System.String)); - columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(System.String)); - columns.Add(SchemaTableColumn.BaseColumnName, typeof(System.String)); - columns.Add(SchemaTableColumn.BaseSchemaName, typeof(System.String)); - columns.Add(SchemaTableColumn.BaseTableName, typeof(System.String)); - columns.Add(SchemaTableColumn.DataType, typeof(System.Type)); - columns.Add(SchemaTableColumn.AllowDBNull, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.ProviderType, typeof(System.Int32)); - columns.Add(SchemaTableColumn.IsAliased, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.IsExpression, typeof(System.Boolean)); - columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(System.Boolean)); - columns.Add(SchemaTableColumn.IsLong, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(System.Boolean)); - columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(System.Type)); - columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(System.String)); - columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(System.String)); - columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(System.String)); - columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(System.String)); + columns.Add(SchemaTableColumn.ColumnName, typeof(string)); + columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int)); + columns.Add(SchemaTableColumn.ColumnSize, typeof(int)); + columns.Add(SchemaTableColumn.NumericPrecision, typeof(short)); + columns.Add(SchemaTableColumn.NumericScale, typeof(short)); + columns.Add(SchemaTableColumn.IsUnique, typeof(bool)); + columns.Add(SchemaTableColumn.IsKey, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string)); + columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(string)); + columns.Add(SchemaTableColumn.BaseColumnName, typeof(string)); + columns.Add(SchemaTableColumn.BaseSchemaName, typeof(string)); + columns.Add(SchemaTableColumn.BaseTableName, typeof(string)); + columns.Add(SchemaTableColumn.DataType, typeof(Type)); + columns.Add(SchemaTableColumn.AllowDBNull, typeof(bool)); + columns.Add(SchemaTableColumn.ProviderType, typeof(int)); + columns.Add(SchemaTableColumn.IsAliased, typeof(bool)); + columns.Add(SchemaTableColumn.IsExpression, typeof(bool)); + columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(bool)); + columns.Add(SchemaTableColumn.IsLong, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(bool)); + columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type)); + columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(string)); + columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(string)); + columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(string)); + columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(string)); } #endregion @@ -1090,7 +1090,7 @@ namespace Umbraco.Core.Persistence /// public decimal GetDecimal(int i) { - return (Decimal)GetValue(i); + return (decimal)GetValue(i); } /// 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/EntityNotFoundException.cs b/src/Umbraco.Core/Persistence/EntityNotFoundException.cs index 1d075339c0..ea6d5142f0 100644 --- a/src/Umbraco.Core/Persistence/EntityNotFoundException.cs +++ b/src/Umbraco.Core/Persistence/EntityNotFoundException.cs @@ -3,8 +3,6 @@ using System.Runtime.Serialization; namespace Umbraco.Core.Persistence { - // TODO: Would be good to use this exception type anytime we cannot find an entity - /// /// An exception used to indicate that an Umbraco entity could not be found. /// diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs index f1473b5888..33dabe1b24 100644 --- a/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/PropertyFactory.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Persistence.Factories public static IEnumerable BuildEntities(PropertyType[] propertyTypes, IReadOnlyCollection dtos, int publishedVersionId, ILanguageRepository languageRepository) { var properties = new List(); - var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable) x); + var xdtos = dtos.GroupBy(x => x.PropertyTypeId).ToDictionary(x => x.Key, x => (IEnumerable)x); foreach (var propertyType in propertyTypes) { @@ -104,10 +104,14 @@ namespace Umbraco.Core.Persistence.Factories /// The properties to map /// /// out parameter indicating that one or more properties have been edited - /// out parameter containing a collection of edited cultures when the contentVariation varies by culture + /// + /// Out parameter containing a collection of edited cultures when the contentVariation varies by culture. + /// The value of this will be used to populate the edited cultures in the umbracoDocumentCultureVariation table. + /// /// public static IEnumerable BuildDtos(ContentVariation contentVariation, int currentVersionId, int publishedVersionId, IEnumerable properties, - ILanguageRepository languageRepository, out bool edited, out HashSet editedCultures) + ILanguageRepository languageRepository, out bool edited, + out HashSet editedCultures) { var propertyDataDtos = new List(); edited = false; @@ -130,6 +134,9 @@ namespace Umbraco.Core.Persistence.Factories // publishing = deal with edit and published values foreach (var propertyValue in property.Values) { + var isInvariantValue = propertyValue.Culture == null; + var isCultureValue = propertyValue.Culture != null && propertyValue.Segment == null; + // deal with published value if (propertyValue.PublishedValue != null && publishedVersionId > 0) propertyDataDtos.Add(BuildDto(publishedVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.PublishedValue)); @@ -138,26 +145,36 @@ namespace Umbraco.Core.Persistence.Factories if (propertyValue.EditedValue != null) propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + // property.Values will contain ALL of it's values, both variant and invariant which will be populated if the + // administrator has previously changed the property type to be variant vs invariant. + // We need to check for this scenario here because otherwise the editedCultures and edited flags + // will end up incorrectly set in the umbracoDocumentCultureVariation table so here we need to + // only process edited cultures based on the current value type and how the property varies. + // The above logic will still persist the currently saved property value for each culture in case the admin + // decides to swap the property's variance again, in which case the edited flag will be recalculated. + + if (property.PropertyType.VariesByCulture() && isInvariantValue || !property.PropertyType.VariesByCulture() && isCultureValue) + continue; + // use explicit equals here, else object comparison fails at comparing eg strings var sameValues = propertyValue.PublishedValue == null ? propertyValue.EditedValue == null : propertyValue.PublishedValue.Equals(propertyValue.EditedValue); + edited |= !sameValues; - if (entityVariesByCulture // cultures can be edited, ie CultureNeutral is supported - && propertyValue.Culture != null && propertyValue.Segment == null // and value is CultureNeutral - && !sameValues) // and edited and published are different + if (entityVariesByCulture && !sameValues) { - editedCultures.Add(propertyValue.Culture); // report culture as edited - } + if (isCultureValue) + { + editedCultures.Add(propertyValue.Culture); // report culture as edited + } + else if (isInvariantValue) + { + // flag culture as edited if it contains an edited invariant property + if (defaultCulture == null) + defaultCulture = languageRepository.GetDefaultIsoCode(); - // flag culture as edited if it contains an edited invariant property - if (propertyValue.Culture == null //invariant property - && !sameValues // and edited and published are different - && entityVariesByCulture) //only when the entity is variant - { - if (defaultCulture == null) - defaultCulture = languageRepository.GetDefaultIsoCode(); - - editedCultures.Add(defaultCulture); + editedCultures.Add(defaultCulture); + } } } } @@ -167,7 +184,7 @@ namespace Umbraco.Core.Persistence.Factories { // not publishing = only deal with edit values if (propertyValue.EditedValue != null) - propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); + propertyDataDtos.Add(BuildDto(currentVersionId, property, languageRepository.GetIdByIsoCode(propertyValue.Culture), propertyValue.Segment, propertyValue.EditedValue)); } edited = true; } 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/AuditRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs index c25328b10c..0788594e3a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/AuditRepository.cs @@ -174,7 +174,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement totalRecords = page.TotalItems; var items = page.Items.Select( - dto => new AuditItem(dto.Id, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); + dto => new AuditItem(dto.NodeId, Enum.ParseOrNull(dto.Header) ?? AuditType.Custom, dto.UserId ?? Constants.Security.UnknownUserId, dto.EntityType, dto.Comment, dto.Parameters)).ToList(); // map the DateStamp for (var i = 0; i < items.Count; i++) diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs index aeb4c3774f..7ab73f3f2d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -512,31 +512,16 @@ namespace Umbraco.Core.Persistence.Repositories.Implement foreach (var a in allPropertyDataDtos) a.PropertyTypeDto = indexedPropertyTypeDtos[a.PropertyTypeId]; - // prefetch configuration for tag properties - var tagEditors = new Dictionary(); - foreach (var propertyTypeDto in indexedPropertyTypeDtos.Values) - { - var editorAlias = propertyTypeDto.DataTypeDto.EditorAlias; - var editorAttribute = PropertyEditors[editorAlias].GetTagAttribute(); - if (editorAttribute == null) continue; - var tagConfigurationSource = propertyTypeDto.DataTypeDto.Configuration; - var tagConfiguration = string.IsNullOrWhiteSpace(tagConfigurationSource) - ? new TagConfiguration() - : JsonConvert.DeserializeObject(tagConfigurationSource); - if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = editorAttribute.Delimiter; - tagEditors[editorAlias] = tagConfiguration; - } - // now we have // - the definitions // - all property data dtos - // - tag editors + // - tag editors (Actually ... no we don't since i removed that code, but we don't need them anyways it seems) // and we need to build the proper property collections - return GetPropertyCollections(temps, allPropertyDataDtos, tagEditors); + return GetPropertyCollections(temps, allPropertyDataDtos); } - private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos, Dictionary tagConfigurations) + private IDictionary GetPropertyCollections(List> temps, IEnumerable allPropertyDataDtos) where T : class, IContentBase { var result = new Dictionary(); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs index 6b751eb8ff..4393d365f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; @@ -90,7 +91,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement contentType = ContentTypeFactory.BuildContentTypeEntity(contentTypeDto); else if (contentTypeDto.NodeDto.NodeObjectType == Constants.ObjectTypes.MemberType) contentType = ContentTypeFactory.BuildMemberTypeEntity(contentTypeDto); - else throw new Exception("panic"); + else throw new PanicException($"The node object type {contentTypeDto.NodeDto.NodeObjectType} is not supported"); contentTypes.Add(contentType.Id, contentType); // map allowed content types diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs index 9d77eb0990..359b967dab 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -18,8 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class ContentTypeRepository : ContentTypeRepositoryBase, IContentTypeRepository { - public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public ContentTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => ContentType.SupportsPublishingConst; @@ -56,7 +57,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 22c9244d8f..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; @@ -15,6 +16,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Scoping; using Umbraco.Core.Services; +using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics; namespace Umbraco.Core.Persistence.Repositories.Implement { @@ -26,14 +28,15 @@ namespace Umbraco.Core.Persistence.Repositories.Implement internal abstract class ContentTypeRepositoryBase : NPocoRepositoryBase, IReadRepository where TEntity : class, IContentTypeComposition { - protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) + protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) : base(scopeAccessor, cache, logger) { CommonRepository = commonRepository; + LanguageRepository = languageRepository; } protected IContentTypeCommonRepository CommonRepository { get; } - + protected ILanguageRepository LanguageRepository { get; } protected abstract bool SupportsPublishing { get; } public IEnumerable> Move(TEntity moving, EntityContainer container) @@ -98,6 +101,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected void PersistNewBaseContentType(IContentTypeComposition entity) { + ValidateVariations(entity); + var dto = ContentTypeFactory.BuildContentTypeDto(entity); //Cannot add a duplicate content type @@ -163,11 +168,11 @@ AND umbracoNode.nodeObjectType = @objectType", foreach (var allowedContentType in entity.AllowedContentTypes) { Database.Insert(new ContentTypeAllowedContentTypeDto - { - Id = entity.Id, - AllowedId = allowedContentType.Id.Value, - SortOrder = allowedContentType.SortOrder - }); + { + Id = entity.Id, + AllowedId = allowedContentType.Id.Value, + SortOrder = allowedContentType.SortOrder + }); } @@ -214,6 +219,9 @@ AND umbracoNode.nodeObjectType = @objectType", protected void PersistUpdatedBaseContentType(IContentTypeComposition entity) { + CorrectPropertyTypeVariations(entity); + ValidateVariations(entity); + var dto = ContentTypeFactory.BuildContentTypeDto(entity); // ensure the alias is not used already @@ -370,7 +378,7 @@ AND umbracoNode.id <> @id", foreach (var propertyGroup in entity.PropertyGroups) { // insert or update group - var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup,entity.Id); + var groupDto = PropertyGroupFactory.BuildGroupDto(propertyGroup, entity.Id); var groupId = propertyGroup.HasIdentity ? Database.Update(groupDto) : Convert.ToInt32(Database.Insert(groupDto)); @@ -388,7 +396,7 @@ AND umbracoNode.id <> @id", //check if the content type variation has been changed var contentTypeVariationDirty = entity.IsPropertyDirty("Variations"); - var oldContentTypeVariation = (ContentVariation) dtoPk.Variations; + var oldContentTypeVariation = (ContentVariation)dtoPk.Variations; var newContentTypeVariation = entity.Variations; var contentTypeVariationChanging = contentTypeVariationDirty && oldContentTypeVariation != newContentTypeVariation; if (contentTypeVariationChanging) @@ -404,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 @@ -449,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); } } @@ -506,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, @@ -518,6 +503,42 @@ 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) + { + foreach (var prop in entity.PropertyTypes) + { + // 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) { var impact = new List(); @@ -525,12 +546,12 @@ AND umbracoNode.id <> @id", var tree = new Dictionary>(); foreach (var x in all) - foreach (var y in x.ContentTypeComposition) - { - if (!tree.TryGetValue(y.Id, out var list)) - list = tree[y.Id] = new List(); - list.Add(x); - } + foreach (var y in x.ContentTypeComposition) + { + if (!tree.TryGetValue(y.Id, out var list)) + list = tree[y.Id] = new List(); + list.Add(x); + } var nset = new List(); do @@ -572,7 +593,7 @@ AND umbracoNode.id <> @id", // new property type, ignore if (!oldVariations.TryGetValue(propertyType.Id, out var oldVariationB)) continue; - var oldVariation = (ContentVariation) oldVariationB; // NPoco cannot fetch directly + var oldVariation = (ContentVariation)oldVariationB; // NPoco cannot fetch directly // only those property types that *actually* changed var newVariation = propertyType.Variations; @@ -636,25 +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); - break; - case ContentVariation.Nothing: - CopyPropertyData(defaultLanguageId, null, propertyTypeIds, impactedL); - CopyTagData(defaultLanguageId, null, 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); } } } @@ -666,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 } } @@ -963,6 +980,205 @@ AND umbracoNode.id <> @id", Database.Execute(sqlDelete); } + + } + + /// + /// Re-normalizes the edited value in the umbracoDocumentCultureVariation and umbracoDocument table when variations are changed + /// + /// + /// + /// + /// If this is not done, then in some cases the "edited" value for a particular culture for a document will remain true when it should be false + /// if the property was changed to invariant. In order to do this we need to recalculate this value based on the values stored for each + /// property, culture and current/published version. + /// + private void RenormalizeDocumentEditedFlags(IReadOnlyCollection propertyTypeIds, IReadOnlyCollection contentTypeIds = null) + { + var defaultLang = LanguageRepository.GetDefaultId(); + + //This will build up a query to get the property values of both the current and the published version so that we can check + //based on the current variance of each item to see if it's 'edited' value should be true/false. + + var whereInArgsCount = propertyTypeIds.Count + (contentTypeIds?.Count ?? 0); + if (whereInArgsCount > 2000) + throw new NotSupportedException("Too many property/content types."); + + var propertySql = Sql() + .Select() + .AndSelect(x => x.NodeId, x => x.Current) + .AndSelect(x => x.Published) + .AndSelect(x => x.Variations) + .From() + .InnerJoin().On((left, right) => left.Id == right.VersionId) + .InnerJoin().On((left, right) => left.Id == right.PropertyTypeId); + + if (contentTypeIds != null) + { + propertySql.InnerJoin().On((c, cversion) => c.NodeId == cversion.NodeId); + } + + propertySql.LeftJoin().On((docversion, cversion) => cversion.Id == docversion.Id) + .Where((docversion, cversion) => cversion.Current || docversion.Published) + .WhereIn(x => x.PropertyTypeId, propertyTypeIds); + + if (contentTypeIds != null) + { + propertySql.WhereIn(x => x.ContentTypeId, contentTypeIds); + } + + propertySql + .OrderBy(x => x.NodeId) + .OrderBy(x => x.PropertyTypeId, x => x.LanguageId, x => x.VersionId); + + //keep track of this node/lang to mark or unmark a culture as edited + var editedLanguageVersions = new Dictionary<(int nodeId, int? langId), bool>(); + //keep track of which node to mark or unmark as edited + var editedDocument = new Dictionary(); + var nodeId = -1; + var propertyTypeId = -1; + + PropertyValueVersionDto pubRow = null; + + //This is a reader (Query), we are not fetching this all into memory so we cannot make any changes during this iteration, we are just collecting data. + //Published data will always come before Current data based on the version id sort. + //There will only be one published row (max) and one current row per property. + foreach (var row in Database.Query(propertySql)) + { + //make sure to reset on each node/property change + if (nodeId != row.NodeId || propertyTypeId != row.PropertyTypeId) + { + nodeId = row.NodeId; + propertyTypeId = row.PropertyTypeId; + pubRow = null; + } + + if (row.Published) + pubRow = row; + + if (row.Current) + { + var propVariations = (ContentVariation)row.Variations; + + //if this prop doesn't vary but the row has a lang assigned or vice versa, flag this as not edited + if (!propVariations.VariesByCulture() && row.LanguageId.HasValue + || propVariations.VariesByCulture() && !row.LanguageId.HasValue) + { + //Flag this as not edited for this node/lang if the key doesn't exist + if (!editedLanguageVersions.TryGetValue((row.NodeId, row.LanguageId), out _)) + editedLanguageVersions.Add((row.NodeId, row.LanguageId), false); + + //mark as false if the item doesn't exist, else coerce to true + editedDocument[row.NodeId] = editedDocument.TryGetValue(row.NodeId, out var edited) ? (edited |= false) : false; + } + else if (pubRow == null) + { + //this would mean that that this property is 'edited' since there is no published version + editedLanguageVersions[(row.NodeId, row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + //compare the property values, if they differ from versions then flag the current version as edited + else if (IsPropertyValueChanged(pubRow, row)) + { + //Here we would check if the property is invariant, in which case the edited language should be indicated by the default lang + editedLanguageVersions[(row.NodeId, !propVariations.VariesByCulture() ? defaultLang : row.LanguageId)] = true; + editedDocument[row.NodeId] = true; + } + + //reset + pubRow = null; + } + } + + //lookup all matching rows in umbracoDocumentCultureVariation + var docCultureVariationsToUpdate = editedLanguageVersions.InGroupsOf(2000) + .SelectMany(_ => Database.Fetch( + Sql().Select().From() + .WhereIn(x => x.LanguageId, editedLanguageVersions.Keys.Select(x => x.langId).ToList()) + .WhereIn(x => x.NodeId, editedLanguageVersions.Keys.Select(x => x.nodeId)))) + //convert to dictionary with the same key type + .ToDictionary(x => (x.NodeId, (int?)x.LanguageId), x => x); + + var toUpdate = new List(); + foreach (var ev in editedLanguageVersions) + { + if (docCultureVariationsToUpdate.TryGetValue(ev.Key, out var docVariations)) + { + //check if it needs updating + if (docVariations.Edited != ev.Value) + { + docVariations.Edited = ev.Value; + toUpdate.Add(docVariations); + } + } + else if (ev.Key.langId.HasValue) + { + //This should never happen! If a property culture is flagged as edited then the culture must exist at the document level + throw new PanicException($"The existing DocumentCultureVariationDto was not found for node {ev.Key.nodeId} and language {ev.Key.langId}"); + } + } + + //Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false + foreach (var editValue in toUpdate.GroupBy(x => x.Edited)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.Id, editValue.Select(x => x.Id))); + } + + //Now bulk update the umbracoDocument table + foreach (var editValue in editedDocument.GroupBy(x => x.Value)) + { + Database.Execute(Sql().Update(u => u.Set(x => x.Edited, editValue.Key)) + .WhereIn(x => x.NodeId, editValue.Select(x => x.Key))); + } + } + + private static bool IsPropertyValueChanged(PropertyValueVersionDto pubRow, PropertyValueVersionDto row) + { + return !pubRow.TextValue.IsNullOrWhiteSpace() && pubRow.TextValue != row.TextValue + || !pubRow.VarcharValue.IsNullOrWhiteSpace() && pubRow.VarcharValue != row.VarcharValue + || pubRow.DateValue.HasValue && pubRow.DateValue != row.DateValue + || pubRow.DecimalValue.HasValue && pubRow.DecimalValue != row.DecimalValue + || pubRow.IntValue.HasValue && pubRow.IntValue != row.IntValue; + } + + private class NameCompareDto + { + public int NodeId { get; set; } + public int CurrentVersion { get; set; } + public int LanguageId { get; set; } + public string CurrentName { get; set; } + public string PublishedName { get; set; } + public int? PublishedVersion { get; set; } + public int Id { get; set; } // the Id of the DocumentCultureVariationDto + public bool Edited { get; set; } + } + + private class PropertyValueVersionDto + { + public int VersionId { get; set; } + public int PropertyTypeId { get; set; } + public int? LanguageId { get; set; } + public string Segment { get; set; } + public int? IntValue { get; set; } + + private decimal? _decimalValue; + [Column("decimalValue")] + public decimal? DecimalValue + { + get => _decimalValue; + set => _decimalValue = value?.Normalize(); + } + + public DateTime? DateValue { get; set; } + public string VarcharValue { get; set; } + public string TextValue { get; set; } + + public int NodeId { get; set; } + public bool Current { get; set; } + public bool Published { get; set; } + + public byte Variations { get; set; } } private void DeletePropertyType(int contentTypeId, int propertyTypeId) 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 1505f394ed..dd9c7c93e5 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 @@ -386,7 +435,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } // refresh content @@ -511,7 +560,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement entity.VersionId = documentVersionDto.Id = contentVersionDto.Id; // get the new id documentVersionDto.Published = false; // non-published version - Database.Insert(documentVersionDto); + Database.Insert(documentVersionDto); } // replace the property data (rather than updating) @@ -571,7 +620,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.BulkInsertRecords(GetContentVariationDtos(entity, publishing)); // insert document variations - Database.BulkInsertRecords(GetDocumentVariationDtos(entity, publishing, editedCultures)); + Database.BulkInsertRecords(GetDocumentVariationDtos(entity, editedCultures)); } // refresh content @@ -1297,25 +1346,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement }; } - private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures) + private IEnumerable GetDocumentVariationDtos(IContent content, HashSet editedCultures) { var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct foreach (var culture in allCultures) - yield return new DocumentCultureVariationDto + { + var dto = new DocumentCultureVariationDto { NodeId = content.Id, LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."), Culture = culture, Name = content.GetCultureName(culture) ?? content.GetPublishName(culture), - - // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem - Available = content.IsCultureAvailable(culture), Published = content.IsCulturePublished(culture), + // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem Edited = content.IsCultureAvailable(culture) && (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))) }; + + yield return dto; + } + } private class ContentVariation diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs index 4a32e373c1..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) @@ -404,7 +415,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement if (isMedia) { sql - .InnerJoin().On((left, right) => left.Id == right.Id); + .LeftJoin().On((left, right) => left.Id == right.Id); } //Any LeftJoin statements need to come last @@ -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 8429532b01..a905294417 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/LanguageRepository.cs @@ -102,17 +102,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement protected override IEnumerable GetDeleteClauses() { - var list = new List { //NOTE: There is no constraint between the Language and cmsDictionary/cmsLanguageText tables (?) // but we still need to remove them - "DELETE FROM cmsLanguageText WHERE languageId = @id", - "DELETE FROM umbracoPropertyData WHERE languageId = @id", - "DELETE FROM umbracoContentVersionCultureVariation WHERE languageId = @id", - "DELETE FROM umbracoDocumentCultureVariation WHERE languageId = @id", - "DELETE FROM umbracoLanguage WHERE id = @id", - "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id" + "DELETE FROM " + Constants.DatabaseSchema.Tables.DictionaryValue + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.ContentVersionCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.DocumentCultureVariation + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.TagRelationship + " WHERE tagId IN (SELECT id FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id)", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Tag + " WHERE languageId = @id", + "DELETE FROM " + Constants.DatabaseSchema.Tables.Language + " WHERE id = @id" }; return list; } @@ -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 @@ -250,7 +263,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement lock (_codeIdMap) { if (_codeIdMap.TryGetValue(isoCode, out var id)) return id; - if (isoCode.Contains('-') && _codeIdMap.TryGetValue(isoCode.Split('-').First(), out var invariantId)) return invariantId; } if (throwOnNotFound) throw new ArgumentException($"Code {isoCode} does not correspond to an existing language.", nameof(isoCode)); diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs index 1abc75cf3a..512011b52c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MediaTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -17,8 +18,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MediaTypeRepository : ContentTypeRepositoryBase, IMediaTypeRepository { - public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MediaTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MediaType.SupportsPublishingConst; @@ -50,7 +51,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } 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/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs index d96854743e..ee651819bf 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/MemberTypeRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NPoco; using Umbraco.Core.Cache; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; @@ -18,8 +19,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement /// internal class MemberTypeRepository : ContentTypeRepositoryBase, IMemberTypeRepository { - public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository) - : base(scopeAccessor, cache, logger, commonRepository) + public MemberTypeRepository(IScopeAccessor scopeAccessor, AppCaches cache, ILogger logger, IContentTypeCommonRepository commonRepository, ILanguageRepository languageRepository) + : base(scopeAccessor, cache, logger, commonRepository, languageRepository) { } protected override bool SupportsPublishing => MemberType.SupportsPublishingConst; @@ -57,7 +58,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement { // the cache policy will always want everything // even GetMany(ids) gets everything and filters afterwards - if (ids.Any()) throw new Exception("panic"); + if (ids.Any()) throw new PanicException("There can be no ids specified"); return CommonRepository.GetAllTypes().OfType(); } 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/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index 9471285148..afd602cfc9 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -32,7 +32,7 @@ using System.Runtime.InteropServices; [assembly: InternalsVisibleTo("Umbraco.Forms.Web")] // Umbraco Headless -[assembly: InternalsVisibleTo("Umbraco.Headless")] +[assembly: InternalsVisibleTo("Umbraco.Cloud.Headless")] // code analysis // IDE1006 is broken, wants _value syntax for consts, etc - and it's even confusing ppl at MS, kill it diff --git a/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs new file mode 100644 index 0000000000..25fba622d5 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ConfigurationFieldsExtensions.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.PropertyEditors +{ + public static partial class ConfigurationFieldsExtensions + { + /// + /// Adds a configuration field. + /// + /// The list of configuration fields. + /// The key (alias) of the field. + /// The name (label) of the field. + /// The description for the field. + /// The path to the editor view to be used for the field. + /// Optional configuration used for field's editor. + public static void Add( + this List fields, + string key, + string name, + string description, + string view, + IDictionary config = null) + { + fields.Add(new ConfigurationField + { + Key = key, + Name = name, + Description = description, + View = view, + Config = config, + }); + } + } +} diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 43f4b68b99..dbb2fc467e 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.PropertyEditors // defaults Type = type; Icon = Constants.Icons.PropertyEditor; - Group = "common"; + Group = Constants.PropertyEditors.Groups.Common; // assign properties based on the attribute, if it is found Attribute = GetType().GetCustomAttribute(false); diff --git a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs index f780cdd3d7..7b3be7ea5f 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditorAttribute.cs @@ -122,7 +122,7 @@ namespace Umbraco.Core.PropertyEditors /// Gets or sets an optional group. /// /// The group can be used for example to group the editors by category. - public string Group { get; set; } = "common"; + public string Group { get; set; } = Constants.PropertyEditors.Groups.Common; /// /// Gets or sets a value indicating whether the value editor is deprecated. 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/PropertyEditors/LabelPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs index d0c40b1e63..60b7d55c01 100644 --- a/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/LabelPropertyEditor.cs @@ -5,7 +5,11 @@ namespace Umbraco.Core.PropertyEditors /// /// Represents a property editor for label properties. /// - [DataEditor(Constants.PropertyEditors.Aliases.Label, "Label", "readonlyvalue", Icon = "icon-readonly")] + [DataEditor( + Constants.PropertyEditors.Aliases.Label, + "Label", + "readonlyvalue", + Icon = "icon-readonly")] public class LabelPropertyEditor : DataEditor { /// diff --git a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs index 2439c7c02e..6549f1a233 100644 --- a/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs +++ b/src/Umbraco.Core/PropertyEditors/TagsPropertyEditorAttribute.cs @@ -55,6 +55,7 @@ namespace Umbraco.Core.PropertyEditors /// /// Gets the type of the dynamic configuration provider. /// + //TODO: This is not used and should be implemented in a nicer way, see https://github.com/umbraco/Umbraco-CMS/issues/6017#issuecomment-516253562 public Type TagsConfigurationProviderType { get; } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs index 84baf226cf..eb461b4920 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -65,6 +65,8 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (source is decimal sourceDecimal) return sourceDecimal; if (source is string sourceDecimalString) return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0; + if (source is double sourceDouble) + return Convert.ToDecimal(sourceDouble); return (decimal) 0; case ValueTypes.Integer: if (source is int sourceInt) return sourceInt; diff --git a/src/Umbraco.Core/PublishedContentExtensions.cs b/src/Umbraco.Core/PublishedContentExtensions.cs index f220f307d6..921883b822 100644 --- a/src/Umbraco.Core/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/PublishedContentExtensions.cs @@ -103,18 +103,26 @@ namespace Umbraco.Core /// Gets the children of the content item. /// /// The content item. - /// The specific culture to get the url children for. If null is used the current culture is used (Default is null). + /// + /// The specific culture to get the url children for. Default is null which will use the current culture in + /// /// /// Gets children that are available for the specified culture. /// Children are sorted by their sortOrder. - /// The '*' culture and supported and returns everything. + /// + /// For culture, + /// if null is used the current culture is used. + /// If an empty string is used only invariant children are returned. + /// If "*" is used all children are returned. + /// + /// + /// If a variant culture is specified or there is a current culture in the then the Children returned + /// will include both the variant children matching the culture AND the invariant children because the invariant children flow with the current culture. + /// However, if an empty string is specified only invariant children are returned. + /// /// public static IEnumerable Children(this IPublishedContent content, string culture = null) { - // invariant has invariant value (whatever the requested culture) - if (!content.ContentType.VariesByCulture() && culture != "*") - culture = ""; - // handle context culture for variant if (culture == null) culture = VariationContextAccessor?.VariationContext?.Culture ?? ""; diff --git a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs index de6eeb6a42..ac8c9f1be7 100644 --- a/src/Umbraco.Core/PublishedModelFactoryExtensions.cs +++ b/src/Umbraco.Core/PublishedModelFactoryExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core @@ -15,12 +17,8 @@ namespace Umbraco.Core /// public static bool IsLiveFactory(this IPublishedModelFactory factory) => factory is ILivePublishedModelFactory; - /// - /// Executes an action with a safe live factory - /// - /// - /// If the factory is a live factory, ensures it is refreshed and locked while executing the action. - /// + [Obsolete("This method is no longer used or necessary and will be removed from future")] + [EditorBrowsable(EditorBrowsableState.Never)] public static void WithSafeLiveFactory(this IPublishedModelFactory factory, Action action) { if (factory is ILivePublishedModelFactory liveFactory) @@ -37,5 +35,38 @@ namespace Umbraco.Core action(); } } + + /// + /// Sets a flag to reset the ModelsBuilder models if the is + /// + /// + /// + /// + /// This does not recompile the pure live models, only sets a flag to tell models builder to recompile when they are requested. + /// + internal static void WithSafeLiveFactoryReset(this IPublishedModelFactory factory, Action action) + { + if (factory is ILivePublishedModelFactory liveFactory) + { + lock (liveFactory.SyncRoot) + { + // TODO: Fix this in 8.3! - We need to change the ILivePublishedModelFactory interface to have a Reset method and then when we have an embedded MB + // version we will publicize the ResetModels (and change the name to Reset). + // For now, this will suffice and we'll use reflection, there should be no other implementation of ILivePublishedModelFactory. + // Calling ResetModels resets the MB flag so that the next time EnsureModels is called (which is called when nucache lazily calls CreateModel) it will + // trigger the recompiling of pure live models. + var resetMethod = liveFactory.GetType().GetMethod("ResetModels", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); + if (resetMethod != null) + resetMethod.Invoke(liveFactory, null); + + action(); + } + } + else + { + action(); + } + } + } } diff --git a/src/Umbraco.Core/Runtime/CoreRuntime.cs b/src/Umbraco.Core/Runtime/CoreRuntime.cs index f9a41b4f66..5b069641c4 100644 --- a/src/Umbraco.Core/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Core/Runtime/CoreRuntime.cs @@ -139,7 +139,7 @@ namespace Umbraco.Core.Runtime // there should be none, really - this is here "just in case" Compose(composition); - // acquire the main domain + // acquire the main domain - if this fails then anything that should be registered with MainDom will not operate AcquireMainDom(mainDom); // determine our runtime level @@ -218,13 +218,13 @@ namespace Umbraco.Core.Runtime IOHelper.SetRootDirectory(path); } - private void AcquireMainDom(MainDom mainDom) + private bool AcquireMainDom(MainDom mainDom) { using (var timer = ProfilingLogger.DebugDuration("Acquiring MainDom.", "Acquired.")) { try { - mainDom.Acquire(); + return mainDom.Acquire(); } catch { diff --git a/src/Umbraco.Core/RuntimeState.cs b/src/Umbraco.Core/RuntimeState.cs index 6fb8a04c0d..5d34fe70a1 100644 --- a/src/Umbraco.Core/RuntimeState.cs +++ b/src/Umbraco.Core/RuntimeState.cs @@ -97,6 +97,11 @@ namespace Umbraco.Core /// internal void EnsureApplicationUrl(HttpRequestBase request = null) { + //Fixme: This causes problems with site swap on azure because azure pre-warms a site by calling into `localhost` and when it does that + // it changes the URL to `localhost:80` which actually doesn't work for pinging itself, it only works internally in Azure. The ironic part + // about this is that this is here specifically for the slot swap scenario https://issues.umbraco.org/issue/U4-10626 + + // see U4-10626 - in some cases we want to reset the application url // (this is a simplified version of what was in 7.x) // note: should this be optional? is it expensive? 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/Serialization/JsonNetSerializer.cs b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs index 1b5a9c6caf..46f23ca35e 100644 --- a/src/Umbraco.Core/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Core/Serialization/JsonNetSerializer.cs @@ -60,7 +60,7 @@ namespace Umbraco.Core.Serialization /// public IStreamedResult ToStream(object input) { - string s = JsonConvert.SerializeObject(input, Formatting.Indented, _settings); + string s = JsonConvert.SerializeObject(input, Formatting.None, _settings); byte[] bytes = Encoding.UTF8.GetBytes(s); MemoryStream ms = new MemoryStream(bytes); diff --git a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs index f643039dca..37f1e5127f 100644 --- a/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs +++ b/src/Umbraco.Core/Services/ContentTypeServiceExtensions.cs @@ -7,6 +7,21 @@ namespace Umbraco.Core.Services { public static class ContentTypeServiceExtensions { + /// + /// Gets all of the element types (e.g. content types that have been marked as an element type). + /// + /// The content type service. + /// Returns all the element types. + public static IEnumerable GetAllElementTypes(this IContentTypeService contentTypeService) + { + if (contentTypeService == null) + { + return Enumerable.Empty(); + } + + return contentTypeService.GetAll().Where(x => x.IsElement); + } + /// /// Returns the available composite content types for a given content type /// @@ -22,12 +37,14 @@ namespace Umbraco.Core.Services /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot /// be looked up via the db, they need to be passed in. /// + /// Wether the composite content types should be applicable for an element type /// internal static ContentTypeAvailableCompositionsResults GetAvailableCompositeContentTypes(this IContentTypeService ctService, IContentTypeComposition source, IContentTypeComposition[] allContentTypes, string[] filterContentTypes = null, - string[] filterPropertyTypes = null) + string[] filterPropertyTypes = null, + bool isElement = false) { filterContentTypes = filterContentTypes == null ? Array.Empty() @@ -46,7 +63,7 @@ namespace Umbraco.Core.Services .Select(c => c.Alias) .Union(filterPropertyTypes) .ToArray(); - + var sourceId = source?.Id ?? 0; // find out if any content type uses this content type @@ -64,8 +81,9 @@ namespace Umbraco.Core.Services x => x.Id)); // usable types are those that are top-level + // do not allow element types to be composed by non-element types as this will break the model generation in ModelsBuilder var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + .Where(x => x.ContentTypeComposition.Any() == false && (isElement == false || x.IsElement)).ToArray(); foreach (var x in usableContentTypes) list.Add(x); 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 4d6400a38f..a684df32a2 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -4,13 +4,13 @@ using System.ComponentModel; using System.Globalization; using System.Linq; using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; -using Umbraco.Core.Persistence.Repositories.Implement; using Umbraco.Core.Scoping; using Umbraco.Core.Services.Changes; @@ -755,11 +755,11 @@ namespace Umbraco.Core.Services.Implement { var publishedState = content.PublishedState; if (publishedState != PublishedState.Published && publishedState != PublishedState.Unpublished) - throw new InvalidOperationException("Cannot save (un)publishing content, use the dedicated SavePublished method."); + throw new InvalidOperationException($"Cannot save (un)publishing content with name: {content.Name} - and state: {content.PublishedState}, use the dedicated SavePublished method."); if (content.Name != null && content.Name.Length > 255) { - throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + throw new InvalidOperationException($"Content with the name {content.Name} cannot be more than 255 characters in length."); } var evtMsgs = EventMessagesFactory.Get(); @@ -884,6 +884,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -892,13 +894,13 @@ namespace Umbraco.Core.Services.Implement // if culture is '*', then publish them all (including variants) //this will create the correct culture impact even if culture is * or null - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -919,6 +921,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var evtMsgs = EventMessagesFactory.Get(); var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -926,25 +930,23 @@ namespace Umbraco.Core.Services.Implement var varies = content.ContentType.VariesByCulture(); - if (cultures.Length == 0) + if (cultures.Length == 0 && !varies) { //no cultures specified and doesn't vary, so publish it, else nothing to publish - return !varies - ? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents) - : new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + return SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents); } if (cultures.Any(x => x == null || x == "*")) throw new InvalidOperationException("Only valid cultures are allowed to be used in this method, wildcards or nulls are not allowed"); - var impacts = cultures.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))); + var impacts = cultures.Select(x => CultureImpact.Explicit(x, IsDefaultCulture(allLangs, x))); // publish the culture(s) // we don't care about the response here, this response will be rechecked below but we need to set the culture info values now. foreach (var impact in impacts) content.PublishCulture(impact); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } @@ -984,6 +986,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); @@ -991,26 +995,39 @@ namespace Umbraco.Core.Services.Implement // all cultures = unpublish whole if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null)) { + // It's important to understand that when the document varies by culture but the "*" is used, + // we are just unpublishing the whole document but leaving all of the culture's as-is. This is expected + // because we don't want to actually unpublish every culture and then the document, we just want everything + // to be non-routable so that when it's re-published all variants were as they were. + content.PublishedState = PublishedState.Unpublishing; + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + scope.Complete(); + return result; } else { - // If the culture we want to unpublish was already unpublished, nothing to do. - // To check for that we need to lookup the persisted content item - var persisted = content.HasIdentity ? GetById(content.Id) : null; + // Unpublish the culture, this will change the document state to Publishing! ... which is expected because this will + // essentially be re-publishing the document with the requested culture removed. + // The call to CommitDocumentChangesInternal will perform all the checks like if this is a mandatory culture or the last culture being unpublished + // and will then unpublish the document accordingly. + // If the result of this is false it means there was no culture to unpublish (i.e. it was already unpublished or it did not exist) + var removed = content.UnpublishCulture(culture); - if (persisted != null && !persisted.IsCulturePublished(culture)) + //save and publish any changes + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId); + + scope.Complete(); + + // In one case the result will be PublishStatusType.FailedPublishNothingToPublish which means that no cultures + // were specified to be published which will be the case when removed is false. In that case + // we want to swap the result type to PublishResultType.SuccessUnpublishAlready (that was the expectation before). + if (result.Result == PublishResultType.FailedPublishNothingToPublish && !removed) return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content); - // unpublish the culture - content.UnpublishCulture(culture); + return result; } - - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId); - scope.Complete(); - return result; } - } /// @@ -1045,15 +1062,35 @@ namespace Umbraco.Core.Services.Implement if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content); - var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents); + var allLangs = _languageRepository.GetMany().ToList(); + + var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, allLangs, userId, raiseEvents); scope.Complete(); return result; } } + /// + /// Handles a lot of business logic cases for how the document should be persisted + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Business logic cases such: as unpublishing a mandatory culture, or unpublishing the last culture, checking for pending scheduled publishing, etc... is dealt with in this method. + /// There is quite a lot of cases to take into account along with logic that needs to deal with scheduled saving/publishing, branch saving/publishing, etc... + /// + /// private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, - ContentSavingEventArgs saveEventArgs, - int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) + ContentSavingEventArgs saveEventArgs, IReadOnlyCollection allLangs, + int userId = Constants.Security.SuperUserId, + bool raiseEvents = true, bool branchOne = false, bool branchRoot = false) { if (scope == null) throw new ArgumentNullException(nameof(scope)); if (content == null) throw new ArgumentNullException(nameof(content)); @@ -1068,8 +1105,8 @@ namespace Umbraco.Core.Services.Implement if (content.PublishedState != PublishedState.Publishing && content.PublishedState != PublishedState.Unpublishing) content.PublishedState = PublishedState.Publishing; - // state here is either Publishing or Unpublishing - // (even though, Publishing to unpublish a culture may end up unpublishing everything) + // State here is either Publishing or Unpublishing + // Publishing to unpublish a culture may end up unpublishing everything so these flags can be flipped later var publishing = content.PublishedState == PublishedState.Publishing; var unpublishing = content.PublishedState == PublishedState.Unpublishing; @@ -1086,6 +1123,18 @@ namespace Umbraco.Core.Services.Implement var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch; var previouslyPublished = content.HasIdentity && content.Published; + //inline method to persist the document with the documentRepository since this logic could be called a couple times below + void SaveDocument(IContent c) + { + // save, always + if (c.HasIdentity == false) + c.CreatorId = userId; + c.WriterId = userId; + + // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing + _documentRepository.Save(c); + } + if (publishing) { //determine cultures publishing/unpublishing which will be based on previous calls to content.PublishCulture and ClearPublishInfo @@ -1095,11 +1144,25 @@ namespace Umbraco.Core.Services.Implement : null; // ensure that the document can be published, and publish handling events, business rules, etc - publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs); + publishResult = StrategyCanPublish(scope, content, /*checkPath:*/ (!branchOne || branchRoot), culturesPublishing, culturesUnpublishing, evtMsgs, saveEventArgs, allLangs); if (publishResult.Success) { // note: StrategyPublish flips the PublishedState to Publishing! publishResult = StrategyPublish(content, culturesPublishing, culturesUnpublishing, evtMsgs); + + //check if a culture has been unpublished and if there are no cultures left, and then unpublish document as a whole + if (publishResult.Result == PublishResultType.SuccessUnpublishCulture && content.PublishCultureInfos.Count == 0) + { + // This is a special case! We are unpublishing the last culture and to persist that we need to re-publish without any cultures + // so the state needs to remain Publishing to do that. However, we then also need to unpublish the document and to do that + // the state needs to be Unpublishing and it cannot be both. This state is used within the documentRepository to know how to + // persist certain things. So before proceeding below, we need to save the Publishing state to publish no cultures, then we can + // mark the document for Unpublishing. + SaveDocument(content); + + //set the flag to unpublish and continue + unpublishing = content.Published; // if not published yet, nothing to do + } } else { @@ -1160,13 +1223,8 @@ namespace Umbraco.Core.Services.Implement } } - // save, always - if (content.HasIdentity == false) - content.CreatorId = userId; - content.WriterId = userId; - - // saving does NOT change the published version, unless PublishedState is Publishing or Unpublishing - _documentRepository.Save(content); + //Persist the document + SaveDocument(content); // raise the Saved event, always if (raiseEvents) @@ -1184,17 +1242,34 @@ namespace Umbraco.Core.Services.Implement if (culturesUnpublishing != null) { - //If we are here, it means we tried unpublishing a culture but it was mandatory so now everything is unpublished - var langs = string.Join(", ", _languageRepository.GetMany() + // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. + + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); - //log that the whole content item has been unpublished due to mandatory culture unpublished - Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); - } - else - Audit(AuditType.Unpublish, userId, content.Id); + if (publishResult == null) + throw new PanicException("publishResult == null - should not happen"); + + switch(publishResult.Result) + { + case PublishResultType.FailedPublishMandatoryCultureMissing: + //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) + + //log that the whole content item has been unpublished due to mandatory culture unpublished + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (mandatory language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishMandatoryCulture, evtMsgs, content); + case PublishResultType.SuccessUnpublishCulture: + //occurs when the last culture is unpublished + + Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); + return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content); + } + + } + + Audit(AuditType.Unpublish, userId, content.Id); return new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); } @@ -1209,6 +1284,9 @@ namespace Umbraco.Core.Services.Implement { if (isNew == false && previouslyPublished == false) changeType = TreeChangeTypes.RefreshBranch; // whole branch + else if (isNew == false && previouslyPublished) + changeType = TreeChangeTypes.RefreshNode; // single node + // invalidate the node/branch if (!branchOne) // for branches, handled by SaveAndPublishBranch @@ -1234,7 +1312,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessPublishCulture: if (culturesPublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesPublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.PublishVariant, userId, content.Id, $"Published languages: {langs}", langs); @@ -1243,7 +1321,7 @@ namespace Umbraco.Core.Services.Implement case PublishResultType.SuccessUnpublishCulture: if (culturesUnpublishing != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.UnpublishVariant, userId, content.Id, $"Unpublished languages: {langs}", langs); @@ -1257,14 +1335,14 @@ namespace Umbraco.Core.Services.Implement // should not happen if (branchOne && !branchRoot) - throw new Exception("panic"); + throw new PanicException("branchOne && !branchRoot - should not happen"); //if publishing didn't happen or if it has failed, we still need to log which cultures were saved if (!branchOne && (publishResult == null || !publishResult.Success)) { if (culturesChanging != null) { - var langs = string.Join(", ", _languageRepository.GetMany() + var langs = string.Join(", ", allLangs .Where(x => culturesChanging.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); Audit(AuditType.SaveVariant, userId, content.Id, $"Saved languages: {langs}", langs); @@ -1295,6 +1373,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + foreach (var d in _documentRepository.GetContentForRelease(date)) { PublishResult result; @@ -1323,7 +1403,7 @@ namespace Umbraco.Core.Services.Implement //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed Property[] invalidProperties = null; - var impact = CultureImpact.Explicit(culture, _languageRepository.IsDefault(culture)); + var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", @@ -1338,7 +1418,7 @@ namespace Umbraco.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) @@ -1388,7 +1468,7 @@ namespace Umbraco.Core.Services.Implement d.UnpublishCulture(c); } - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); yield return result; @@ -1414,7 +1494,7 @@ namespace Umbraco.Core.Services.Implement } // utility 'PublishCultures' func used by SaveAndPublishBranch - private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish) + private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet culturesToPublish, IReadOnlyCollection allLangs) { //TODO: This does not support being able to return invalid property details to bubble up to the UI @@ -1424,7 +1504,7 @@ namespace Umbraco.Core.Services.Implement { return culturesToPublish.All(culture => { - var impact = CultureImpact.Create(culture, _languageRepository.IsDefault(culture), content); + var impact = CultureImpact.Create(culture, IsDefaultCulture(allLangs, culture), content); return content.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } @@ -1533,7 +1613,7 @@ namespace Umbraco.Core.Services.Implement internal IEnumerable SaveAndPublishBranch(IContent document, bool force, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, int userId = Constants.Security.SuperUserId) { if (shouldPublish == null) throw new ArgumentNullException(nameof(shouldPublish)); @@ -1547,6 +1627,8 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.ContentTree); + var allLangs = _languageRepository.GetMany().ToList(); + if (!document.HasIdentity) throw new InvalidOperationException("Cannot not branch-publish a new document."); @@ -1555,7 +1637,7 @@ namespace Umbraco.Core.Services.Implement throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch."); // deal with the branch root - if it fails, abort - var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId); + var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1586,7 +1668,7 @@ namespace Umbraco.Core.Services.Implement } // no need to check path here, parent has to be published here - result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId); + result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId, allLangs); if (result != null) { results.Add(result); @@ -1618,10 +1700,10 @@ namespace Umbraco.Core.Services.Implement // publishValues: a function publishing values (using the appropriate PublishCulture calls) private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document, Func> shouldPublish, - Func, bool> publishCultures, + Func, IReadOnlyCollection, bool> publishCultures, bool isRoot, ICollection publishedDocuments, - EventMessages evtMsgs, int userId) + EventMessages evtMsgs, int userId, IReadOnlyCollection allLangs) { var culturesToPublish = shouldPublish(document); if (culturesToPublish == null) // null = do not include @@ -1634,13 +1716,13 @@ namespace Umbraco.Core.Services.Implement return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document); // publish & check if values are valid - if (!publishCultures(document, culturesToPublish)) + if (!publishCultures(document, culturesToPublish, allLangs)) { //TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document); } - var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, userId, branchOne: true, branchRoot: isRoot); + var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, allLangs, userId, branchOne: true, branchRoot: isRoot); if (result.Success) publishedDocuments.Add(document); return result; @@ -1766,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)); @@ -2341,6 +2423,9 @@ namespace Umbraco.Core.Services.Implement _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Document), message, parameters)); } + private bool IsDefaultCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsDefault && x.IsoCode.InvariantEquals(culture)); + private bool IsMandatoryCulture(IReadOnlyCollection langs, string culture) => langs.Any(x => x.IsMandatory && x.IsoCode.InvariantEquals(culture)); + #endregion #region Event Handlers @@ -2495,7 +2580,9 @@ namespace Umbraco.Core.Services.Implement /// /// /// - private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs) + private PublishResult StrategyCanPublish(IScope scope, IContent content, bool checkPath, IReadOnlyList culturesPublishing, + IReadOnlyCollection culturesUnpublishing, EventMessages evtMsgs, ContentSavingEventArgs savingEventArgs, + IReadOnlyCollection allLangs) { // raise Publishing event if (scope.Events.DispatchCancelable(Publishing, this, savingEventArgs.ToContentPublishingEventArgs())) @@ -2507,8 +2594,8 @@ namespace Umbraco.Core.Services.Implement var variesByCulture = content.ContentType.VariesByCulture(); var impactsToPublish = culturesPublishing == null - ? new[] {CultureImpact.Invariant} //if it's null it's invariant - : culturesPublishing.Select(x => CultureImpact.Explicit(x, _languageRepository.IsDefault(x))).ToArray(); + ? new[] { CultureImpact.Invariant } //if it's null it's invariant + : culturesPublishing.Select(x => CultureImpact.Explicit(x, allLangs.Any(lang => lang.IsoCode.InvariantEquals(x) && lang.IsMandatory))).ToArray(); // publish the culture(s) if (!impactsToPublish.All(content.PublishCulture)) @@ -2529,11 +2616,17 @@ namespace Umbraco.Core.Services.Implement if (culturesPublishing == null) throw new InvalidOperationException("Internal error, variesByCulture but culturesPublishing is null."); - if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) // no published cultures = cannot be published + if (content.Published && culturesPublishing.Count == 0 && culturesUnpublishing.Count == 0) + { + // no published cultures = cannot be published + // This will occur if for example, a culture that is already unpublished is sent to be unpublished again, or vice versa, in that case + // there will be nothing to publish/unpublish. return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); + } + // missing mandatory culture = cannot be published - var mandatoryCultures = _languageRepository.GetMany().Where(x => x.IsMandatory).Select(x => x.IsoCode); + var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); var mandatoryMissing = mandatoryCultures.Any(x => !content.PublishedCultures.Contains(x, StringComparer.OrdinalIgnoreCase)); if (mandatoryMissing) return new PublishResult(PublishResultType.FailedPublishMandatoryCultureMissing, evtMsgs, content); @@ -2674,6 +2767,7 @@ namespace Umbraco.Core.Services.Implement { var attempt = new PublishResult(PublishResultType.SuccessUnpublish, evtMsgs, content); + //TODO: What is this check?? we just created this attempt and of course it is Success?! if (attempt.Success == false) return attempt; @@ -2894,8 +2988,22 @@ namespace Umbraco.Core.Services.Implement content.CreatorId = userId; content.WriterId = userId; + IEnumerable cultures = ArrayOfOneNullString; + if (blueprint.CultureInfos.Count > 0) + { + cultures = blueprint.CultureInfos.Values.Select(x => x.Culture); + using (var scope = ScopeProvider.CreateScope()) + { + if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out var defaultCulture)) + { + defaultCulture.Name = name; + } + + scope.Complete(); + } + } + var now = DateTime.Now; - var cultures = blueprint.CultureInfos.Count > 0 ? blueprint.CultureInfos.Values.Select(x => x.Culture) : ArrayOfOneNullString; foreach (var culture in cultures) { foreach (var property in blueprint.Properties) diff --git a/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Core/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs index f14fdb6c42..7ae330f8f1 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()); } } @@ -390,6 +390,11 @@ namespace Umbraco.Core.Services.Implement if (string.IsNullOrWhiteSpace(item.Name)) throw new ArgumentException("Cannot save item with empty name."); + if (item.Name != null && item.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + scope.WriteLock(WriteLockIds); // validate the DAG transform, within the lock diff --git a/src/Umbraco.Core/Services/Implement/DataTypeService.cs b/src/Umbraco.Core/Services/Implement/DataTypeService.cs index 3552b2d8fc..5a93fb91b1 100644 --- a/src/Umbraco.Core/Services/Implement/DataTypeService.cs +++ b/src/Umbraco.Core/Services/Implement/DataTypeService.cs @@ -349,6 +349,11 @@ namespace Umbraco.Core.Services.Implement throw new ArgumentException("Cannot save datatype with empty name."); } + if (dataType.Name != null && dataType.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + _dataTypeRepository.Save(dataType); saveEventArgs.CanCancel = false; @@ -461,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/Implement/EntityXmlSerializer.cs b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs index 863ecc0b1b..bc21da15a7 100644 --- a/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs +++ b/src/Umbraco.Core/Services/Implement/EntityXmlSerializer.cs @@ -172,7 +172,7 @@ namespace Umbraco.Core.Services.Implement xml.Add(new XAttribute("Id", dataType.EditorAlias)); xml.Add(new XAttribute("Definition", dataType.Key)); xml.Add(new XAttribute("DatabaseType", dataType.DatabaseType.ToString())); - xml.Add(new XAttribute("Configuration", JsonConvert.SerializeObject(dataType.Configuration))); + xml.Add(new XAttribute("Configuration", JsonConvert.SerializeObject(dataType.Configuration, PropertyEditors.ConfigurationEditor.ConfigurationJsonSettings))); var folderNames = string.Empty; if (dataType.Level != 1) @@ -437,7 +437,8 @@ namespace Umbraco.Core.Services.Implement new XElement("Description", contentType.Description), new XElement("AllowAtRoot", contentType.AllowedAsRoot.ToString()), new XElement("IsListView", contentType.IsContainer.ToString()), - new XElement("IsElement", contentType.IsElement.ToString())); + new XElement("IsElement", contentType.IsElement.ToString()), + new XElement("Variations", contentType.Variations.ToString())); var masterContentType = contentType.ContentTypeComposition.FirstOrDefault(x => x.Id == contentType.ParentId); if(masterContentType != null) @@ -487,7 +488,8 @@ namespace Umbraco.Core.Services.Implement new XElement("SortOrder", propertyType.SortOrder), new XElement("Mandatory", propertyType.Mandatory.ToString()), propertyType.ValidationRegExp != null ? new XElement("Validation", propertyType.ValidationRegExp) : null, - propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null); + propertyType.Description != null ? new XElement("Description", new XCData(propertyType.Description)) : null, + new XElement("Variations", propertyType.Variations.ToString())); genericProperties.Add(genericProperty); } diff --git a/src/Umbraco.Core/Services/Implement/FileService.cs b/src/Umbraco.Core/Services/Implement/FileService.cs index 79d5b35775..ce8600d798 100644 --- a/src/Umbraco.Core/Services/Implement/FileService.cs +++ b/src/Umbraco.Core/Services/Implement/FileService.cs @@ -358,6 +358,11 @@ namespace Umbraco.Core.Services.Implement { "ContentTypeAlias", contentTypeAlias }, }; + if (contentTypeAlias != null && contentTypeAlias.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + // check that the template hasn't been created on disk before creating the content type // if it exists, set the new template content to the existing file content string content = GetViewContent(contentTypeAlias); @@ -365,7 +370,10 @@ namespace Umbraco.Core.Services.Implement { template.Content = content; } + + + using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(template, true, evtMsgs, additionalData); @@ -397,6 +405,21 @@ namespace Umbraco.Core.Services.Implement /// public ITemplate CreateTemplateWithIdentity(string name, string alias, string content, ITemplate masterTemplate = null, int userId = Constants.Security.SuperUserId) { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException("Name cannot be empty or contain only white-space characters", nameof(name)); + } + + if (name.Length > 255) + { + throw new ArgumentOutOfRangeException(nameof(name), "Name cannot be more than 255 characters in length."); + } + // file might already be on disk, if so grab the content to avoid overwriting var template = new Template(name, alias) { @@ -531,6 +554,17 @@ namespace Umbraco.Core.Services.Implement /// public void SaveTemplate(ITemplate template, int userId = Constants.Security.SuperUserId) { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + if (string.IsNullOrWhiteSpace(template.Name) || template.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be null, empty, contain only white-space characters or be more than 255 characters in length."); + } + + using (var scope = ScopeProvider.CreateScope()) { if (scope.Events.DispatchCancelable(SavingTemplate, this, new SaveEventArgs(template))) diff --git a/src/Umbraco.Core/Services/Implement/MediaService.cs b/src/Umbraco.Core/Services/Implement/MediaService.cs index b1a9601fac..528d0a0bf9 100644 --- a/src/Umbraco.Core/Services/Implement/MediaService.cs +++ b/src/Umbraco.Core/Services/Implement/MediaService.cs @@ -138,6 +138,10 @@ namespace Umbraco.Core.Services.Implement var parent = parentId > 0 ? GetById(parentId) : null; if (parentId > 0 && parent == null) throw new ArgumentException("No media with that id.", nameof(parentId)); + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, parentId, mediaType); using (var scope = ScopeProvider.CreateScope()) @@ -167,6 +171,10 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, -1, mediaType); using (var scope = ScopeProvider.CreateScope()) @@ -201,6 +209,10 @@ namespace Umbraco.Core.Services.Implement var mediaType = GetMediaType(mediaTypeAlias); if (mediaType == null) throw new ArgumentException("No media type with that alias.", nameof(mediaTypeAlias)); // causes rollback + if (name != null && name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } var media = new Models.Media(name, parent, mediaType); CreateMedia(scope, media, parent, userId, false); @@ -647,6 +659,11 @@ namespace Umbraco.Core.Services.Implement if (string.IsNullOrWhiteSpace(media.Name)) throw new ArgumentException("Media has no name.", nameof(media)); + if (media.Name != null && media.Name.Length > 255) + { + throw new InvalidOperationException("Name cannot be more than 255 characters in length."); throw new InvalidOperationException("Name cannot be more than 255 characters in length."); + } + scope.WriteLock(Constants.Locks.MediaTree); if (media.HasIdentity == false) media.CreatorId = userId; @@ -759,7 +776,7 @@ namespace Umbraco.Core.Services.Implement const int pageSize = 500; var page = 0; var total = long.MaxValue; - while(page * pageSize < total) + while (page * pageSize < total) { //get descendants - ordered from deepest to shallowest var descendants = GetPagedDescendants(media.Id, page, pageSize, out total, ordering: Ordering.By("Path", Direction.Descending)); @@ -944,7 +961,7 @@ namespace Umbraco.Core.Services.Implement // if media was trashed, and since we're not moving to the recycle bin, // indicate that the trashed status should be changed to false, else just // leave it unchanged - var trashed = media.Trashed ? false : (bool?) null; + var trashed = media.Trashed ? false : (bool?)null; PerformMoveLocked(media, parentId, parent, userId, moves, trashed); scope.Events.Dispatch(TreeChanged, this, new TreeChange(media, TreeChangeTypes.RefreshBranch).ToEventArgs()); @@ -1008,7 +1025,7 @@ namespace Umbraco.Core.Services.Implement private void PerformMoveMediaLocked(IMedia media, int userId, bool? trash) { - if (trash.HasValue) ((ContentBase) media).Trashed = trash.Value; + if (trash.HasValue) ((ContentBase)media).Trashed = trash.Value; _mediaRepository.Save(media); } diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 29eda5bb0b..a64e30495b 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -816,8 +816,8 @@ namespace Umbraco.Core.Services.Implement { //trimming username and email to make sure we have no trailing space member.Username = member.Username.Trim(); - member.Email = member.Email.Trim(); - + member.Email = member.Email.Trim(); + using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(member); 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/Services/PublishResultType.cs b/src/Umbraco.Core/Services/PublishResultType.cs index 1a2b52f9c9..66c1e38267 100644 --- a/src/Umbraco.Core/Services/PublishResultType.cs +++ b/src/Umbraco.Core/Services/PublishResultType.cs @@ -49,6 +49,11 @@ /// SuccessUnpublishMandatoryCulture = 6, + /// + /// The specified document culture was unpublished, and was the last published culture in the document, therefore the document itself was unpublished. + /// + SuccessUnpublishLastCulture = 8, + #endregion #region Success - Mixed @@ -113,9 +118,9 @@ FailedPublishContentInvalid = FailedPublish | 8, /// - /// The document could not be published because it has no publishing flags or values. + /// The document could not be published because it has no publishing flags or values or if its a variant document, no cultures were specified to be published. /// - FailedPublishNothingToPublish = FailedPublish | 9, // TODO: in ContentService.StrategyCanPublish - weird + FailedPublishNothingToPublish = FailedPublish | 9, /// /// The document could not be published because some mandatory cultures are missing. diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 94b9b5617a..4451fdbba7 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -722,67 +722,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/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index 9cd4a58c65..2a558f85aa 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -97,8 +97,7 @@ namespace Umbraco.Core.Sync ? ":" + request.ServerVariables["SERVER_PORT"] : ""; - var useSsl = globalSettings.UseHttps || port == "443"; - var ssl = useSsl ? "s" : ""; // force, whatever the first request + var ssl = globalSettings.UseHttps ? "s" : ""; // force, whatever the first request var url = "http" + ssl + "://" + request.ServerVariables["SERVER_NAME"] + port + IOHelper.ResolveUrl(SystemDirectories.Umbraco); return url.TrimEnd('/'); diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6adce1944f..ffe20afdb3 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -10,6 +10,7 @@ Umbraco.Core ..\ + $(AdditionalFileItemNames);Content true @@ -60,6 +61,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all + 1.3.0 @@ -204,6 +210,7 @@ + @@ -234,6 +241,7 @@ + @@ -243,6 +251,8 @@ + + @@ -261,10 +271,13 @@ + + + @@ -979,7 +992,6 @@ - @@ -1559,4 +1571,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Core/WaitHandleExtensions.cs b/src/Umbraco.Core/WaitHandleExtensions.cs index 0d840a2496..7a9294c113 100644 --- a/src/Umbraco.Core/WaitHandleExtensions.cs +++ b/src/Umbraco.Core/WaitHandleExtensions.cs @@ -23,6 +23,8 @@ namespace Umbraco.Core handle, (state, timedOut) => { + //TODO: We aren't checking if this is timed out + tcs.SetResult(null); // we take a lock here to make sure the outer method has completed setting the local variable callbackHandle. diff --git a/src/Umbraco.Core/Xml/DynamicContext.cs b/src/Umbraco.Core/Xml/DynamicContext.cs index d39c4e133b..e5b90a1622 100644 --- a/src/Umbraco.Core/Xml/DynamicContext.cs +++ b/src/Umbraco.Core/Xml/DynamicContext.cs @@ -236,7 +236,7 @@ namespace Umbraco.Core.Xml _name = name; _value = value; - if (value is String) + if (value is string) _type = XPathResultType.String; else if (value is bool) _type = XPathResultType.Boolean; diff --git a/src/Umbraco.Examine/BaseValueSetBuilder.cs b/src/Umbraco.Examine/BaseValueSetBuilder.cs index 22d379d148..93cee88231 100644 --- a/src/Umbraco.Examine/BaseValueSetBuilder.cs +++ b/src/Umbraco.Examine/BaseValueSetBuilder.cs @@ -45,7 +45,7 @@ namespace Umbraco.Examine continue; case string strVal: { - if (strVal.IsNullOrWhiteSpace()) return; + if (strVal.IsNullOrWhiteSpace()) continue; var key = $"{keyVal.Key}{cultureSuffix}"; if (values.TryGetValue(key, out var v)) values[key] = new List(v) { val }.ToArray(); diff --git a/src/Umbraco.Examine/ContentIndexPopulator.cs b/src/Umbraco.Examine/ContentIndexPopulator.cs index 51b9de4a0b..99ff4d7f87 100644 --- a/src/Umbraco.Examine/ContentIndexPopulator.cs +++ b/src/Umbraco.Examine/ContentIndexPopulator.cs @@ -15,7 +15,7 @@ namespace Umbraco.Examine /// /// Performs the data lookups required to rebuild a content index /// - public class ContentIndexPopulator : IndexPopulator + public class ContentIndexPopulator : IndexPopulator { private readonly IContentService _contentService; private readonly IValueSetBuilder _contentValueSetBuilder; @@ -36,7 +36,7 @@ namespace Umbraco.Examine /// public ContentIndexPopulator(IContentService contentService, ISqlContext sqlContext, IContentValueSetBuilder contentValueSetBuilder) : this(false, null, contentService, sqlContext, contentValueSetBuilder) - { + { } /// @@ -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/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 44cef08813..9cbc311639 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -54,7 +54,7 @@ namespace Umbraco.Examine {"updateDate", new object[] {c.UpdateDate}}, //Always add invariant updateDate {"nodeName", (PublishedValuesOnly //Always add invariant nodeName ? c.PublishName?.Yield() - : c?.Name.Yield()) ?? Enumerable.Empty()}, + : c.Name?.Yield()) ?? Enumerable.Empty()}, {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, diff --git a/src/Umbraco.Examine/ExamineExtensions.cs b/src/Umbraco.Examine/ExamineExtensions.cs index 1b8033c458..d231a86f69 100644 --- a/src/Umbraco.Examine/ExamineExtensions.cs +++ b/src/Umbraco.Examine/ExamineExtensions.cs @@ -12,6 +12,7 @@ using Lucene.Net.Store; using Umbraco.Core; using Version = Lucene.Net.Util.Version; using Umbraco.Core.Logging; +using System.Threading; namespace Umbraco.Examine { @@ -28,6 +29,29 @@ namespace Umbraco.Examine /// internal static readonly Regex CultureIsoCodeFieldNameMatchExpression = new Regex("^([_\\w]+)_([a-z]{2}-[a-z0-9]{2,4})$", RegexOptions.Compiled); + private static bool _isConfigured = false; + private static object _configuredInit = null; + private static object _isConfiguredLocker = new object(); + + /// + /// Called on startup to configure each index. + /// + /// + /// Configures and unlocks all Lucene based indexes registered with the . + /// + internal static void ConfigureIndexes(this IExamineManager examineManager, IMainDom mainDom, ILogger logger) + { + LazyInitializer.EnsureInitialized( + ref _configuredInit, + ref _isConfigured, + ref _isConfiguredLocker, + () => + { + examineManager.ConfigureLuceneIndexes(logger, !mainDom.IsMainDom); + return null; + }); + } + //TODO: We need a public method here to just match a field name against CultureIsoCodeFieldNameMatchExpression /// @@ -48,6 +72,31 @@ namespace Umbraco.Examine } } + /// + /// Returns all index fields that are culture specific (suffixed) or invariant + /// + /// + /// + /// + public static IEnumerable GetCultureAndInvariantFields(this IUmbracoIndex index, string culture) + { + var allFields = index.GetFields(); + // ReSharper disable once LoopCanBeConvertedToQuery + foreach (var field in allFields) + { + var match = CultureIsoCodeFieldNameMatchExpression.Match(field); + if (match.Success && match.Groups.Count == 3 && culture.InvariantEquals(match.Groups[2].Value)) + { + yield return field; //matches this culture field + } + else if (!match.Success) + { + yield return field; //matches no culture field (invariant) + } + + } + } + internal static bool TryParseLuceneQuery(string query) { // TODO: I'd assume there would be a more strict way to parse the query but not that i can find yet, for now we'll diff --git a/src/Umbraco.Examine/IUmbracoContentIndex.cs b/src/Umbraco.Examine/IUmbracoContentIndex.cs new file mode 100644 index 0000000000..3181ff663e --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoContentIndex.cs @@ -0,0 +1,9 @@ +using Examine; + +namespace Umbraco.Examine +{ + public interface IUmbracoContentIndex : IIndex + { + + } +} 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/IUmbracoMemberIndex.cs b/src/Umbraco.Examine/IUmbracoMemberIndex.cs new file mode 100644 index 0000000000..b1f325b2e9 --- /dev/null +++ b/src/Umbraco.Examine/IUmbracoMemberIndex.cs @@ -0,0 +1,9 @@ +using Examine; + +namespace Umbraco.Examine +{ + public interface IUmbracoMemberIndex : IIndex + { + + } +} diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index 43c309b9c5..786aecac71 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -5,7 +5,8 @@ using System.Threading.Tasks; using Examine; namespace Umbraco.Examine -{ +{ + /// /// Utility to rebuild all indexes ensuring minimal data queries /// diff --git a/src/Umbraco.Examine/LuceneIndexDiagnostics.cs b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs new file mode 100644 index 0000000000..96363904b4 --- /dev/null +++ b/src/Umbraco.Examine/LuceneIndexDiagnostics.cs @@ -0,0 +1,83 @@ +using System.Collections.Generic; +using Examine.LuceneEngine.Providers; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Lucene.Net.Store; +using Umbraco.Core.IO; +using System.Linq; + +namespace Umbraco.Examine +{ + public class LuceneIndexDiagnostics : IIndexDiagnostics + { + public LuceneIndexDiagnostics(LuceneIndex index, ILogger logger) + { + Index = index; + Logger = logger; + } + + public LuceneIndex Index { get; } + public ILogger Logger { get; } + + public int DocumentCount + { + get + { + try + { + return Index.GetIndexDocumentCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); + return 0; + } + } + } + + public int FieldCount + { + get + { + try + { + return Index.GetIndexFieldCount(); + } + catch (AlreadyClosedException) + { + Logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); + return 0; + } + } + } + + public Attempt IsHealthy() + { + var isHealthy = Index.IsHealthy(out var indexError); + return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); + } + + public virtual IReadOnlyDictionary Metadata + { + get + { + var luceneDir = Index.GetLuceneDirectory(); + var d = new Dictionary + { + [nameof(UmbracoExamineIndex.CommitCount)] = Index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = Index.DefaultAnalyzer.GetType().Name, + ["LuceneDirectory"] = luceneDir.GetType().Name + }; + + if (luceneDir is FSDirectory fsDir) + { + d[nameof(UmbracoExamineIndex.LuceneIndexFolder)] = fsDir.Directory.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'); + } + + return d; + } + } + + + } +} diff --git a/src/Umbraco.Examine/MemberIndexPopulator.cs b/src/Umbraco.Examine/MemberIndexPopulator.cs index e20dab91ca..26a3b0aedd 100644 --- a/src/Umbraco.Examine/MemberIndexPopulator.cs +++ b/src/Umbraco.Examine/MemberIndexPopulator.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Services; namespace Umbraco.Examine { - public class MemberIndexPopulator : IndexPopulator + public class MemberIndexPopulator : IndexPopulator { private readonly IMemberService _memberService; private readonly IValueSetBuilder _valueSetBuilder; diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 95690c17e4..db623ecddd 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -10,6 +10,7 @@ Umbraco.Examine ..\ + $(AdditionalFileItemNames);Content true @@ -56,6 +57,11 @@ + + 3.3.0 + runtime; build; native; contentfiles; analyzers + all + @@ -64,14 +70,19 @@ + + + + + @@ -101,4 +112,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Examine/UmbracoContentIndex.cs b/src/Umbraco.Examine/UmbracoContentIndex.cs index a9e2c72cb6..e266ca789d 100644 --- a/src/Umbraco.Examine/UmbracoContentIndex.cs +++ b/src/Umbraco.Examine/UmbracoContentIndex.cs @@ -17,13 +17,13 @@ namespace Umbraco.Examine /// /// An indexer for Umbraco content and media /// - public class UmbracoContentIndex : UmbracoExamineIndex + public class UmbracoContentIndex : UmbracoExamineIndex, IUmbracoContentIndex { public const string VariesByCultureFieldName = SpecialFieldPrefix + "VariesByCulture"; protected ILocalizationService LanguageService { get; } #region Constructors - + /// /// Create an index at runtime /// @@ -141,6 +141,6 @@ namespace Umbraco.Examine base.PerformDeleteFromIndex(idsAsList, onComplete); } - + } } diff --git a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs index fed5b9bae7..4a926deebe 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs @@ -7,73 +7,24 @@ using Umbraco.Core.Logging; namespace Umbraco.Examine { - public class UmbracoExamineIndexDiagnostics : IIndexDiagnostics + public class UmbracoExamineIndexDiagnostics : LuceneIndexDiagnostics { private readonly UmbracoExamineIndex _index; - private readonly ILogger _logger; public UmbracoExamineIndexDiagnostics(UmbracoExamineIndex index, ILogger logger) + : base(index, logger) { _index = index; - _logger = logger; } - public int DocumentCount + public override IReadOnlyDictionary Metadata { get { - try - { - return _index.GetIndexDocumentCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); - return 0; - } - } - } + var d = base.Metadata.ToDictionary(x => x.Key, x => x.Value); - public int FieldCount - { - get - { - try - { - return _index.GetIndexFieldCount(); - } - catch (AlreadyClosedException) - { - _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); - return 0; - } - } - } - - public Attempt IsHealthy() - { - var isHealthy = _index.IsHealthy(out var indexError); - return isHealthy ? Attempt.Succeed() : Attempt.Fail(indexError.Message); - } - - public virtual IReadOnlyDictionary Metadata - { - get - { - var d = new Dictionary - { - [nameof(UmbracoExamineIndex.CommitCount)] = _index.CommitCount, - [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = _index.DefaultAnalyzer.GetType().Name, - ["LuceneDirectory"] = _index.GetLuceneDirectory().GetType().Name, - [nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, - [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = - _index.LuceneIndexFolder == null - ? string.Empty - : _index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), - [nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly, - //There's too much info here - //[nameof(UmbracoExamineIndexer.FieldDefinitionCollection)] = _index.FieldDefinitionCollection, - }; + d[nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler; + d[nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly; if (_index.ValueSetValidator is ValueSetValidator vsv) { 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.Examine/UmbracoMemberIndex.cs b/src/Umbraco.Examine/UmbracoMemberIndex.cs index fbf8a1cc0f..445707ab0c 100644 --- a/src/Umbraco.Examine/UmbracoMemberIndex.cs +++ b/src/Umbraco.Examine/UmbracoMemberIndex.cs @@ -11,7 +11,7 @@ namespace Umbraco.Examine /// /// Custom indexer for members /// - public class UmbracoMemberIndex : UmbracoExamineIndex + public class UmbracoMemberIndex : UmbracoExamineIndex, IUmbracoMemberIndex { /// /// Constructor to allow for creating an indexer at runtime @@ -32,6 +32,6 @@ namespace Umbraco.Examine base(name, luceneDirectory, fieldDefinitions, analyzer, profilingLogger, validator) { } - + } } diff --git a/src/Umbraco.Tests/Cache/RefresherTests.cs b/src/Umbraco.Tests/Cache/RefresherTests.cs index b1d0ad7b1a..eb8580c9e2 100644 --- a/src/Umbraco.Tests/Cache/RefresherTests.cs +++ b/src/Umbraco.Tests/Cache/RefresherTests.cs @@ -12,20 +12,22 @@ namespace Umbraco.Tests.Cache [Test] public void MediaCacheRefresherCanDeserializeJsonPayload() { - var source = new[] { new MediaCacheRefresher.JsonPayload(1234, TreeChangeTypes.None) }; + var source = new[] { new MediaCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None) }; var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); Assert.AreEqual(source[0].Id, payload[0].Id); + Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); } [Test] public void ContentCacheRefresherCanDeserializeJsonPayload() { - var source = new[] { new ContentCacheRefresher.JsonPayload(1234, TreeChangeTypes.None) }; + var source = new[] { new ContentCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None) }; var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); Assert.AreEqual(source[0].Id, payload[0].Id); + Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); } 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/TypeHelperTests.cs b/src/Umbraco.Tests/Composing/TypeHelperTests.cs index 756ca4ca15..1f2477bf98 100644 --- a/src/Umbraco.Tests/Composing/TypeHelperTests.cs +++ b/src/Umbraco.Tests/Composing/TypeHelperTests.cs @@ -165,7 +165,7 @@ namespace Umbraco.Tests.Composing Assert.IsTrue(TypeHelper.MatchType(typeof(int?), typeof(Nullable<>))); - Assert.IsTrue(TypeHelper.MatchType(typeof(Derived), typeof(Object))); + Assert.IsTrue(TypeHelper.MatchType(typeof(Derived), typeof(object))); Assert.IsFalse(TypeHelper.MatchType(typeof(Derived), typeof(List<>))); Assert.IsFalse(TypeHelper.MatchType(typeof(Derived), typeof(IEnumerable<>))); Assert.IsTrue(TypeHelper.MatchType(typeof(Derived), typeof(Base))); diff --git a/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs index 54080f05de..50ead4b702 100644 --- a/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs +++ b/src/Umbraco.Tests/Configurations/GlobalSettingsTests.cs @@ -1,6 +1,4 @@ -using System.Web.Mvc; -using System.Web.Routing; -using Moq; +using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Composing; @@ -10,6 +8,7 @@ using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.Configurations { + [TestFixture] public class GlobalSettingsTests : BaseWebTest { @@ -47,73 +46,18 @@ namespace Umbraco.Tests.Configurations [TestCase("~/some-wacky/nestedPath", "/MyVirtualDir/NestedVDir/", "some-wacky-nestedpath")] public void Umbraco_Mvc_Area(string path, string rootPath, string outcome) { - var globalSettingsMock = Mock.Get(Factory.GetInstance()); //this will modify the IGlobalSettings instance stored in the container - globalSettingsMock.Setup(x => x.Path).Returns(IOHelper.ResolveUrl(path)); + var globalSettings = SettingsForTests.GenerateMockGlobalSettings(); + + var globalSettingsMock = Mock.Get(globalSettings); + globalSettingsMock.Setup(x => x.Path).Returns(() => IOHelper.ResolveUrl(path)); SystemDirectories.Root = rootPath; - Assert.AreEqual(outcome, Current.Configs.Global().GetUmbracoMvcArea()); + Assert.AreEqual(outcome, globalSettings.GetUmbracoMvcAreaNoCache()); } - [TestCase("/umbraco/editContent.aspx")] - [TestCase("/install/default.aspx")] - [TestCase("/install/")] - [TestCase("/install")] - [TestCase("/install/?installStep=asdf")] - [TestCase("/install/test.aspx")] - public void Is_Reserved_Path_Or_Url(string url) - { - var globalSettings = TestObjects.GetGlobalSettings(); - Assert.IsTrue(globalSettings.IsReservedPathOrUrl(url)); - } - - [TestCase("/base/somebasehandler")] - [TestCase("/")] - [TestCase("/home.aspx")] - [TestCase("/umbraco-test")] - [TestCase("/install-test")] - [TestCase("/install.aspx")] - public void Is_Not_Reserved_Path_Or_Url(string url) - { - var globalSettings = TestObjects.GetGlobalSettings(); - Assert.IsFalse(globalSettings.IsReservedPathOrUrl(url)); - } + - [TestCase("/Do/Not/match", false)] - [TestCase("/Umbraco/RenderMvcs", false)] - [TestCase("/Umbraco/RenderMvc", true)] - [TestCase("/Umbraco/RenderMvc/Index", true)] - [TestCase("/Umbraco/RenderMvc/Index/1234", true)] - [TestCase("/Umbraco/RenderMvc/Index/1234/9876", false)] - [TestCase("/api", true)] - [TestCase("/api/WebApiTest", true)] - [TestCase("/api/WebApiTest/1234", true)] - [TestCase("/api/WebApiTest/Index/1234", false)] - public void Is_Reserved_By_Route(string url, bool shouldMatch) - { - //reset the app config, we only want to test routes not the hard coded paths - var globalSettingsMock = Mock.Get(Factory.GetInstance()); //this will modify the IGlobalSettings instance stored in the container - globalSettingsMock.Setup(x => x.ReservedPaths).Returns(""); - globalSettingsMock.Setup(x => x.ReservedUrls).Returns(""); - - var routes = new RouteCollection(); - - routes.MapRoute( - "Umbraco_default", - "Umbraco/RenderMvc/{action}/{id}", - new { controller = "RenderMvc", action = "Index", id = UrlParameter.Optional }); - routes.MapRoute( - "WebAPI", - "api/{controller}/{id}", - new { controller = "WebApiTestController", action = "Index", id = UrlParameter.Optional }); - - - var context = new FakeHttpContextFactory(url); - - - Assert.AreEqual( - shouldMatch, - globalSettingsMock.Object.IsReservedPathOrUrl(url, context.HttpContext, routes)); - } + } } 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]]> - - At least {{installer.current.model.minCharLength}} characters long - - - At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} - +
+
+
+
+ +
+ +
+
+ +
+ +
+ + Your email will be used as your login +
+
+ +
+ +
+ + + At least {{installer.current.model.minCharLength}} characters long + + + At least {{installer.current.model.minNonAlphaNumericLength}} symbol{{installer.current.model.minNonAlphaNumericLength > 1 ? 's' : ''}} + +
-
-
-
- -
-
+
+
+ +
+
-
-
- - Customize -
-
+
+
+ + +
+
-
-
- - + + + + diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index bd1cdd5b4f..391fafb3fa 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -77,6 +77,7 @@ @import "listview.less"; @import "gridview.less"; @import "footer.less"; +@import "filter-toggle.less"; @import "forms/umb-validation-label.less"; @@ -102,7 +103,6 @@ @import "components/tree/umb-actions.less"; @import "components/tree/umb-tree-item.less"; - @import "components/editor.less"; @import "components/overlays.less"; @import "components/card.less"; @@ -112,7 +112,7 @@ @import "components/umb-editor-navigation-item.less"; @import "components/umb-editor-sub-views.less"; @import "components/editor/subheader/umb-editor-sub-header.less"; -@import "components/umb-flatpickr.less"; +@import "components/umb-date-time-picker.less"; @import "components/umb-grid-selector.less"; @import "components/umb-child-selector.less"; @import "components/umb-group-builder.less"; @@ -124,11 +124,13 @@ @import "components/umb-form-check.less"; @import "components/umb-locked-field.less"; @import "components/umb-tabs.less"; +@import "components/umb-loader.less"; @import "components/umb-load-indicator.less"; @import "components/umb-breadcrumbs.less"; @import "components/umb-media-grid.less"; @import "components/umb-folder-grid.less"; @import "components/umb-content-grid.less"; +@import "components/umb-contextmenu.less"; @import "components/umb-layout-selector.less"; @import "components/tooltip/umb-tooltip.less"; @import "components/tooltip/umb-tooltip-list.less"; @@ -137,8 +139,10 @@ @import "components/umb-grid.less"; @import "components/umb-empty-state.less"; @import "components/umb-property-editor.less"; +@import "components/umb-property-actions.less"; @import "components/umb-color-swatches.less"; @import "components/check-circle.less"; +@import "components/umb-file-icon.less"; @import "components/umb-iconpicker.less"; @import "components/umb-insert-code-box.less"; @import "components/umb-packages.less"; @@ -186,6 +190,8 @@ @import "components/users/umb-user-preview.less"; @import "components/users/umb-user-picker-list.less"; +@import "components/contextdialogs/umb-dialog-datatype-delete.less"; + // Utilities @import "utilities/layout/_display.less"; @@ -216,6 +222,7 @@ @import "dashboards/umbraco-forms.less"; @import "dashboards/examine-management.less"; @import "dashboards/healthcheck.less"; +@import "dashboards/nucache.less"; @import "typeahead.less"; @import "hacks.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index 91a6c29a17..85532f4231 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -338,3 +338,31 @@ input[type="submit"].btn { text-decoration: none; } } + +// Icon buttons +// ------------------------------ + +// 31 July 19, Nathan Woulfe says: Reset styles for cases where button shows an icon only (eg edit/remove property on document type) +// This is lifted from umb-group-builder.less + +.btn-icon { + border: none; + + font-size: 18px; + position: relative; + cursor: pointer; + color: @ui-icon; + + margin: 0; + padding: 5px 10px; + width: auto; + overflow: visible; + background: transparent; + line-height: normal; + outline: 0; + -webkit-appearance: none; + + &:hover, &:focus { + color: @ui-icon-hover; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less index 7440a5723a..b36c73a61a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less +++ b/src/Umbraco.Web.UI.Client/src/less/canvas-designer.less @@ -163,7 +163,8 @@ a, a:hover{ background-clip: padding-box; } -.dropdown-menu > li > a { +.dropdown-menu > li > a, +.dropdown-menu > li > button { display: block; padding: 3px 20px; clear: both; @@ -174,7 +175,12 @@ a, a:hover{ cursor:pointer; } -.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-menu > li > button:hover, +.dropdown-menu > li > button:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { color: #000000; background: #e4e0dd; } @@ -266,7 +272,6 @@ a, a:hover{ ul.sections { display: block; background: #1b264f; - height: 100%; position:absolute; top: 90px; width: 80px; @@ -275,12 +280,21 @@ ul.sections { margin:0; padding:0; margin-left: -80px; + overflow: auto; + overflow-x: hidden; + height: calc(100% - 91px); + + &::-webkit-scrollbar { + width: 0px; + background: transparent; + } } ul.sections li { display: block; border-left: 4px #1b264f solid; transition: all .3s linear; + cursor: pointer; } .fix-left-menu ul.sections li a span, @@ -302,35 +316,40 @@ ul.sections li a { &:hover { span, i { opacity: 1; + color:#fff; } } } ul.sections li a i { font-size: 30px; + opacity: 0.8; } ul.sections li a span { - display:block; + display: block; font-size: 10px; line-height: 1.4em; - opacity: 0.4; + opacity: 0.8; } ul.sections li.current { - background-color: #2E2246; -} - -ul.sections li.current a i { - color: #ffffff; -} - -ul.sections li.current, ul.sections li:hover { border-left: 4px #f5c1bc solid; } -.fix-left-menu:hover ul.sections li a span, -.fix-left-menu:hover ul.sections li a i, +ul.sections li.current a i { + color: #f5c1bc; +} + +ul.sections li.current { + border-left: 4px #f5c1bc solid; +} + +ul.sections li:hover a i, +ul.sections li:hover a span { + opacity: 1; +} + .fix-left-menu:hover .help { opacity: 1; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less index 17fdcc905e..52ff2c2b01 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-dashboard.less @@ -12,7 +12,6 @@ flex: 0 0 @editorHeaderHeight; background: @white; border-bottom: 1px solid @gray-9; - padding: 20px 0 0 0; box-sizing: border-box; display: flex; justify-content: flex-end; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less index 8c3b059a94..064ad67438 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less @@ -194,7 +194,6 @@ .umb-help-list-item__title { font-size: 14px; display: block; - margin-left: 26px; } .umb-help-list-item__description { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less index 527c742382..7d91783e32 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-language-picker.less @@ -12,17 +12,20 @@ align-items: center; justify-content: space-between; padding: 0 20px; - cursor: pointer; + background: transparent; + border: 0 none; border-bottom: 1px solid @gray-9; height: @editorHeaderHeight; box-sizing: border-box; color: @ui-option-type; + width: 100%; } .umb-language-picker__expand { font-size: 14px; } +.umb-language-picker__toggle:focus, .umb-language-picker__toggle:hover { background: @ui-option-hover; color:@ui-option-type-hover; @@ -43,20 +46,24 @@ overflow: auto; } -.umb-language-picker__dropdown a { +.umb-language-picker__dropdown-item { + background: transparent; + border: 0 none; padding: 8px 20px; display: block; font-size: 14px; + width: 100%; + text-align: left; } -.umb-language-picker__dropdown a:hover, -.umb-language-picker__dropdown a:focus { +.umb-language-picker__dropdown-item:hover, +.umb-language-picker__dropdown-item:focus { background: @ui-option-hover; text-decoration: none; color:@ui-option-type-hover; } -.umb-language-picker__dropdown a.umb-language-picker__dropdown-item--current { +.umb-language-picker__dropdown .umb-language-picker__dropdown-item.umb-language-picker__dropdown-item--current { padding-left: 16px; border-left: 4px solid @ui-light-active-border; color:@ui-light-active-type; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less index 315cd91dbd..33a723a3f7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -1,8 +1,12 @@ -.umb-tour__loader { - background: @white; - z-index: @zindexTourModal; +.umb-loader-wrapper.umb-tour__loader { + margin: 0; position: fixed; - height: 5px; + z-index: @zindexTourModal; + + .umb-loader { + background-color: @white; + height: 5px; + } } .umb-tour__pulse { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less index e40282cb58..0465881387 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less @@ -9,6 +9,7 @@ left: auto; } +.umb-button-group__sub-buttons>li>button, .umb-button-group__sub-buttons>li>a { display: flex; } @@ -20,7 +21,7 @@ } .umb-button-group__toggle { - border-radius: 0px @baseBorderRadius @baseBorderRadius 0; + border-radius: 0 @baseBorderRadius @baseBorderRadius 0; border-left: 1px solid rgba(0,0,0,0.09); margin-left: -2px; padding-left: 10px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/contextdialogs/umb-dialog-datatype-delete.less b/src/Umbraco.Web.UI.Client/src/less/components/contextdialogs/umb-dialog-datatype-delete.less new file mode 100644 index 0000000000..0e0b8f22bd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/contextdialogs/umb-dialog-datatype-delete.less @@ -0,0 +1,33 @@ +.umb-dialog-datatype-delete { + + + .umb-dialog-datatype-delete__table-head-column-name { + width: 140px; + } + + .umb-table-body__icon { + margin-right: 5px; + vertical-align: top; + display: inline-block; + } + + .table tbody td { + vertical-align: top; + } + .table tbody td > span { + margin: 5px 0; + vertical-align: middle; + } + .table tbody p { + line-height: 12px; + margin: 5px 0; + vertical-align: middle; + } + + .table tbody .icon { + vertical-align: top; + margin-right: 5px; + display: inline-block; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index 52dd7ea678..85fcc249f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -74,8 +74,11 @@ } .umb-editor-header__back { - color: @gray-6; - margin-bottom: 1px; + background: transparent; + border: 0; + color: @gray-6; + margin: 0 0 1px 0; + padding: 0; transition: color 0.1s ease-in-out; } @@ -373,14 +376,3 @@ a.umb-variant-switcher__toggle { margin-right: auto; padding-right: 10px; } - -/* Confirm */ -.umb-editor-confirm { - background-color: @white; - padding: 20px; - position: absolute; - left: 0; - bottom: 0; - z-index: 10; - box-shadow: 0 -3px 12px 0px rgba(0,0,0,0.16); -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 6cf3598638..44cd86a189 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -1,14 +1,11 @@ .umb-editor-sub-header { padding: 10px 0; - margin-bottom: 10px; background: @brownGrayLight; border-left: 5px solid @brownGrayLight; border-right: 5px solid @brownGrayLight; - margin-left: -5px; - margin-right: -5px; display: flex; justify-content: space-between; - margin-top: -10px; + margin: -10px -5px 10px; position: relative; top: 0; box-sizing: border-box; @@ -34,30 +31,25 @@ [umb-sticky-bar] { transition: box-shadow 240ms; - margin-top: 0; - margin-bottom: 0; position:sticky; - z-index: 99; + z-index: 30; &.umb-sticky-bar--active { box-shadow: 0 6px 3px -3px rgba(0,0,0,.16); } + + .umb-dashboard__content & { + top:-20px; // umb-dashboard__content has 20px padding - offset here prevents sticky position from firing when page loads + } } .umb-sticky-sentinel { - position: absolute; - left: 0; - width: 100%; pointer-events: none; + z-index: 5050; &.-top { - top:0px; height:1px; - } - - &.-bottom { - bottom:50px; - height:10px; + transform:translateY(-10px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less index a9d879ab7f..6859280680 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-editor.less @@ -1,82 +1,86 @@ .umb-editors { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - overflow: hidden; + .absolute(); + overflow: hidden; + + .umb-editor { + box-shadow: 0px 0 30px 0 rgba(0,0,0,.3); + } } .umb-editor { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + .absolute(); background: @brownGrayLight; z-index: @zIndexEditor; -} -.umb-editor--infinityMode { - transform: none; - will-change: transform; - transition: transform 400ms ease-in-out; - &.moveRight { - transform: translateX(110%); + &--infiniteMode { + transform: none; + will-change: transform; + transition: transform 400ms ease-in-out; + + &.umb-editor--moveRight { + transform: translateX(110%); + } } -} -.umb-editor--outOfRange { - transform: none; - display: none; - will-change: auto; - transition: display 0s 320ms; -} -.umb-editor--level0 { - transform: none; -} -.umb-editor--level1 { - transform: translateX(60px); -} -.umb-editor--level2 { - transform: translateX(120px); -} -.umb-editor--level3 { - transform: translateX(180px); -} - -.umb-editor--n1 { - right:60px; -} -.umb-editor--n2 { - right:120px; -} -.umb-editor--n3 { - right:180px; -} - -// hide all infinite editors by default -// will be shown through animation -.umb-editors .umb-editor { - box-shadow: 0px 0 30px 0 rgba(0,0,0,.3); -} - -.umb-editor--small { - width: 500px; - will-change: transform; - left: auto; + &--outOfRange { + transform: none; + display: none; + will-change: auto; + transition: display 0s 320ms; + } - .umb-editor-container { - max-width: 500px; + &--level0 { + transform: none; } } +// use a loop to build the editor levels +@iterations: 3; +@step: 60px; + +.level-loop (@i) when (@i > 0) { + @x: @i * @step; + .umb-editor--level@{i} { + transform: translateX(@x); + } + + .umb-editor--n@{i} { + right:@x; + } + + .level-loop(@i - 1); +} + +.level-loop(@iterations); + +// and also use a loop to build editor sizes - easily extended with new sizes by adding to the map +@editorSizes: + small 500px, + medium 800px; + +.create-editor-sizes(@iterator:1) when(@iterator <= length(@editorSizes)) { + .umb-editor { + @size: extract(extract(@editorSizes, @iterator), 1); + @value: extract(extract(@editorSizes, @iterator), 2); + + &--@{size} { + width: @value; + will-change: transform; + left: auto; + + .umb-editor--container { + max-width: @value; + } + } + } + + .create-editor-sizes(@iterator + 1); +} + +.create-editor-sizes(); + .umb-editor__overlay { - position: absolute; - top: 0; - bottom: 0; - right: 0; - left: 0; + .absolute(); background: rgba(0,0,0,0.4); z-index: @zIndexEditor; visibility: hidden; @@ -85,7 +89,7 @@ } #contentcolumn > .umb-editor__overlay, -.--notInFront .umb-editor__overlay { +.umb-editor--notInFront .umb-editor__overlay { visibility: visible; opacity: 1; transition: opacity 320ms 20ms, visibility 0s; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less index 7f04fef9a9..67038380ca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/notifications/umb-notifications.less @@ -1,5 +1,5 @@ .umb-notifications { - z-index: 1000; + z-index: 1100; position: absolute; bottom: @editorFooterHeight; left: 0; @@ -14,7 +14,7 @@ list-style: none; margin: 0; position: relative; -} +} .umb-notifications__notification { padding: 5px 20px; @@ -24,11 +24,14 @@ position: relative; border-radius: 10px; margin: 10px; - + .close { + position: absolute; top: 0; - right: -6px; + bottom: 0; + right: 6px; opacity: 0.4; + margin: auto 0; } } @@ -36,3 +39,8 @@ padding-top: 20px; padding-bottom: 20px; } + + +.emptySection .umb-notifications{ + left:0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index c8f0195ea5..609cf0af3d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -6,6 +6,10 @@ animation: fadeIn 0.2s; box-shadow: 0 10px 50px rgba(0,0,0,0.1), 0 6px 20px rgba(0,0,0,0.16); text-align: left; + + .scoped-view{ + display: none; + } } .umb-overlay__form { @@ -16,23 +20,22 @@ } .umb-overlay .umb-overlay-header { - border-bottom: 1px solid @purple-l3; + border-bottom: 1px solid @gray-9; margin-top: 0; flex-grow: 0; flex-shrink: 0; - - padding: 20px 30px 0; + padding: 20px 20px 0; } .umb-overlay__section-header { width: 100%; margin-top:30px; margin-bottom: 10px; - + h5 { display: inline; } - + button { display: inline; float: right; @@ -45,10 +48,11 @@ } .umb-overlay__title { - font-size: @fontSizeLarge; + font-size: 16px; color: @black; + line-height: 16px; font-weight: bold; - margin: 7px 0; + margin: 5px 0; } .umb-overlay__subtitle { @@ -62,9 +66,8 @@ flex-shrink: 1; flex-basis: auto; position: relative; - - padding: 0px 30px; - margin-bottom: 10px; + padding: 20px; + background: @white; max-height: calc(100vh - 170px); overflow-y: auto; } @@ -72,12 +75,11 @@ .umb-overlay-drawer { flex-grow: 0; flex-shrink: 0; - flex-basis: 31px; - padding: 10px 20px; + flex-basis: 33px; + padding: 8px 20px; margin: 0; - - background: @gray-10; - border-top: 1px solid @purple-l3; + background: @white; + border-top: 1px solid @gray-9; } .umb-overlay-drawer.-auto-height { @@ -115,7 +117,7 @@ .umb-overlay.umb-overlay-center .umb-overlay-drawer { border: none; background: transparent; - padding: 0 30px 20px; + padding: 0 20px 20px; } /* ---------- OVERLAY TARGET ---------- */ @@ -124,11 +126,7 @@ max-height: 100vh; box-sizing: border-box; border-radius: @baseBorderRadius; - /* default: - &.umb-overlay--small { - width: 400px; - } - */ + &.umb-overlay--medium { width: 480px; } @@ -142,7 +140,6 @@ .umb-overlay.umb-overlay-target .umb-overlay-drawer { border: none; background: transparent; - padding: 0 30px 20px; } /* ---------- OVERLAY RIGHT ---------- */ @@ -153,10 +150,14 @@ bottom: 0; border: none; box-shadow: 0 0 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); + + .umb-drawer-is-visible & { + right:400px; + } } .umb-overlay.umb-overlay-right .umb-overlay-header { - flex-basis: 100px; + flex-basis: 70px; box-sizing: border-box; } @@ -228,12 +229,12 @@ } .umb-overlay__item-details-title { - margin-top: 0; - margin-bottom: 0; + margin: 0; + font-size: 15px; } .umb-overlay__item-details-description { - margin-top: 10px; + margin: 10px 0 0; font-size: 12px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less index d6e792de73..0c231830de 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-actions.less @@ -94,6 +94,7 @@ font-size: 14px; color: @black; margin-left: 10px; + text-align: left; } small { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less index 8f0b55f9ed..8945d15ec6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree-item.less @@ -39,17 +39,10 @@ // Loading Animation // ------------------------ - .l { + .umb-tree-item__loader { width: 100%; - height: 2px; - overflow: hidden; position: absolute; - left: 0; - bottom: -1px; - - div { - .umb-loader; - } + margin: 0; } .umb-tree-item__label { @@ -57,19 +50,20 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - flex: 1 0 auto; + flex: 1 1 auto; } } // active is equivilant to selected, its the item that is begin affected by the actions performed in the right-click-dialog. .umb-tree-item.active > .umb-tree-item__inner { + border-color: @ui-selected-border; + box-shadow: 0 0 2px 0 fade(@ui-selected-border, 80%); color: @ui-selected-type; + a { color: @ui-selected-type; } - - border-color: @ui-selected-border; - box-shadow: 0 0 2px 0 fade(@ui-selected-border, 80%); + &::before { content: ""; position: absolute; @@ -79,8 +73,10 @@ bottom: 0; border: 2px solid fade(white, 80%); } + &:hover { color: @ui-selected-type-hover; + a { color: @ui-selected-type-hover; } @@ -88,7 +84,6 @@ } .umb-tree-item.current > .umb-tree-item__inner { - background: @ui-active; color:@ui-active-type; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less index 5c54232200..0a0fb29eed 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/tree/umb-tree.less @@ -33,12 +33,6 @@ display: none; } } - - //loader defaults - .umb-loader { - height: 10px; - margin: 10px 10px 10px 10px; - } .search-subtitle { color: @gray-7; @@ -72,9 +66,7 @@ body.touch .umb-tree { overflow: hidden; display: flex; flex-wrap: nowrap; - align-items: center; - - border:2px solid transparent; + align-items: center; color: @ui-option-type; a { @@ -105,6 +97,10 @@ body.touch .umb-tree { } } +.umb-tree-item__inner { + border: 2px solid transparent; +} + .umb-tree-header { display: flex; padding: 20px 0 20px 20px; @@ -183,7 +179,6 @@ body.touch .umb-tree { &:hover { background: @btnBackgroundHighlight; } - // NOTE - We're having to repeat ourselves here due to an .sr-only class appearing in umbraco/lib/font-awesome/css/font-awesome.min.css &.sr-only--hoverable:hover, &.sr-only--focusable:focus { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less index c0e91e28c2..1fe59ab9dd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-box.less @@ -1,12 +1,14 @@ +@boxUnit: 10px; + .umb-box { background: @white; border-radius: 3px; - margin-bottom: 20px; + margin-bottom: @boxUnit * 2; box-shadow: 0 1px 1px 0 rgba(0,0,0,.16); } .umb-box-header { - padding: 10px 20px; + padding: @boxUnit @boxUnit * 2; border-bottom: 1px solid @gray-9; display: flex; align-items: center; @@ -27,5 +29,19 @@ } .umb-box-content { - padding: 20px; + padding: @boxUnit * 2; } + +// allow side-by-side boxes +.umb-box-row { + margin-left:-@boxUnit; + margin-right:-@boxUnit; + display:flex; + justify-content: space-around; + + .umb-box { + margin-left:@boxUnit; + margin-right:@boxUnit; + flex:1; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index ba04069c6b..3c63d74a47 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -8,6 +8,14 @@ .umb-breadcrumbs__ancestor { display: flex; + min-height: 25px; +} + +.umb-breadcrumbs__action { + background: transparent; + border: 0 none; + padding: 0; + margin-top: -4px; } .umb-breadcrumbs__ancestor-link, @@ -38,7 +46,6 @@ input.umb-breadcrumbs__add-ancestor { height: 25px; - margin-top: -2px; - margin-left: 3px; + margin: 0 0 0 3px; width: 100px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less index 985e57bea5..11194eeb43 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-checkbox-list.less @@ -4,6 +4,7 @@ .umb-checkbox-list { list-style: none; margin-left: 0; + margin-top: 6px; } .umb-checkbox-list__item { @@ -12,6 +13,10 @@ margin-bottom: 2px; } +.umb-checkbox-list li:first-child { + font-weight: bold; +} + .umb-checkbox-list__item:last-child { border-bottom: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less index 84cfe04263..f27e1e4ec8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-content-grid.less @@ -105,6 +105,7 @@ .umb-content-grid__details-value { display: inline-block; word-break: break-word; + margin-left: 3px; } .umb-content-grid__checkmark { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-contextmenu.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-contextmenu.less new file mode 100644 index 0000000000..8512e2020d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-contextmenu.less @@ -0,0 +1,75 @@ +.umb-contextmenu { + margin: 0; + list-style: none; + user-select: none; + + overflow: hidden; + border-radius: 3px; + border: 1px solid @dropdownBorder; + .box-shadow(0 5px 20px rgba(0,0,0,.3)); + border-bottom: 1px solid rgba(0,0,0,.2); + + .sep { + display: block; + border-top: 1px solid @gray-9; + + &:first-child { + border-top: none; + } + } + +} + +.umb-contextmenu-item { + + .icon { + font-size: 18px; + vertical-align: middle; + } + + .menu-label { + display: inline-block; + vertical-align: middle; + margin-left: 5px; + } + + button { + + position: relative; + + display: block; + font-weight: normal; + line-height: @baseLineHeight; + white-space: nowrap; + + background-color: @ui-option; + border: 0; + padding: 7px 12px; + color: @ui-option-type; + width: 100%; + + font-size: 14px; + text-align: left; + + &:hover { + text-decoration: none; + color: @ui-option-type-hover; + background-color: @ui-option-hover; + } + } + + &.-opens-dialog { + .menu-label:after { + // adds an ellipsis (...) after the menu label for actions that open a dialog + content: '\2026'; + } + } + button:disabled { + cursor: not-allowed; + color: @ui-option-disabled-type; + &:hover { + color: @ui-option-disabled-type-hover; + background-color: @ui-option; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less similarity index 82% rename from src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less rename to src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less index 8cdcc8b877..5bcfdd1c71 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-flatpickr.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-date-time-picker.less @@ -1,4 +1,4 @@ -.flatpickr-calendar.flatpickr-calendar { +.flatpickr-calendar { border-radius: @baseBorderRadius; box-shadow: 0 5px 10px 0 rgba(0,0,0,0.16); } @@ -6,6 +6,10 @@ span.flatpickr-day { border-radius: @baseBorderRadius; border: none; + + &.today:not(.active) { + border: 1px solid; + } } span.flatpickr-day:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index e6b3fdbfa9..c26c89a478 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -1,172 +1,195 @@ .umb-sub-views-nav-item { position: relative; display: block; -} -.umb-sub-views-nav-item > a { - text-align: center; - cursor: pointer; - display: block; - padding: 4px 10px 0 10px; - min-width: 70px; - border-right: 1px solid @gray-9; - box-sizing: border-box; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: calc(~'@{editorHeaderHeight}' - ~'1px'); // need to offset the 1px border-bottom on .umb-editor-header - avoids overflowing top of the container - position: relative; - - color: @ui-active-type; - - &:hover { - color: @ui-active-type-hover !important; - } - - &::after { - content: ""; - height: 0px; - left: 8px; - right: 8px; - background-color: @ui-light-active-border; - position: absolute; - bottom: 0; - border-radius: 3px 3px 0 0; - opacity: 0; - transition: all .2s linear; - } -} -.umb-sub-views-nav-item > a:active { - .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); -} -.umb-sub-views-nav-item > a:focus { - outline: none; -} + &__action, + > a { + background: transparent; + text-align: center; + cursor: pointer; + display: block; + padding: 4px 10px 0 10px; + min-width: 70px; + border: 0 none; + border-right: 1px solid @gray-9; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: calc(~'@{editorHeaderHeight}'- ~'1px'); // need to offset the 1px border-bottom on .umb-editor-header - avoids overflowing top of the container + position: relative; + color: @ui-active-type; -.umb-sub-views-nav-item > a:hover, -.umb-sub-views-nav-item > a:focus { - text-decoration: none; -} + &:focus, + &:hover { + color: @ui-active-type-hover !important; + text-decoration: none; + } -.umb-sub-views-nav-item > a.is-active { - - color: @ui-light-active-type; - - &::after { + &:focus { + outline: none; + } + + &::after { + content: ""; + height: 0px; + left: 8px; + right: 8px; + background-color: @ui-light-active-border; + position: absolute; + bottom: 0; + border-radius: 3px 3px 0 0; + opacity: 0; + transition: all .2s linear; + } + + &.is-active { + color: @ui-light-active-type; + + &::after { + opacity: 1; + height: 4px; + } + } + } + + &__action:focus, + &__action:active, + & > a:active { + .box-shadow(~"inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05)"); + } + + &:focus-within &__anchor_dropdown, + &:hover &__anchor_dropdown { + visibility: visible; opacity: 1; - height: 4px; + transition: opacity 20ms 0; + } + + .icon { + font-size: 24px; + display: block; + text-align: center; + margin-bottom: 7px; + } + + .badge { + position: absolute; + top: 6px; + right: 6px; + min-width: 16px; + color: @white; + background-color: @ui-active-type; + border: 2px solid @white; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + padding: 2px; + line-height: 16px; + display: block; + + &.-type-alert { + background-color: @red; + } + + &.-type-warning { + background-color: @yellow-d2; + } + + &:empty { + height: 12px; + min-width: 12px; + } + } + + &-text { + font-size: 12px; + line-height: 1em; + } + + &__anchor_dropdown { + // inherits from .dropdown-menu + margin: 0; + overflow: hidden; + + // center align horizontal + left: 50%; + transform: translateX(-50%); + opacity: 0; + transition: opacity 250ms 250ms; + visibility: hidden; + + li { + &.is-active a { + border-left-color: @ui-selected-border; + } + + a { + border-left: 4px solid transparent; + } + } + } + + // Currently Edge 18 does unfortunately not support :focus-within so for now we will use the "old" behavior - Support is coming with the upcoming release of Edge 76 + // See https://caniuse.com/#search=focus-within + @supports (-ms-ime-align:auto) { + &:hover &__anchor_dropdown { + transition: visibility 0 0, opacity 20ms 0; + } + + &__anchor_dropdown { + visibility: hidden; + transition: visibility 0 500ms, opacity 250ms 250ms; + } + } + + // -------------------------------- + // item__more, appears when there is not enough room for the visible items. + // -------------------------------- + + &-more__icon { + margin-bottom: 10px; + + i { + height: 5px; + width: 5px; + border-radius: 50%; + background: @ui-active-type; // fallback if browser doesnt support currentColor + background: currentColor; + display: inline-block; + margin: 0 5px 0 0; + } + + i:last-of-type { + margin-right: 0; + } + } + + &-more__dropdown { + left: auto; + right: 0; + display: grid; + grid-template-columns: 1fr 1fr 1fr; + min-width: auto; + margin-top: 10px; + + > li { + display: flex; + } + + .umb-sub-views-nav-item:first { + border-left: none; + } } } +// Validation +.umb-sub-views-nav-item__action.-has-error, .show-validation .umb-sub-views-nav-item > a.-has-error { - color: @red; - &::after { - background-color: @red; - } -} + color: @red; -.umb-sub-views-nav-item .icon { - font-size: 24px; - display: block; - text-align: center; - margin-bottom: 7px; -} - -.umb-sub-views-nav-item .badge { - position: absolute; - top: 6px; - right: 6px; - min-width: 16px; - color: @white; - background-color: @ui-active-type; - border: 2px solid @white; - border-radius: 50%; - font-size: 10px; - font-weight: bold; - padding: 2px; - line-height: 16px; - display: block; - - &.-type-alert { - background-color: @red; - } - &.-type-warning { - background-color: @yellow-d2; - } - &:empty { - height: 12px; - min-width: 12px; - } -} - -.umb-sub-views-nav-item-text { - font-size: 12px; - line-height: 1em; -} - - -.umb-sub-views-nav-item__anchor_dropdown {// inherits from .dropdown-menu - display: block; - margin: 0; - overflow: hidden; - - // center align horizontal - left: 50%; - transform: translateX(-50%); - - visibility:hidden; - opacity: 0; - transition: visibility 0s 500ms, opacity 250ms 250ms; -} -.umb-sub-views-nav-item__anchor_dropdown li a { - border-left: 4px solid transparent; -} -.umb-sub-views-nav-item__anchor_dropdown li.is-active a { - border-left-color: @ui-selected-border; -} - -.umb-sub-views-nav-item:hover .umb-sub-views-nav-item__anchor_dropdown { - visibility:visible; - opacity: 1; - transition: visibility 0s 0s, opacity 20ms 0s; -} - - - -// -------------------------------- -// item__more, appears when there is not enough room for the visible items. -// -------------------------------- - -.umb-sub-views-nav-item-more__icon { - margin-bottom: 10px; -} - -.umb-sub-views-nav-item-more__icon i { - height: 5px; - width: 5px; - border-radius: 50%; - background: @ui-active-type;// fallback if browser doesnt support currentColor - background: currentColor; - display: inline-block; - margin: 0 5px 0 0; -} - -.umb-sub-views-nav-item-more__icon i:last-of-type { - margin-right: 0; -} - -.umb-sub-views-nav-item-more__dropdown { - left: auto; - right: 0; - display: grid; - grid-template-columns: 1fr 1fr 1fr; - min-width: auto; - margin-top: 10px; -} -.umb-sub-views-nav-item-more__dropdown > li { - display: flex; -} -.umb-sub-views-nav-item-more__dropdown .umb-sub-views-nav-item:first { - border-left: none; -} + &::after { + background-color: @red; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less index 5071091fcc..b5d8c3cced 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-dropzone.less @@ -14,15 +14,18 @@ margin: 0 0 20px 0; position: relative; transition: height 0.8s; + .illustration { width: 300px; } + &.is-small { height: 100px; .illustration { width: 200px; } } + &.drag-over { border: 1px dashed @gray-1; } @@ -35,15 +38,19 @@ top: 50%; left: 50%; transform: translate(-50%,-50%); + display: flex; + flex-direction: column; } // file select link .file-select { + background: transparent; + border: 0; + padding: 0; font-size: 15px; color: @ui-action-discreet-type; cursor: pointer; - margin-top: 10px; &:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less new file mode 100644 index 0000000000..febee80a97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less @@ -0,0 +1,60 @@ +.umb-file-icon { + text-align: center; + display: flex; + flex-direction: column; + align-items: center; + + .file-icon { + display: flex; + flex-direction: column; + align-items: flex-start; + position: relative; + + > .icon { + font-size: 50px; + line-height: 100%; + color: @gray-4; + display: block; + text-align: center; + } + + > span { + position: absolute; + color: @white; + background: @ui-active; + padding: 1px 3px; + font-size: 10px; + line-height: 130%; + display: block; + margin-bottom: 0.75rem; + min-width: 1.2rem; + bottom: 0; + } + + & + small { + display: block; + margin-top: 0.25rem; + } + } +} + +.umb-file-icon--s { + .file-icon { + > .icon { + + } + } +} + +.umb-file-icon--m { + .file-icon { + > .icon { + font-size: 70px; + } + > span { + font-size: 14px; + margin-bottom: 0.95rem; + min-width: 1.5rem; + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index deb573920f..7f19c4933c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -12,10 +12,18 @@ line-height: 22px; cursor: pointer !important; + &.-small-text{ + font-size: 13px; + } + + &.-bold{ + font-weight: 700; + } + &__text { margin: 0 0 0 26px; position: relative; - top: 0; + top: 1px; user-select: none; } @@ -24,7 +32,7 @@ top: 0; left: 0; opacity: 0; - + &:hover ~ .umb-form-check__state .umb-form-check__check { border-color: @inputBorderFocus; } @@ -36,7 +44,7 @@ background: @ui-option-type-hover; } } - + &:checked ~ .umb-form-check__state { .umb-form-check__check { // This only happens if the state has a radiobutton modifier @@ -62,8 +70,7 @@ } } } - - + .tabbing-active &.umb-form-check--radiobutton &__input:focus ~ .umb-form-check__state .umb-form-check__check { //outline: 2px solid @inputBorderTabFocus; border: 2px solid @inputBorderTabFocus; @@ -76,6 +83,11 @@ border-color: white; } + // add spacing between when flexed/inline, equal to the width of the input + .flex & + & { + margin-left:@checkboxWidth; + } + &__state { display: flex; height: 18px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 2eafe9b3d7..277c2bcbe8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -408,7 +408,19 @@ margin-bottom: 10px; } +.umb-grid .umb-editor-preview { + position: relative; + .umb-editor-preview-overlay { + cursor: pointer; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1; + } +} // Active states // ------------------------- @@ -625,9 +637,12 @@ } .umb-grid .mce-toolbar { - border-bottom: 1px solid @gray-8; - background-color: rgba(250, 250, 250, 1); + border-bottom: 1px solid @gray-7; + background-color: white; display: none; + + left: 0; + right: 0; } .umb-grid .umb-control.-active .mce-toolbar { @@ -642,6 +657,13 @@ overflow-y: hidden!important; } +// had to overwrite defaults from TinyMCE, needed for buttons panel to float to new line in narrow space. +.umb-grid .mce-container > div { + white-space: normal; + left:0; + right:0; +} + // MEDIA EDITOR // ------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index 4c23aef5f0..c51fd37fe4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -367,7 +367,6 @@ input.umb-group-builder__group-title-input:disabled:hover { overflow: visible; background: transparent; line-height: normal; - outline: 0; -webkit-appearance: none; &:hover, &:focus { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less index cf407b667f..cdc6cfcb63 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-layout-selector.less @@ -4,6 +4,7 @@ } .umb-layout-selector__active-layout { + background: transparent; box-sizing: border-box; border: 1px solid @inputBorder; cursor: pointer; @@ -33,6 +34,7 @@ } .umb-layout-selector__dropdown-item { + background: transparent; padding: 5px; margin: 3px 5px; display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less index 733836d85f..f6dfed63c1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list-view-settings.less @@ -1,54 +1,76 @@ .umb-list-view-settings__box { - background: @gray-10; - border: 1px solid @gray-7; - display: flex; - animation: fadeIn 0.5s; - padding: 15px; - position: relative; + background: @gray-10; + display: flex; + flex: 1; + padding: 15px; + position: relative; + border-radius: @baseBorderRadius; + + .btn-link { + font-size: 13px; + padding: 0; + } } .umb-list-view-settings__trigger { - margin-bottom: 20px; -} - -.umb-list-view-settings__box.-open { - border-bottom: transparent; -} - -.umb-list-view-settings__content { - display: flex; + margin-bottom: 20px; } .umb-list-view-settings__list-view-icon { - font-size: 20px; - color: @gray-7; - margin-right: 10px; + font-size: 20px; + color: @gray-7; + margin-right: 10px; } .umb-list-view-settings__name { - margin-right: 5px; - font-size: 14px; - font-weight: bold; - float: left; + font-size: 14px; + font-weight: bold; } .umb-list-view-settings__create-new { - font-size: 13px; - color: @ui-action-type; -} - -.umb-list-view-settings__create-new:hover { - color: @ui-action-type-hover; - border-color: @ui-action-type-hover; + color: @ui-action-type; } .umb-list-view-settings__remove-new { - font-size: 13px; - color: @red; + color: @red; } -.umb-list-view-settings__settings { - border: 1px dashed @gray-7; - border-top: none; - padding: 20px; +// display `columns displayed` table as a list-view layout +.umb-list-view-settings__overlay { + + .btn { + vertical-align: top; + } + + .btn-icon { + padding: 0; + } + + table { + width: 100%; + } + + tbody tr { + background: @gray-10; + border-bottom: 1px solid #fff; + } + + th { + text-align: left; + } + + td { + padding: 10px 15px 10px 0; + + &:first-child { + padding-left: 15px; + } + + .ui-sortable-handle { + min-height: 37px; + display: flex; + width:0; + align-items: center; + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less index 12d7085b0a..94cfa6f62c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-list.less @@ -36,6 +36,10 @@ a.umb-list-item:focus { color: @gray-4; } +.umb-list-item__description--checkbox{ + margin: 0 0 0 26px; +} + .umb-list-checkbox { position: absolute; opacity: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less new file mode 100644 index 0000000000..260710ce72 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-loader.less @@ -0,0 +1,42 @@ +// Loading Animation +// ------------------------ + +.umb-loader { + background-color: @blue; + margin-top: 0; + margin-left: -100%; + animation-name: bounce_loadingProgressG; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; + width: 100%; + height: 2px; +} + +@keyframes bounce_loadingProgressG { + 0% { + margin-left: -100%; + } + + 100% { + margin-left: 100%; + } +} + +.umb-loader-wrapper { + position: absolute; + right: 0; + left: 0; + margin: 10px 0; + overflow: hidden; +} + +.umb-loader-wrapper.-top { + top: 0; + bottom: auto; +} + +.umb-loader-wrapper.-bottom { + top: auto; + bottom: 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less index 8d9ae86ce7..2b49348190 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less @@ -6,13 +6,26 @@ } .umb-locked-field__wrapper { - display: flex; - align-items: center; - margin-bottom: 5px; + display: flex; + align-items: center; + margin-bottom: 5px; } .umb-locked-field__toggle { margin-right: 3px; + padding: 0; + background: none; + border: 0; + font-size: inherit; + line-height: inherit; + + &:focus { + outline: none; + + .tabbing-active & { + outline: 2px solid @inputBorderTabFocus; + } + } } .umb-locked-field__toggle:hover, @@ -29,7 +42,8 @@ color: @gray-3; } -input.umb-locked-field__input { +input.umb-locked-field__input, +.umb-locked-field__text { background: rgba(255, 255, 255, 0); // if using transparent it will hide the text in safari border-color: transparent !important; font-size: 13px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less index 7b8845542e..76223589e4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less @@ -1,5 +1,3 @@ -/* PACKAGE DETAILS */ - .umb-logviewer { display: flex; flex-flow: row wrap; @@ -41,7 +39,7 @@ flex: 1 1 auto; width: 100%; margin-bottom: 30px; - margin-right: 0; + margin-right: 0; } .umb-logviewer__sidebar { @@ -49,3 +47,99 @@ width: 100%; } } + +.umb-logviewer-search { + .filter-name { + margin-left: 5px; + margin-right: 3px; + max-width: 150px; + } + + .dropdown-item { + padding: 8px 20px 8px 16px; + } + + .filter { + position: relative; + + a.btn-link { + padding-left: 0px; + } + } + + .search-box { + width: 100%; + + .flex-auto { + position: relative; + + .search-input { + width: 100%; + padding-right: 160px; + } + + .icon-rate { + position: absolute; + top: 0; + line-height: 32px; + right: 140px; + color: #fdb45c; + cursor: pointer; + } + + .icon-wrong { + position: absolute; + top: 0; + line-height: 32px; + right: 120px; + color: #bbbabf; + cursor: pointer; + } + + .umb-variant-switcher__toggle { + top: 1px; + right: 0; + position: absolute; + + .icon-navigation-down { + margin-top: 0; + } + } + + .saved-searches { + width: 100%; + max-height: 250px; + overflow-y: scroll; + margin-top: -10px; + } + } + } + + .log-items { + .table { + table-layout: fixed; + + thead th:first-child, thead th:nth-child(3) { + width: 20%; + } + + thead th:nth-child(2) { + width: 15%; + } + + tr td:nth-child(3) { + word-break: break-word; + } + } + + .exception { + border-left: 4px solid #D42054; + padding: 0 10px 10px 10px; + box-shadow: rgba(0,0,0,0.07) 2px 2px 10px; + + .exception-message { + white-space: pre-wrap; + } + } + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less index 68973b4c7c..50244c2079 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-media-grid.less @@ -21,18 +21,29 @@ margin: 10px; position: relative; - //overflow: hidden; - user-select: none; - - cursor: pointer; - box-shadow: 0 1px 1px 0 rgba(0,0,0,.2); - //border: 2px solid transparent; - transition: box-shadow 150ms ease-in-out; } +.umb-media-grid__item.-unselectable { + &::before { + content: ""; + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(230, 230, 230, .5); + pointer-events: none; + } +} + +.umb-media-grid__item.-selectable { + cursor: pointer; +} + .umb-media-grid__item.-file { background-color: @white; } @@ -43,7 +54,8 @@ color: @ui-selected-type; } } -.umb-media-grid__item.-selected, .umb-media-grid__item:hover { +.umb-media-grid__item.-selected, +.umb-media-grid__item.-selectable:hover { &::before { content: ""; position: absolute; @@ -58,7 +70,7 @@ pointer-events: none; } } -.umb-media-grid__item:hover { +.umb-media-grid__item.-selectable:hover { &::before { opacity: .33; } @@ -69,16 +81,11 @@ } } -.umb-media-grid__item-file-icon > span { - color: @white; - background: @gray-4; - padding: 1px 3px; - font-size: 10px; - line-height: 130%; - display: block; - margin-top: -30px; - margin-left: -10px; - position: relative; +.umb-media-grid__item-file-icon { + transform: translate(-50%,-50%); + position: absolute; + top: 45%; + left: 50%; } .umb-media-grid__item:hover { @@ -86,34 +93,27 @@ } .umb-media-grid__item-image { - //max-width: 100% !important; - //height: auto; position: relative; - object-fit: contain; height: 100%; } .umb-media-grid__item-image-placeholder { width: 100%; - //max-width: 100%; - //height: auto; position: relative; - object-fit: contain; height: 100%; } .umb-media-grid__image-background { content: ""; - background: url("../img/checkered-background.png"); - background-repeat: repeat; opacity: 0.5; top: 0; left: 0; bottom: 0; right: 0; position: absolute; + .checkeredBackground(); } .umb-media-grid__item-overlay { @@ -139,19 +139,6 @@ } } -/* -.umb-media-grid__item.-file .umb-media-grid__item-overlay { - opacity: 1; - color: @gray-4; - background: @white; -} - -.umb-media-grid__item.-file:hover .umb-media-grid__item-overlay, -.umb-media-grid__item.-file.-selected .umb-media-grid__item-overlay { - color: @white; - background: @blueExtraDark; -} -*/ .umb-media-grid__info { margin-right: 5px; } @@ -167,39 +154,17 @@ } } +.umb-media-grid__item-name { + cursor: pointer; +} + .umb-media-grid__item-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.umb-media-grid__item-icon { - color: @gray-4; - position: absolute; - top: 45%; - left: 50%; - font-size: 40px !important; - transform: translate(-50%,-50%); -} -/* -.umb-media-grid__checkmark { - position: absolute; - z-index: 2; - top: 10px; - left: 10px; - width: 26px; - height: 26px; - border: 2px solid @white; - background: @green; - border-radius: 50px; - box-sizing: border-box; - display: flex; - justify-content: center; - align-items: center; - color: @white; - transition: background 100ms; -} -*/ + .umb-media-grid__edit { position: absolute; opacity: 0; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 59c90972d2..455a147395 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -1,5 +1,4 @@ .umb-nested-content { - text-align: center; position: relative; } @@ -154,6 +153,11 @@ .umb-nested-content__icon--disabled { opacity: 0.3; + cursor: default !important; + + &:hover { + color: @ui-option-type; + } } @@ -165,6 +169,7 @@ .umb-nested-content__add-content { display: flex; + width: 100%; align-items: center; justify-content: center; border: 1px dashed @ui-action-discreet-border; @@ -229,8 +234,46 @@ margin-left: 10px; } -.form-horizontal .umb-nested-content--narrow .controls-row -{ +.umb-nested-content__placeholder { + height: 22px; + padding: 4px 6px; + border: 1px dashed #d8d7d9; + background: 0 0; + cursor: pointer; + color: #1b264f; + -webkit-animation: fadeIn .5s; + animation: fadeIn .5s; + text-align: center; + + &--selected { + border: 1px solid #d8d7d9; + text-align: left; + } +} + +.umb-nested-content__placeholder-name{ + font-size: 15px; +} + +.umb-nested-content__placeholder:hover { + color: #2152a3; + border-color: #2152a3; + text-decoration: none; +} + +.umb-nested-content__placeholder-icon-holder { + width: 20px; + text-align: center; + display: inline-block; +} + +.umb-nested-content__placeholder-icon { + font-size: 18px; + vertical-align: middle; +} + + +.form-horizontal .umb-nested-content--narrow .controls-row { margin-left: 40% !important; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index 1edaffe824..f754a09368 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -3,6 +3,7 @@ display: flex; box-sizing: border-box; border-bottom: 1px solid @gray-9; + flex-wrap: wrap; } .umb-editor-wrapper .umb-node-preview { @@ -64,9 +65,13 @@ flex: 0 0 auto; display: flex; align-items: center; + margin-left: auto; } .umb-node-preview__action { + background: transparent; + padding: 0; + border: 0 none; margin-left: 5px; margin-right: 5px; font-size: 13px; @@ -89,11 +94,13 @@ display: flex; align-items: center; justify-content: center; + background: transparent; border: 1px dashed @ui-action-discreet-border; color: @ui-action-discreet-type; font-weight: bold; padding: 5px 15px; box-sizing: border-box; + width: 100%; } .umb-node-preview-add:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index f704dd48e2..16457787a3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -264,12 +264,16 @@ flex-flow: row wrap; } -a.umb-package-details__back-link { +.umb-package-details__back-action { font-weight: bold; color: @black; + padding: 0; + border: 0; + background-color: transparent; } -.umb-package-details__back-link:hover { +.umb-package-details__back-action:focus, +.umb-package-details__back-action:hover { color: @gray-4; text-decoration: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less index 487c1881b1..46d7f04af6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less @@ -107,5 +107,5 @@ .umb-panel-group__details-status-action-description { margin-top: 5px; font-size: 12px; - padding-left: 165px; + padding-left:165px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less new file mode 100644 index 0000000000..3ce284870e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-property-actions.less @@ -0,0 +1,96 @@ +.umb-property-actions { + display: inline; +} + +.umb-property-actions__toggle, +.umb-property-actions__menu-open-toggle { + position: relative; + display: flex; + flex: 0 0 auto; + padding: 6px 6px; + text-align: center; + cursor: pointer; + border-radius: 3px; + + background-color: @ui-action-hover; + + i { + height: 3px !important; + width: 3px !important; + border-radius: 3px; + background: @ui-action-type; + display: inline-block; + margin: 0 2px 0 0; + + &:last-child { + margin: 0; + } + } + &:hover { + i { + background: @ui-action-type-hover; + } + } +} +.umb-property-actions__menu-open-toggle { + position: absolute; + z-index:1; + outline: none;// this is not acceccible by keyboard, since we use the .umb-property-actions__toggle for that. + + top: -15px; + border-radius: 3px 3px 0 0; + + border-top-left-radius: 3px; + border-top-right-radius: 3px; + + border: 1px solid @dropdownBorder; + + border-bottom: 1px solid @gray-9; + + .box-shadow(0 5px 20px rgba(0,0,0,.3)); + + background-color: white; + +} + +.umb-property .umb-property-actions { + float: left; +} +.umb-property .umb-property-actions__toggle { + margin-top: 2px; + opacity: 0; + transition: opacity 120ms; +} +.umb-property:hover .umb-property-actions__toggle, +.umb-property .umb-property-actions__toggle:focus { + opacity: 1; +} +// Revert-style-hack that ensures that we only show property-actions on properties that are directly begin hovered. +.umb-property:hover .umb-property:not(:hover) .umb-property-actions__toggle { + opacity: 0; +} + +.umb-property-actions__menu { + + position: absolute; + z-index: 1000; + + display: block; + + float: left; + min-width: 160px; + list-style: none; + + .umb-contextmenu { + + border-top-left-radius: 0; + margin-top:1px; + + } + + .umb-contextmenu-item > button { + + z-index:2;// need to stay on top of menu-toggle-open shadow. + + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index f387b6540b..5e766b7578 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -27,14 +27,13 @@ } +.umb-table__action, .umb-table a { + background: transparent; + border: 0 none; text-decoration: none; - cursor: pointer; - - &:focus { - outline: none; - text-decoration: none; - } + padding: 0; + margin-left: 1px; } input.umb-table__input { @@ -51,20 +50,26 @@ input.umb-table__input { } .umb-table-head__link { + background: transparent; + border: 0 none; position: relative; - cursor: default; text-decoration: none; color: @gray-3; + font-size: inherit; + font-weight: inherit; + padding: 0 1px; + &:hover { text-decoration: none; + cursor: default; color: @gray-3; } } -.umb-table-head__link .sortable { +.umb-table-head__link.sortable { + cursor: pointer; &:hover { text-decoration: none; - cursor: pointer; color: @black; } } @@ -136,15 +141,15 @@ input.umb-table__input { } .umb-table-body__link { - color: @ui-option-type; font-size: 14px; font-weight: bold; text-decoration: none; - + &:hover, &:focus { color: @ui-option-type-hover; text-decoration: underline; + outline: none; } } @@ -155,6 +160,7 @@ input.umb-table__input { font-size: 20px; line-height: 20px; color: @ui-option-type; + vertical-align: bottom; } .umb-table-body__checkicon, @@ -240,7 +246,7 @@ input.umb-table__input { .umb-table-cell { display: flex; flex-flow: row nowrap; - flex: 1 1 1%; //NOTE 1% is a Internet Explore hack, so that cells don't collapse + flex: 1 1 5%; position: relative; margin: auto 14px; padding: 6px 2px; @@ -253,6 +259,11 @@ input.umb-table__input { white-space: nowrap; //NOTE Disable/Enable this to keep textstring on one line text-overflow: ellipsis; } +.umb-table-cell.--noOverflow > * { + overflow: visible; + white-space: normal; + text-overflow: unset; +} .umb-table-cell:first-of-type:not(.not-fixed) { flex: 0 0 25px; @@ -264,6 +275,9 @@ input.umb-table__input { flex: 0 0 auto !important; } +.umb-table-cell--nano { + flex: 0 0 50px; +} .umb-table-cell--small { flex: .5 .5 1%; max-width: 12.5%; @@ -280,8 +294,8 @@ input.umb-table__input { // Increases the space for the name cell .umb-table__name { - flex: 1 1 25%; - max-width: 25%; + flex: 1 1 20%; + max-width: 300px; } .umb-table__loading-overlay { @@ -295,7 +309,11 @@ input.umb-table__input { .umb-table__row-expand { font-size: 12px; text-decoration: none; - color: @black; + color: @gray-4; + + &:hover { + color: @black; + } } .umb-table__row-expand--hidden { diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less index 0c219af1e4..7b842c40ad 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/examine-management.less @@ -12,4 +12,9 @@ border-bottom-left-radius: 0; } } + + .umb-panel-group__details-status-action{ + background-color:transparent; + padding-left:0; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less index 449cc4066b..bf01c21dca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less @@ -8,7 +8,6 @@ .umb-healthcheck-help-text { line-height: 1.6em; - max-width: 750px; } .umb-healthcheck-action-bar { @@ -22,15 +21,18 @@ .umb-healthcheck-group { display: flex; flex-wrap: wrap; - flex-direction: column; - background: @white; + flex-direction: column; + align-items: center; + background: @white; + border: 0; border-radius: 3px; padding: 20px; box-sizing: border-box; text-align: center; box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16); height: 100%; - box-sizing: border-box; + box-sizing: border-box; + width: 100%; } .umb-healthcheck-group:hover { @@ -117,7 +119,10 @@ /* DETAILS */ .umb-healthcheck-back-link { - font-weight: bold; + background: transparent; + border: 0 none; + padding: 0; + font-weight: bold; color: @black; } diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/nucache.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/nucache.less new file mode 100644 index 0000000000..4ebe1d47b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/nucache.less @@ -0,0 +1,13 @@ +#nuCache { + .no-background { + background-color: transparent; + } + + .top-border { + border-top: 2px solid #f3f3f5; + } + + .no-left-padding { + padding-left: 0; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less b/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less new file mode 100644 index 0000000000..82f9f3f553 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/filter-toggle.less @@ -0,0 +1,20 @@ +.filter-toggle{ + margin: 0; + padding: 0 8px 0 0; + position: relative; +} + +.filter-toggle__level{ + display: inline-block; + font-weight: 700; + margin: 0 5px; + max-width: 150px; +} + +.filter-toggle__icon{ + position: absolute; + top: 0; + bottom: 0; + right: 0; + margin: auto 0; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index 0b6d1b7a60..3ead4d6905 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -111,11 +111,10 @@ iframe, .content-column-body { } .pa-select-type label { - padding: 0 20px; + padding: 0 15px; } .pa-access-header { - font-weight: bold; margin: 0 0 3px 0; padding-bottom: 0; } diff --git a/src/Umbraco.Web.UI.Client/src/less/installer.less b/src/Umbraco.Web.UI.Client/src/less/installer.less index 798252c394..865f015ffa 100644 --- a/src/Umbraco.Web.UI.Client/src/less/installer.less +++ b/src/Umbraco.Web.UI.Client/src/less/installer.less @@ -1,4 +1,4 @@ -// Core variables and mixins +// Core variables and mixins @import "fonts.less"; // Loading fonts @import "variables.less"; // Modify this for custom colors, font-sizes, etc @import "mixins.less"; @@ -13,241 +13,245 @@ @import "../../lib/bootstrap/less/thumbnails.less"; @import "../../lib/bootstrap/less/media.less"; +// Umbraco Components +@import "components/umb-loader.less"; + [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { - display: none !important; + display: none !important; } html { - background: url('../img/installer.jpg') no-repeat center center fixed; - background-size: cover; + background: url('../img/installer.jpg') no-repeat center center fixed; + background-size: cover; } body { - margin: 0; - padding: 0; - height: 100%; - width: 100%; - - font-family: @baseFontFamily; - font-size: @baseFontSize; - line-height: @baseLineHeight; - color: @textColor; - - vertical-align: middle; - text-align: center; - - // better font rendering - -webkit-font-smoothing: antialiased; - font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - + margin: 0; + padding: 0; + height: 100%; + width: 100%; + font-family: @baseFontFamily; + font-size: @baseFontSize; + line-height: @baseLineHeight; + color: @textColor; + vertical-align: middle; + text-align: center; + // better font rendering + -webkit-font-smoothing: antialiased; + font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -#logo{ - position: absolute; - top: 20px; - left: 20px; - opacity: 0.8; - z-index: 777; +#logo { + position: absolute; + top: 20px; + left: 20px; + opacity: 0.8; + z-index: 777; } -#installer{ - margin: auto; +#installer { + margin: auto; background: @white; - width: 750px; - height: 600px; - text-align: left; - padding: 30px; - overflow:hidden; - z-index: 667; + width: 750px; + height: 600px; + text-align: left; + padding: 30px; + overflow: hidden; + z-index: 667; } -#overlay{ - position: absolute; - top: 0; - right: 0; - left: 0; - bottom: 0; - background: @purple-d2; - z-index: 666; +#overlay { + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + background: @purple-d2; + z-index: 666; } -.loading #overlay{ +.loading #overlay { opacity: 0.5; display: block !important; } -#fact{ - color: @white; - text-shadow: 0px 0px 4px @black; - font-size: 25px; - text-align: left; - line-height: 35px; - z-index: 667; - height: 600px; - width: 750px; +#fact { + color: @white; + text-shadow: 0px 0px 4px @black; + font-size: 25px; + text-align: left; + line-height: 35px; + z-index: 667; + height: 600px; + width: 750px; + + h2 { + font-size: 35px; + border-bottom: 1px solid @white; + padding-bottom: 10px; + margin-bottom: 20px; + color: @white; + } + + a { + color: @white; + } } -#fact h2{ - font-size: 35px; - border-bottom: 1px solid @white; - padding-bottom: 10px; - margin-bottom: 20px; - color: @white; -} - -#fact a{color: @white;} - -#feedback{ - color: @white; - text-shadow: 0px 0px 4px @black; - font-size: 14px; - text-align: center; - line-height: 20px; - z-index: 667; - bottom: 20px; - right: 0; - left: 0; - height: 25px; - position: absolute; +#feedback { + color: @white; + text-shadow: 0px 0px 4px @black; + font-size: 14px; + text-align: center; + line-height: 20px; + z-index: 667; + bottom: 20px; + right: 0; + left: 0; + height: 25px; + position: absolute; } -h1{ - border-bottom: 1px solid @gray-10; - padding-bottom: 10px; - color: @gray-2; +h1 { + border-bottom: 1px solid @gray-10; + padding-bottom: 10px; + color: @gray-2; } -.error h1, .error .message, span.error{ color: @red;} +.error h1, .error .message, span.error { + color: @red; +} -legend{font-size: 14px; font-weight: bold} +legend { + font-size: 14px; + font-weight: bold +} -input.ng-dirty.ng-invalid{border-color: #b94a48; color: #b94a48;} -.disabled{ - opacity: 0.6; +input.ng-dirty.ng-invalid { + border-color: #b94a48; + color: #b94a48; +} + +.disabled { + opacity: 0.6; } -#installer label.control-label, +#installer label.control-label, #installer .constrol-label { padding-top: 5px !important; } -.controls{ - text-align: left -} +.controls { + text-align: left; -.controls small{display: block; color: @gray-3;} + small { + display: block; + color: @gray-3; + } +} .absolute-center { - margin: auto; - position: absolute; - top: 0; left: 0; bottom: 0; right: 0; + margin: auto; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; } -.fade-hide, .fade-show { - transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; -} -.fade-hide { - opacity:1; -} -.fade-hide.fade-hide-active { - opacity:0; -} +.fade-hide, .fade-show { - opacity:0; + transition: all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s; } + +.fade-hide { + opacity: 1; +} + +.fade-hide.fade-hide-active { + opacity: 0; +} + +.fade-show { + opacity: 0; +} + .fade-show.fade-show-active { - opacity:1; + opacity: 1; } +.umb-installer-loader { + margin: 0; + width: 0; + z-index: 777; -.umb-loader{ - background-color: @white; - margin-top:0; - margin-left:-100%; - animation-name:bounce_loadingProgressG; - animation-duration:1s; - animation-iteration-count:infinite; - animation-timing-function:linear; - width:100%; - height: 5px; -} - -@keyframes bounce_loadingProgressG{ - 0%{ - margin-left:-100%; - } - 100%{ - margin-left:100%; + .umb-loader { + background-color: @white; + height: 5px; } } -//loader defaults -.umb-loader-container, .umb-loader-done{ - height: 3px; - position: absolute; - bottom: 0; - left: 0; - overflow: hidden; - width: 0%; - z-index: 777; -} - -.umb-loader-done{ - right: 0%; - background: @white; -} - - .permissions-report { - overflow:auto; - height:320px; - margin:0; - display:block; - padding:0; + overflow: auto; + height: 320px; + margin: 0; + display: block; + padding: 0; } + .permissions-report > li { - list-style:none; + list-style: none; } + .permissions-report h4 { - margin:7px; + margin: 7px; } .upgrade-report { - overflow:auto; - height:280px; - display:block; + overflow: auto; + height: 280px; + display: block; } select { - width:320px; + width: 320px; } #ysod { - height:500px; - width:100%; - overflow:auto; - border:none; + height: 500px; + width: 100%; + overflow: auto; + border: none; } -#starterKits .thumbnails{ - min-height:128px; -} -#starterKits a.thumbnail { - position:relative; - height:98px; -} -#starterKits a.thumbnail small { - position:absolute; - z-index: 50; - top:10px; - left:10px; -} -#starterKits a.thumbnail img { - position:relative; - z-index:100; + +#starterKits { + .thumbnails { + min-height: 128px; + padding-left: 0; + } + + .thumbnail { + position: relative; + cursor: pointer; + + small { + padding: 10px 10px 5px; + display: inline-block; + } + + img { + position: relative; + z-index: 100; + } + } + + .btn-link { + padding-left: 0; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/listview.less b/src/Umbraco.Web.UI.Client/src/less/listview.less index 9bd582a8ad..975dbdbd4a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/listview.less +++ b/src/Umbraco.Web.UI.Client/src/less/listview.less @@ -203,9 +203,9 @@ .list-view-layout__name-text { margin-right: 3px; } - + .list-view-layout__system { - font-size: 10px; + font-size: 10px; font-weight: normal; } @@ -236,6 +236,8 @@ } .list-view-add-layout { + width:100%; + background:0 0; margin-top: 10px; color: @ui-action-discreet-type; border: 1px dashed @ui-action-discreet-border; diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 9f9bbce310..86a1acbeae 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -116,24 +116,42 @@ h5.-black { margin: 20px; } .umb-control-group { - border-bottom: 1px solid @gray-11; - padding-bottom: 20px; + position: relative; + &::after { + content: ''; + display:block; + margin-top: 20px; + width: 100%; + height: 1px; + background-color: @gray-11; + } } .umb-control-group.-no-border { - border: none; + &::after { + margin-top: 0px; + height: 0; + background-color: transparent; + } } .umb-property:last-of-type .umb-control-group { - border: none; - margin-bottom: 0 !important; - padding-bottom: 0; + &::after { + margin-top: 0px; + height: 0; + background-color: transparent; + } + margin-bottom: 0 !important; } /* BLOCK MODE */ .block-form .umb-control-group { - border-bottom: none; - padding-bottom: 0; + margin-top: 0px; + &::after { + margin-top: 0px; + height: 0; + background-color: transparent; + } } .block-form .umb-control-group label .help-block, @@ -163,7 +181,36 @@ h5.-black { } .umb-control-group .umb-el-wrap { - padding: 0 + padding: 0; +} +.form-horizontal .umb-control-group .control-header { + float: left; + width: 160px; + padding-top: 5px; + text-align: left; + + .control-label { + float: left; + width: auto; + padding-top: 0; + text-align: left; + } + + .control-description { + display: block; + clear: both; + max-width:480px;// avoiding description becoming too wide when its placed on top of property. + margin-bottom: 10px; + } +} +@media (max-width: 767px) { + + .form-horizontal .umb-control-group .control-header { + float: none; + width: 100%; + } + + } /* LABELS*/ @@ -486,42 +533,6 @@ table thead a { color:@green; } -// Loading Animation -// ------------------------ - -.umb-loader{ - background-color: @blue; - margin-top:0; - margin-left:-100%; - animation-name:bounce_loadingProgressG; - animation-duration:1s; - animation-iteration-count:infinite; - animation-timing-function:linear; - width:100%; - height:2px; -} - -@keyframes bounce_loadingProgressG{ - 0%{ - margin-left:-100%; - } - 100%{ - margin-left:100%; - } -} - -.umb-loader-wrapper { - position: absolute; - right: 0; - left: 0; - margin: 10px 0; - overflow: hidden; -} - -.umb-loader-wrapper.-bottom { - bottom: 0; -} - // Helpers .strong { @@ -592,13 +603,3 @@ input[type=checkbox]:checked + .input-label--small { background-color: @green-l3; text-decoration: none; } - -.visuallyhidden{ - position: absolute !important; - clip: rect(1px, 1px, 1px, 1px); - padding:0 !important; - border:0 !important; - height: 1px !important; - width: 1px !important; - overflow: hidden; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index ce35097658..e49755338b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -308,7 +308,14 @@ opacity: @opacity / 100; } - +// Position +.absolute() { + position:absolute; + top:0; + right:0; + bottom:0; + left:0; +} // BACKGROUNDS // -------------------------------------------------- @@ -397,11 +404,19 @@ } } - +.checkeredBackground(@backgroundColor: #eee, @fillColor: #000, @fillOpacity: 0.25) { + background-image: url('data:image/svg+xml;charset=utf-8,\ + \ + \ + \ + '); + background-color: @backgroundColor; + background-size: 10px 10px; + background-repeat: repeat; +} // COMPONENT MIXINS // -------------------------------------------------- - // Limit width of specific property editors .umb-property-editor--limit-width { max-width: 800px; diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 84de751b12..fa23e08983 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -99,7 +99,7 @@ top: 0px; left: 0px; right: 0px; - bottom: 52px; + bottom: 49px; } .umb-dialog-body .umb-pane{margin-top: 15px;} @@ -111,7 +111,7 @@ left: 0px; right: 0px; bottom: 0px; - padding: 20px; + padding: 8px; margin: 0; .btn.umb-outline { diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index a2710fab6c..5b97464e31 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -237,7 +237,27 @@ color: @ui-option-type; } -.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus, .dropdown-submenu:hover > a, .dropdown-submenu:focus > a { +.dropdown-menu > li > button { + background: transparent; + border: 0; + padding: 8px 20px; + color: @ui-option-type; + display: block; + clear: both; + font-weight: normal; + line-height: 20px; + white-space: nowrap; + cursor:pointer; + width: 100%; + text-align: left; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus, +.dropdown-menu > li > button:hover, +.dropdown-menu > li > button:focus, +.dropdown-submenu:hover > a, +.dropdown-submenu:focus > a { color: @ui-option-type-hover; background: @ui-option-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index e14bb5c0d6..152ea49bbd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -48,6 +48,10 @@ flex-direction: row; } +.date-wrapper-mini--checkbox{ + margin: 0 0 0 26px; +} + .date-wrapper-mini__date { display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 92351d09ca..823daedf22 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -1,11 +1,8 @@ -@checkered-background: url(../img/checkered-background.png); - // // Container styles // -------------------------------------------------- -.umb-property-editor { +.umb-property-editor { width: 100%; - position:relative; } .umb-property-editor-tiny { @@ -273,8 +270,8 @@ } } -.umb-mediapicker .label{ - &__trashed{ +.umb-mediapicker .label { + &__trashed { background-color: @red; position: absolute; top: 50%; @@ -282,6 +279,7 @@ z-index: 1; transform: translate3d(-50%,-50%,0); margin: 0; + pointer-events: none; } } @@ -357,7 +355,6 @@ padding: 5px; margin: 5px; background: @white; - //border: 1px solid @gray-10; max-width: 100%; } .umb-mediapicker { @@ -368,14 +365,9 @@ } - - .umb-mediapicker .umb-sortable-thumbnails li { flex-direction: column; } -/*.umb-mediapicker .umb-sortable-thumbnails li.add-wrapper { - padding: 0px; -}*/ .umb-sortable-thumbnails li:hover a { display: flex; @@ -384,11 +376,11 @@ } .umb-sortable-thumbnails li img { - max-width:100%; - max-height:100%; - margin:auto; - display:block; - background-image: @checkered-background; + max-width: 100%; + max-height: 100%; + margin: auto; + display: block; + .checkeredBackground(); } .umb-sortable-thumbnails li .trashed { @@ -400,34 +392,6 @@ max-height: none !important; } -.umb-sortable-thumbnails .umb-icon-holder { - text-align: center; -} - -.umb-sortable-thumbnails .umb-icon-holder .icon { - font-size: 40px; - line-height: 50px; - color: @gray-3; - display: block; -} - -.umb-sortable-thumbnails .umb-icon-holder .file-icon > span { - color: @white; - background: @gray-4; - padding: 1px 3px; - font-size: 10px; - line-height: 130%; - display: block; - margin-top: -30px; - width: 2em; -} - -.umb-sortable-thumbnails .umb-icon-holder .file-icon + small { - display: block; - margin-top: 1em; -} - - .umb-sortable-thumbnails .umb-sortable-thumbnails__wrapper { width: 124px; height: 124px; @@ -451,8 +415,6 @@ text-decoration: none; display: flex; flex-direction: row; - opacity: 0; - visibility: hidden; } .umb-sortable-thumbnails.ui-sortable:not(.ui-sortable-disabled) { @@ -461,9 +423,8 @@ } } -.umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__actions { +.umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__action { opacity: 1; - visibility: visible; } .umb-sortable-thumbnails .umb-sortable-thumbnails__action { @@ -478,9 +439,13 @@ align-items: center; margin-left: 5px; text-decoration: none; - - //border-color: @inputBorder; .box-shadow(0 1px 2px rgba(0,0,0,0.25)); + opacity: 0; + transition: opacity .1s ease-in-out; + + .tabbing-active &:focus { + opacity: 1; + } } .umb-sortable-thumbnails .umb-sortable-thumbnails__action.-red { @@ -619,7 +584,19 @@ .viewport { max-width: 600px; - background: @checkered-background; + .checkeredBackground(); + + img { + display: block; + margin-left: auto; + margin-right: auto; + } + + img { + display: block; + margin-left: auto; + margin-right: auto; + } &:hover { cursor: pointer; @@ -639,7 +616,7 @@ } .viewport img { - background: @checkered-background; + .checkeredBackground(); } } @@ -758,13 +735,13 @@ } .umb-fileupload ul { - list-style: none; - vertical-align: middle; - margin-bottom: 0; + list-style: none; + vertical-align: middle; + margin-bottom: 0; - img { - background: @checkered-background; - } + img { + .checkeredBackground(); + } } .umb-fileupload label { @@ -783,30 +760,6 @@ padding-top: 27px; } -.umb-fileupload .file-icon { - display: inline-block; - position: relative; - padding: 5px 0; - - > .icon { - font-size: 70px; - line-height: 110%; - color: @gray-4; - text-align: center; - } - - > span { - color: @white; - background: @gray-4; - padding: 1px 3px; - font-size: 12px; - line-height: 130%; - position: absolute; - top: 45px; - left: 10px; - } -} - .umb-fileupload input { font-size: 12px; line-height: 1; diff --git a/src/Umbraco.Web.UI.Client/src/less/rte-content.less b/src/Umbraco.Web.UI.Client/src/less/rte-content.less new file mode 100644 index 0000000000..5fd7bbf1c3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/rte-content.less @@ -0,0 +1,44 @@ +@import "variables.less"; + +.mce-content-body .umb-macro-holder { + border: 3px dotted @pinkLight; + padding: 7px; + display: block; + margin: 3px; +} + + +.umb-rte .mce-content-body .umb-macro-holder.loading { + background: url(assets/img/loader.gif) right no-repeat; + background-size: 18px; background-position-x: 99%; +} + + +.umb-rte .embeditem { + position:relative; + > * { + user-select: none; + pointer-events: none; + } +} + +.umb-rte .embeditem[data-mce-selected] { + outline: 2px solid @pinkLight; +} + +.umb-rte .embeditem::before { + z-index:1000; + width:100%; + height:100%; + position:absolute; + content:' '; +} + +.umb-rte .embeditem[data-mce-selected]::before { + background:rgba(0,0,0,0.025); +} + +.umb-rte *[data-mce-selected="inline-boundary"] { + background:rgba(0,0,0,0.025); + outline: 2px solid @pinkLight; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/rte.less b/src/Umbraco.Web.UI.Client/src/less/rte.less index 9f537a7931..445ed7eb4a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/rte.less +++ b/src/Umbraco.Web.UI.Client/src/less/rte.less @@ -9,12 +9,20 @@ .umb-rte .mce-tinymce { box-shadow: none; - } -.umb-rte .umb-rte-editor{ +.umb-rte .umb-rte-editor-con { + height: 24px; + visibility: hidden; +} +.umb-rte .umb-rte-editor { min-height: 100px; } +.umb-rte.--initialized .umb-rte-editor-con { + height:auto; + min-height: 100px; + visibility: visible; +} .umb-rte .mce-content-body { background-color: @white; @@ -48,12 +56,14 @@ } */ -.mce-ico { +.umb-rte .mce-ico { + text-align: center; font-size: 12px !important; /*color: @gray-1 !important;*/ } /* Special case to support helviticons for the tiny mce button controls */ +// Also used in Prevalue editor. .mce-ico.mce-i-custom[class^="icon-"], .mce-ico.mce-i-custom[class*=" icon-"] { font-family: icomoon; @@ -73,6 +83,85 @@ } } -.mce-fullscreen { - position:absolute; +.umb-rte .mce-toolbar .mce-btn-group { + padding: 0; +} + +.umb-rte .mce-btn { + color: @ui-action-type; + border-radius: 3px; +} + +.umb-rte .mce-btn-group .mce-btn { + margin-top:2px; + margin-bottom:2px; +} + +.umb-rte .mce-btn { + button:hover { + .mce-caret { + border-top-color: @ui-action-type-hover; + } + } +} + +.umb-rte .mce-btn:hover, .umb-rte .mce-btn:active { + background: @ui-action-hover; + border-color: transparent; + button { + color: @ui-action-type-hover; + .mce-ico { + color: @ui-action-type-hover; + } + } +} + +.umb-rte .mce-btn.mce-active, .umb-rte .mce-btn.mce-active:active, +.umb-rte .mce-btn.mce-active:hover, .umb-rte .mce-btn.mce-active:focus { + background: @gray-9; + border-color: transparent; + button { + color: @ui-action-type-hover; + .mce-ico { + color: @ui-action-type-hover; + } + .mce-caret { + border-top-color: @ui-action-type-hover; + } + } +} + + +.mce-menu { + border-radius: 3px; +} + +.mce-menu-item.mce-menu-item-normal.mce-stack-layout-item { + .mce-text, .mce-ico { + color: @ui-action-type; + } + &:hover { + background: @ui-action-hover; + .mce-text, .mce-ico { + color: @ui-action-type-hover; + } + } +} + +.mce-menu-item.mce-menu-item-normal.mce-stack-layout-item.mce-active { + &, &:hover { + background: @gray-9; + } + .mce-text, .mce-ico { + color: @ui-action-type-hover; + } +} + +.umb-grid .umb-rte { + border: 1px solid #d8d7d9; + max-width: none; +} + +.mce-fullscreen { + position: absolute; } diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index ef6c5f5046..40921c5b76 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -2,129 +2,143 @@ // ------------------------- ul.sections { - margin: 0; - display: flex; - margin-left: -20px; -} - -ul.sections>li { - display: flex; - justify-content: center; - align-items: center; - position: relative; -} - -ul.sections>li>a { - color: @white; - height: @appHeaderHeight; + margin: 0; display: flex; - align-items: center; - justify-content: center; - position: relative; - padding: 0 10px; - text-decoration: none; - outline: none; - cursor: pointer; -} + margin-left: -20px; -ul.sections>li>a .section__name { - border-radius: 3px; - margin-top:1px; - padding: 3px 10px 4px 10px; - opacity: 0.8; - transition: opacity .1s linear, box-shadow .1s; -} + > li { + display: flex; + justify-content: center; + align-items: center; + position: relative; -ul.sections>li>a::after { - content: ""; - left: 10px; - right: 10px; - height: 4px; - bottom: 0; - transform: translateY(4px); - background-color: @pinkLight; - position: absolute; - border-radius: 3px 3px 0 0; - opacity: 0; - padding: 0 2px; - transition: transform 240ms ease-in-out; -} + > a { + color: @white; + height: @appHeaderHeight; + display: flex; + align-items: center; + justify-content: center; + position: relative; + padding: 0 10px; + text-decoration: none; + outline: none; + cursor: pointer; -ul.sections>li.current>a { - color:@pinkLight; -} -ul.sections>li.current>a::after { - opacity: 1; - transform: translateY(0px); -} -ul.sections > li.current > a .section__name, -ul.sections > li > a:hover .section__name { - opacity: 1; - -webkit-font-smoothing: subpixel-antialiased; -} + &::after { + content: ""; + left: 10px; + right: 10px; + height: 4px; + bottom: 0; + transform: translateY(4px); + background-color: @ui-active; + position: absolute; + border-radius: 3px 3px 0 0; + opacity: 0; + padding: 0 2px; + transition: transform 240ms ease-in-out; + } -ul.sections > li > a:focus .section__name { - .tabbing-active & { - - border: 1px solid; - border-color: @gray-9; + &:focus .section__name { + .tabbing-active & { + border: 1px solid; + border-color: @gray-9; + } + } + } + + .section__name { + border-radius: 3px; + margin-top: 1px; + padding: 3px 10px 4px 10px; + opacity: 0.8; + transition: opacity .1s linear, box-shadow .1s; + } + + &.current a { + color: @ui-active; + + &::after { + opacity: 1; + transform: translateY(0px); + } + } + + &.expand { + i { + height: 5px; + width: 5px; + border-radius: 50%; + background: @white; + display: inline-block; + margin: 0 5px 0 0; + opacity: 0.6; + transition: opacity .1s linear; + } + + &:hover i { + opacity:1; + } + } + + &.current .section__name, + a:hover .section__name { + opacity: 1; + -webkit-font-smoothing: subpixel-antialiased; + } } } - - /* Sections tray */ -ul.sections>li.expand i { - height: 5px; - width: 5px; - border-radius: 50%; - background: #fff; - display: inline-block; - margin: 0 5px 0 0; - opacity: 0.6; -} - ul.sections-tray { - position: absolute; - top: @appHeaderHeight; - left: 0; - margin: 0; + position: absolute; + top: @appHeaderHeight; + left: 0; + margin: 0; list-style: none; - background: @purple; - z-index: 10000; - border-radius: 0 0 3px 3px; -} + background: @blueExtraDark; + z-index: 10000; + border-radius: 0 0 3px 3px; -ul.sections-tray>li>a { - padding: 8px 24px; - color: @white; - text-decoration: none; - display: block; - position: relative; -} + li { -ul.sections-tray>li>a::after { - content: ""; - width: 4px; - height: 100%; - background-color: @ui-active; - position: absolute; - border-radius: 0 3px 3px 0; - opacity: 0; - transition: all .2s linear; - top: 0; - left: 0; -} + &.current a { + color: @ui-active; + opacity: 1; -ul.sections-tray>li.current>a::after { - opacity: 1; -} + &::after { + opacity: 1; + } + } -ul.sections-tray>li>a .section__name { - opacity: 0.6; -} + a { + padding: 8px 24px; + color: @white; + text-decoration: none; + display: block; + position: relative; + outline: none; -ul.sections-tray>li>a:hover .section__name { - opacity: 1; + &::after { + content: ""; + width: 4px; + height: 100%; + background-color: @ui-active; + position: absolute; + border-radius: 0 3px 3px 0; + opacity: 0; + transition: all .2s linear; + top: 0; + left: 0; + } + + &:focus .section__name { + .tabbing-active & { + border: 1px solid; + border-color: @gray-9; + } + } + } + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/tables.less b/src/Umbraco.Web.UI.Client/src/less/tables.less index cd6304ef49..2ecfa4d04e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tables.less +++ b/src/Umbraco.Web.UI.Client/src/less/tables.less @@ -62,7 +62,7 @@ table { } -.table tr > td:first-child { +.table:not(.table-bordered) tr > td:first-child { border-left: 4px solid transparent; } .table tr.--selected > td:first-child { @@ -263,3 +263,15 @@ table th[class*="span"], .table-sortable tbody tr { cursor: move; } + +.table__action-overlay{ + background: transparent; + border: 0 none; + padding: 0; + font-style: italic; + + &:focus, + &:hover{ + text-decoration: underline; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less index c0815fa8ac..a3427074cd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_flexbox.less @@ -1,7 +1,5 @@ /* - Flexbox - */ .flex { display: flex; } diff --git a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less index b9c8b909e8..787e50f204 100644 --- a/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less +++ b/src/Umbraco.Web.UI.Client/src/less/utilities/_spacing.less @@ -66,3 +66,19 @@ .ml5 { margin-left: @spacing-extra-large; } .ml6 { margin-left: @spacing-extra-extra-large; } .ml7 { margin-left: @spacing-extra-extra-extra-large; } + +.mr0 { margin-right: @spacing-none; } +.mr1 { margin-right: @spacing-extra-small; } +.mr2 { margin-right: @spacing-small; } +.mr3 { margin-right: @spacing-medium; } +.mr4 { margin-right: @spacing-large; } +.mr5 { margin-right: @spacing-extra-large; } +.mr6 { margin-right: @spacing-extra-extra-large; } +.mr7 { margin-right: @spacing-extra-extra-extra-large; } + +.p0 { padding: @spacing-none; } + +.pt0 { padding-top: @spacing-none; } +.pb0 { padding-bottom: @spacing-none; } +.pl0 { padding-left: @spacing-none; } +.pr0 { padding-right: @spacing-none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index a1dc0ba187..e8f6d4ee58 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -132,6 +132,7 @@ @ui-option-type: @blueExtraDark; @ui-option-type-hover: @blueMid; +@ui-option: white; @ui-option-hover: @sand-7; @ui-option-disabled-type: @gray-6; @@ -517,7 +518,7 @@ @heroUnitLeadColor: inherit; -// alerts +// Alerts // ------------------------- @warningText: @white; @warningBackground: @yellow-d2; diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js similarity index 96% rename from src/Umbraco.Web.UI.Client/src/controllers/main.controller.js rename to src/Umbraco.Web.UI.Client/src/main.controller.js index 654bbb1d03..93870f8a56 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -10,7 +10,7 @@ */ function MainController($scope, $location, appState, treeService, notificationsService, userService, historyService, updateChecker, navigationService, eventsService, - tmhDynamicLocale, localStorageService, editorService, overlayService) { + tmhDynamicLocale, localStorageService, editorService, overlayService, assetsService, tinyMceAssets) { //the null is important because we do an explicit bool check on this in the view $scope.authenticated = null; @@ -21,7 +21,13 @@ function MainController($scope, $location, appState, treeService, notificationsS $scope.search = {}; $scope.login = {}; $scope.tabbingActive = false; - + + // Load TinyMCE assets ahead of time in the background for the user + // To help with first load of the RTE + tinyMceAssets.forEach(function (tinyJsAsset) { + assetsService.loadJs(tinyJsAsset, $scope); + }); + // There are a number of ways to detect when a focus state should be shown when using the tab key and this seems to be the simplest solution. // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { diff --git a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/navigation.controller.js similarity index 95% rename from src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js rename to src/Umbraco.Web.UI.Client/src/navigation.controller.js index e4c94f3c66..b585d22e9f 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/navigation.controller.js @@ -212,6 +212,22 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar evts.push(eventsService.on("editors.languages.languageDeleted", function (e, args) { loadLanguages().then(function (languages) { $scope.languages = languages; + const defaultCulture = $scope.languages[0].culture; + + if (args.language.culture === $scope.selectedLanguage.culture) { + $scope.selectedLanguage = defaultCulture; + + if ($scope.languages.length > 1) { + $location.search("mculture", defaultCulture); + } else { + $location.search("mculture", null); + } + + var currentEditorState = editorState.getCurrent(); + if (currentEditorState && currentEditorState.path) { + $scope.treeApi.syncTree({ path: currentEditorState.path, activate: true }); + } + } }); })); @@ -255,7 +271,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar evts.push(eventsService.on("treeService.removeNode", function (e, args) { //check to see if the current page has been removed - var currentEditorState = editorState.getCurrent() + var currentEditorState = editorState.getCurrent(); if (currentEditorState && currentEditorState.id.toString() === args.node.id.toString()) { //current page is loaded, so navigate to root var section = appState.getSectionState("currentSection"); @@ -279,6 +295,9 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar //select the current language if set in the query string if (mainCulture && $scope.languages && $scope.languages.length > 1) { var found = _.find($scope.languages, function (l) { + if (mainCulture === true) { + return false; + } return l.culture.toLowerCase() === mainCulture.toLowerCase(); }); if (found) { @@ -348,7 +367,7 @@ function NavigationController($scope, $rootScope, $location, $log, $q, $routePar return contentResource.allowsCultureVariation().then(function (b) { if (b === true) { - return languageResource.getAll() + return languageResource.getAll(); } else { return $q.when([]); //resolve an empty collection } diff --git a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js index 1316642e93..c4cb821818 100644 --- a/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/preview/preview.controller.js @@ -143,7 +143,10 @@ var app = angular.module("umbraco.preview", ['umbraco.resources', 'umbraco.servi setPageUrl(); } }; - + + $scope.isCurrentCulture = function(culture) { + return $location.search().culture === culture; + } }) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 56db1fd88a..4ae3121098 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -9,7 +9,7 @@ -
+
Tours
@@ -38,8 +38,8 @@ {{ tour.name }}
- - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js index 75bf414099..7cfa02f95a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function CompositionsController($scope, $location, $filter) { + function CompositionsController($scope, $location, $filter, overlayService, localizationService) { var vm = this; var oldModel = null; @@ -56,19 +56,39 @@ if ($scope.model && $scope.model.submit) { // check if any compositions has been removed - vm.compositionRemoved = false; + var compositionRemoved = false; for (var i = 0; oldModel.compositeContentTypes.length > i; i++) { var oldComposition = oldModel.compositeContentTypes[i]; if (_.contains($scope.model.compositeContentTypes, oldComposition) === false) { - vm.compositionRemoved = true; + compositionRemoved = true; } } /* submit the form if there havne't been removed any composition or the confirm checkbox has been checked */ - if (!vm.compositionRemoved || vm.allowSubmit) { - $scope.model.submit($scope.model); + if (compositionRemoved) { + vm.allowSubmit = false; + localizationService.localize("general_remove").then(function(value) { + const dialog = { + view: "views/common/infiniteeditors/compositions/overlays/confirmremove.html", + title: value, + submitButtonLabelKey: "general_ok", + submitButtonStyle: "danger", + closeButtonLabelKey: "general_cancel", + submit: function (model) { + $scope.model.submit($scope.model); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + overlayService.open(dialog); + }); + return; } + + $scope.model.submit($scope.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html index bf74431d96..4096192081 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html @@ -60,9 +60,9 @@ -
+
    -
  • +
  • {{group.containerPath}}
  • @@ -77,7 +77,8 @@ checklist-model="model.compositeContentTypes" checklist-value="compositeContentType.contentType.alias" ng-change="model.selectCompositeContentType(compositeContentType.contentType)" - ng-disabled="compositeContentType.allowed===false || compositeContentType.inherited" /> + ng-disabled="compositeContentType.allowed===false || compositeContentType.inherited" /> +
- +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js index c86f55b255..167e74c25d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.controller.js @@ -10,7 +10,7 @@ (function() { "use strict"; - function DataTypePicker($scope, $filter, dataTypeResource, dataTypeHelper, contentTypeResource, localizationService, editorService) { + function DataTypePicker($scope, $filter, dataTypeResource, contentTypeResource, localizationService, editorService) { var vm = this; @@ -122,10 +122,18 @@ vm.showTabs = false; var regex = new RegExp(vm.searchTerm, "i"); + + var userConfigured = filterCollection(vm.userConfigured, regex), + typesAndEditors = filterCollection(vm.typesAndEditors, regex); + + var totalResults = _.reduce(_.pluck(_.union(userConfigured, typesAndEditors), 'count'), (m, n) => m + n, 0); + vm.filterResult = { - userConfigured: filterCollection(vm.userConfigured, regex), - typesAndEditors: filterCollection(vm.typesAndEditors, regex) + userConfigured: userConfigured, + typesAndEditors: typesAndEditors, + totalResults: totalResults }; + } else { vm.filterResult = null; vm.showTabs = true; @@ -134,11 +142,15 @@ function filterCollection(collection, regex) { return _.map(_.keys(collection), function (key) { + + var filteredDataTypes = $filter('filter')(collection[key], function (dataType) { + return regex.test(dataType.name) || regex.test(dataType.alias); + }); + return { group: key, - dataTypes: $filter('filter')(collection[key], function (dataType) { - return regex.test(dataType.name) || regex.test(dataType.alias); - }) + count: filteredDataTypes.length, + dataTypes: filteredDataTypes } }); } @@ -150,7 +162,6 @@ propertyDetails.title = property.name; $scope.model.itemDetails = propertyDetails; - } function hideDetailsOverlay() { @@ -177,7 +188,6 @@ }; editorService.open(dataTypeSettings); - } function pickDataType(selectedDataType) { @@ -205,7 +215,7 @@ } function close() { - if($scope.model.close) { + if ($scope.model.close) { $scope.model.close(); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html index fd859b9e2e..534fdc5648 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html @@ -20,17 +20,19 @@ + + @@ -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/embed/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js index 06a5f028ef..515f54e3d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.controller.js @@ -7,6 +7,7 @@ var origWidth = 500; var origHeight = 300; + vm.loading = false; vm.trustedPreview = null; $scope.model.embed = { @@ -17,9 +18,16 @@ preview: "", success: false, info: "", - supportsDimensions: "" + supportsDimensions: false }; + if ($scope.model.modify) { + angular.extend($scope.model.embed, $scope.model.modify); + + showPreview(); + } + + vm.toggleConstrain = toggleConstrain; vm.showPreview = showPreview; vm.changeSize = changeSize; vm.submit = submit; @@ -37,10 +45,10 @@ if ($scope.model.embed.url) { $scope.model.embed.show = true; - $scope.model.embed.preview = "
    "; $scope.model.embed.info = ""; $scope.model.embed.success = false; + vm.loading = true; $http({ method: 'GET', @@ -54,29 +62,41 @@ $scope.model.embed.preview = ""; - switch (response.data.OEmbedStatus) { case 0: //not supported + $scope.model.embed.preview = ""; $scope.model.embed.info = "Not supported"; + $scope.model.embed.success = false; + $scope.model.embed.supportsDimensions = false; + vm.trustedPreview = null; break; case 1: //error + $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; + $scope.model.embed.success = false; + $scope.model.embed.supportsDimensions = false; + vm.trustedPreview = null; break; case 2: + $scope.model.embed.success = true; + $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; $scope.model.embed.preview = response.data.Markup; vm.trustedPreview = $sce.trustAsHtml(response.data.Markup); - $scope.model.embed.supportsDimensions = response.data.SupportsDimensions; - $scope.model.embed.success = true; break; } + + vm.loading = false; + }, function() { + $scope.model.embed.success = false; $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; $scope.model.embed.info = "Could not embed media - please ensure the URL is valid"; - }); + vm.loading = false; + }); } else { $scope.model.embed.supportsDimensions = false; $scope.model.embed.preview = ""; @@ -105,6 +125,10 @@ } + function toggleConstrain() { + $scope.model.embed.constrain = !$scope.model.embed.constrain; + } + function submit() { if($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html index e48ec84b25..5862ca7059 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html @@ -16,7 +16,7 @@ - + - - -

    -
    -
    - -
    - - + + + +
    + + +

    +
    + +
    + + + - - - + + + - - - + + + +
    +
    @@ -62,6 +68,7 @@ button-style="success" label-key="general_submit" state="vm.saveButtonState" + disabled="!model.embed.url.length || !model.embed.preview.length" action="vm.submit(model)"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js index f2cc0dbecb..3de26ba99c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.controller.js @@ -5,8 +5,8 @@ var vm = this; - vm.field; - vm.defaultValue; + vm.field = null; + vm.defaultValue = null; vm.recursive = false; vm.showDefaultValue = false; @@ -16,10 +16,14 @@ function onInit() { + var labelKeys = [ + "template_insertPageField" + ]; + // set default title if(!$scope.model.title) { - localizationService.localize("template_insertPageField").then(function(value){ - $scope.model.title = value; + localizationService.localizeMany(labelKeys).then(function (data) { + $scope.model.title = data[0]; }); } @@ -37,42 +41,40 @@ function generateOutputSample() { - var fallback; + var fallback = null; - if(vm.recursive !== false && vm.defaultValue !== undefined){ + if (vm.recursive !== false && vm.defaultValue !== null) { fallback = "Fallback.To(Fallback.Ancestors, Fallback.DefaultValue)"; - }else if(vm.recursive !== false){ + } else if (vm.recursive !== false) { fallback = "Fallback.ToAncestors"; - }else if(vm.defaultValue !== undefined){ + } else if (vm.defaultValue !== null) { fallback = "Fallback.ToDefaultValue"; } - var pageField = (vm.field !== undefined ? '@Model.Value("' + vm.field + '"' : "") - + (fallback !== undefined? ', fallback: ' + fallback : "") - + (vm.defaultValue !== undefined ? ', defaultValue: new HtmlString("' + vm.defaultValue + '")' : "") + var pageField = (vm.field !== null ? '@Model.Value("' + vm.field + '"' : "") + + (fallback !== null? ', fallback: ' + fallback : "") + + (vm.defaultValue !== null ? ', defaultValue: new HtmlString("' + vm.defaultValue + '")' : "") + (vm.field ? ')' : ""); $scope.model.umbracoField = pageField; return pageField; - } function submit(model) { - if($scope.model.submit) { + if ($scope.model.submit) { $scope.model.submit(model); } } function close() { - if($scope.model.close) { + if ($scope.model.close) { $scope.model.close(); } } onInit(); - } angular.module("umbraco").controller("Umbraco.Editors.InsertFieldController", InsertFieldController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html index b2c6382b98..bbb2e8c798 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/insertfield/insertfield.html @@ -33,10 +33,9 @@
    - +
    @@ -52,13 +51,17 @@
    -
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js index 5da14fc6d3..b5043293e5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.controller.js @@ -93,7 +93,7 @@ angular.module("umbraco").controller("Umbraco.Editors.LinkPickerController", }); } - } else if ($scope.model.target.url.length) { + } else if ($scope.model.target.url && $scope.model.target.url.length) { // a url but no id/udi indicates an external link - trim the url to remove the anchor/qs // only do the substring if there's a # or a ? var indexOfAnchor = $scope.model.target.url.search(/(#|\?)/); 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 bcda921269..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 @@ -3,70 +3,103 @@ angular.module("umbraco") .controller("Umbraco.Editors.MediaPickerController", function ($scope, mediaResource, entityResource, userService, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService) { - if (!$scope.model.title) { - localizationService.localizeMany(["defaultdialogs_selectMedia", "general_includeFromsubFolders"]) - .then(function (data) { - $scope.labels = { - title: data[0], - includeSubFolders: data[1] - } - }); - } + var vm = this; + + vm.submit = submit; + vm.close = close; + + vm.toggle = toggle; + vm.upload = upload; + vm.dragLeave = dragLeave; + vm.dragEnter = dragEnter; + vm.onUploadComplete = onUploadComplete; + vm.onFilesQueue = onFilesQueue; + vm.changeSearch = changeSearch; + vm.submitFolder = submitFolder; + vm.enterSubmitFolder = enterSubmitFolder; + vm.focalPointChanged = focalPointChanged; + vm.changePagination = changePagination; + + vm.clickHandler = clickHandler; + vm.clickItemName = clickItemName; + vm.editMediaItem = editMediaItem; + vm.gotoFolder = gotoFolder; var dialogOptions = $scope.model; - - $scope.disableFolderSelect = dialogOptions.disableFolderSelect; - $scope.onlyImages = dialogOptions.onlyImages; - $scope.showDetails = dialogOptions.showDetails; + + $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; $scope.multiPicker = (dialogOptions.multiPicker && dialogOptions.multiPicker !== "0") ? true : false; $scope.startNodeId = dialogOptions.startNodeId ? dialogOptions.startNodeId : -1; $scope.cropSize = dialogOptions.cropSize; $scope.lastOpenedNode = localStorageService.get("umbLastOpenedMediaNodeId"); $scope.lockedFolder = true; $scope.allowMediaEdit = dialogOptions.allowMediaEdit ? dialogOptions.allowMediaEdit : false; - + var userStartNodes = []; var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings; var allowedUploadFiles = mediaHelper.formatFileTypes(umbracoSettings.allowedUploadFiles); + if ($scope.onlyImages) { - $scope.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); + vm.acceptedFileTypes = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes); } else { // Use whitelist of allowed file types if provided if (allowedUploadFiles !== '') { - $scope.acceptedFileTypes = allowedUploadFiles; + vm.acceptedFileTypes = allowedUploadFiles; } else { // If no whitelist, we pass in a blacklist by adding ! to the file extensions, allowing everything EXCEPT for disallowedUploadFiles - $scope.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); + vm.acceptedFileTypes = !mediaHelper.formatFileTypes(umbracoSettings.disallowedUploadFiles); } } - $scope.maxFileSize = umbracoSettings.maxFileSize + "KB"; + vm.maxFileSize = umbracoSettings.maxFileSize + "KB"; $scope.model.selection = []; - $scope.acceptedMediatypes = []; + vm.acceptedMediatypes = []; mediaTypeHelper.getAllowedImagetypes($scope.startNodeId) .then(function (types) { - $scope.acceptedMediatypes = types; + vm.acceptedMediatypes = types; }); - $scope.searchOptions = { + var dataTypeKey = null; + if ($scope.model && $scope.model.dataTypeKey) { + dataTypeKey = $scope.model.dataTypeKey; + } + + vm.searchOptions = { pageNumber: 1, pageSize: 100, totalItems: 0, totalPages: 0, filter: '', - dataTypeKey: $scope.model.dataTypeKey + dataTypeKey: dataTypeKey }; - //preload selected item - $scope.target = undefined; + // preload selected item + $scope.target = null; + if (dialogOptions.currentTarget) { $scope.target = dialogOptions.currentTarget; } + function setTitle() { + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMedia") + .then(function (data) { + $scope.model.title = data; + }); + } + } + function onInit() { + + setTitle(); + userService.getCurrentUser().then(function (userData) { userStartNodes = userData.startMediaIds; @@ -92,37 +125,47 @@ angular.module("umbraco") gotoStartNode(); } } else { - //if a target is specified, go look it up - generally this target will just contain ids not the actual full - //media object so we need to look it up + // if a target is specified, go look it up - generally this target will just contain ids not the actual full + // media object so we need to look it up var id = $scope.target.udi ? $scope.target.udi : $scope.target.id; var altText = $scope.target.altText; - entityResource.getById(id, "Media") + + // ID of a UDI or legacy int ID still could be null/undefinied here + // As user may dragged in an image that has not been saved to media section yet + if(id){ + entityResource.getById(id, "Media") .then(function (node) { $scope.target = node; if (ensureWithinStartNode(node)) { - selectImage(node); - $scope.target.url = mediaHelper.resolveFile(node); + selectMedia(node); + $scope.target.url = mediaHelper.resolveFileFromEntity(node); + $scope.target.thumbnail = mediaHelper.resolveFileFromEntity(node, true); $scope.target.altText = altText; - $scope.openDetailsDialog(); + openDetailsDialog(); } - }, - gotoStartNode); + }, gotoStartNode); + } + else { + // No ID set - then this is going to be a tmpimg that has not been uploaded + // User editing this will want to be changing the ALT text + openDetailsDialog(); + } } } - $scope.upload = function (v) { + function upload(v) { angular.element(".umb-file-dropzone .file-select").trigger("click"); - }; + } - $scope.dragLeave = function (el, event) { + function dragLeave(el, event) { $scope.activeDrag = false; - }; + } - $scope.dragEnter = function (el, event) { + function dragEnter(el, event) { $scope.activeDrag = true; - }; + } - $scope.submitFolder = function () { + function submitFolder() { if ($scope.model.newFolderName) { $scope.model.creatingFolder = true; mediaResource @@ -134,25 +177,25 @@ angular.module("umbraco") childrenOf: data.parentId //clear the children of the parent }); $scope.model.creatingFolder = false; - $scope.gotoFolder(data); + gotoFolder(data); $scope.model.showFolderInput = false; $scope.model.newFolderName = ""; }); } else { $scope.model.showFolderInput = false; } - }; + } - $scope.enterSubmitFolder = function (event) { + function enterSubmitFolder(event) { if (event.keyCode === 13) { - $scope.submitFolder(); + submitFolder(); event.stopPropagation(); } - }; + } - $scope.gotoFolder = function (folder) { + function gotoFolder(folder) { if (!$scope.multiPicker) { - deselectAllImages($scope.model.selection); + deselectAllMedia($scope.model.selection); } if (!folder) { @@ -160,7 +203,7 @@ angular.module("umbraco") } if (folder.id > 0) { - entityResource.getAncestors(folder.id, "media", null, { dataTypeKey: $scope.model.dataTypeKey }) + entityResource.getAncestors(folder.id, "media", null, { dataTypeKey: dataTypeKey }) .then(function (anc) { $scope.path = _.filter(anc, function (f) { @@ -170,93 +213,97 @@ angular.module("umbraco") mediaTypeHelper.getAllowedImagetypes(folder.id) .then(function (types) { - $scope.acceptedMediatypes = types; + vm.acceptedMediatypes = types; }); } else { $scope.path = []; } $scope.lockedFolder = (folder.id === -1 && $scope.model.startNodeIsVirtual) || hasFolderAccess(folder) === false; - $scope.currentFolder = folder; - localStorageService.set("umbLastOpenedMediaNodeId", folder.id); - return getChildren(folder.id); - }; - $scope.clickHandler = function (image, event, index) { - if (image.isFolder) { + localStorageService.set("umbLastOpenedMediaNodeId", folder.id); + + return getChildren(folder.id); + } + + function clickHandler(media, event, index) { + + if (media.isFolder) { if ($scope.disableFolderSelect) { - $scope.gotoFolder(image); + gotoFolder(media); } else { - eventsService.emit("dialogs.mediaPicker.select", image); - selectImage(image); + selectMedia(media); } } else { - eventsService.emit("dialogs.mediaPicker.select", image); if ($scope.showDetails) { - - $scope.target = image; - + + $scope.target = media; + // handle both entity and full media object - if (image.image) { - $scope.target.url = image.image; + if (media.image) { + $scope.target.url = media.image; } else { - $scope.target.url = mediaHelper.resolveFile(image); + $scope.target.url = mediaHelper.resolveFile(media); } - - $scope.openDetailsDialog(); + + openDetailsDialog(); } else { - selectImage(image); + selectMedia(media); } } - }; + } - $scope.clickItemName = function (item) { + function clickItemName(item) { if (item.isFolder) { - $scope.gotoFolder(item); + gotoFolder(item); } - }; + } - function selectImage(image) { - if (image.selected) { + function selectMedia(media) { + if (!media.selectable) { + return; + } + if (media.selected) { for (var i = 0; $scope.model.selection.length > i; i++) { var imageInSelection = $scope.model.selection[i]; - if (image.key === imageInSelection.key) { - image.selected = false; + if (media.key === imageInSelection.key) { + media.selected = false; $scope.model.selection.splice(i, 1); } } } else { if (!$scope.multiPicker) { - deselectAllImages($scope.model.selection); + deselectAllMedia($scope.model.selection); } - image.selected = true; - $scope.model.selection.push(image); + eventsService.emit("dialogs.mediaPicker.select", media); + media.selected = true; + $scope.model.selection.push(media); } } - function deselectAllImages(images) { - for (var i = 0; i < images.length; i++) { - var image = images[i]; - image.selected = false; + function deselectAllMedia(medias) { + for (var i = 0; i < medias.length; i++) { + var media = medias[i]; + media.selected = false; } - images.length = 0; + medias.length = 0; } - $scope.onUploadComplete = function (files) { - $scope.gotoFolder($scope.currentFolder).then(function () { + function onUploadComplete(files) { + gotoFolder($scope.currentFolder).then(function () { if (files.length === 1 && $scope.model.selection.length === 0) { var image = $scope.images[$scope.images.length - 1]; $scope.target = image; $scope.target.url = mediaHelper.resolveFile(image); - selectImage(image); + selectMedia(image); } - }); - }; + }) + } - $scope.onFilesQueue = function () { + function onFilesQueue() { $scope.activeDrag = false; - }; + } function ensureWithinStartNode(node) { // make sure that last opened node is on the same path as start node @@ -264,10 +311,10 @@ angular.module("umbraco") // also make sure the node is not trashed if (nodePath.indexOf($scope.startNodeId.toString()) !== -1 && node.trashed === false) { - $scope.gotoFolder({ id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder" }); + gotoFolder({ id: $scope.lastOpenedNode, name: "Media", icon: "icon-folder", path: node.path }); return true; } else { - $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); + gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); return false; } } @@ -284,83 +331,93 @@ angular.module("umbraco") } function gotoStartNode(err) { - $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); + gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); } - $scope.openDetailsDialog = function () { + 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); - $scope.mediaPickerDetailsOverlay = {}; - $scope.mediaPickerDetailsOverlay.show = true; + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; + }, + close: function (oldModel) { + vm.mediaPickerDetailsOverlay.show = false; + vm.mediaPickerDetailsOverlay = null; - $scope.mediaPickerDetailsOverlay.submit = function (model) { - $scope.model.selection.push($scope.target); - $scope.model.submit($scope.model); - - $scope.mediaPickerDetailsOverlay.show = false; - $scope.mediaPickerDetailsOverlay = null; - }; - - $scope.mediaPickerDetailsOverlay.close = function (oldModel) { - $scope.mediaPickerDetailsOverlay.show = false; - $scope.mediaPickerDetailsOverlay = null; - }; + close(); + } + }; + }); }; var debounceSearchMedia = _.debounce(function () { $scope.$apply(function () { - if ($scope.searchOptions.filter) { + if (vm.searchOptions.filter) { searchMedia(); } else { + // reset pagination - $scope.searchOptions = { + vm.searchOptions = { pageNumber: 1, pageSize: 100, totalItems: 0, totalPages: 0, filter: '', - dataTypeKey: $scope.model.dataTypeKey + dataTypeKey: dataTypeKey }; + getChildren($scope.currentFolder.id); } }); }, 500); - $scope.changeSearch = function () { - $scope.loading = true; + function changeSearch() { + vm.loading = true; debounceSearchMedia(); - }; - - $scope.toggle = function () { - // Make sure to activate the changeSearch function everytime the toggle is clicked - $scope.changeSearch(); } - $scope.changePagination = function (pageNumber) { - $scope.loading = true; - $scope.searchOptions.pageNumber = pageNumber; + function toggle() { + // Make sure to activate the changeSearch function everytime the toggle is clicked + changeSearch(); + } + + function changePagination(pageNumber) { + vm.loading = true; + vm.searchOptions.pageNumber = pageNumber; searchMedia(); }; function searchMedia() { - $scope.loading = true; - entityResource.getPagedDescendants($scope.currentFolder.id, "Media", $scope.searchOptions) + vm.loading = true; + entityResource.getPagedDescendants($scope.currentFolder.id, "Media", vm.searchOptions) .then(function (data) { // update image data to work with image grid angular.forEach(data.items, function (mediaItem) { setMediaMetaData(mediaItem); }); + // update images $scope.images = data.items ? data.items : []; + // update pagination if (data.pageNumber > 0) - $scope.searchOptions.pageNumber = data.pageNumber; + vm.searchOptions.pageNumber = data.pageNumber; if (data.pageSize > 0) - $scope.searchOptions.pageSize = data.pageSize; - $scope.searchOptions.totalItems = data.totalItems; - $scope.searchOptions.totalPages = data.totalPages; - // set already selected images to selected - preSelectImages(); - $scope.loading = false; + vm.searchOptions.pageSize = data.pageSize; + + vm.searchOptions.totalItems = data.totalItems; + vm.searchOptions.totalPages = data.totalPages; + + // set already selected medias to selected + preSelectMedia(); + vm.loading = false; }); } @@ -368,6 +425,7 @@ angular.module("umbraco") // set thumbnail and src mediaItem.thumbnail = mediaHelper.resolveFileFromEntity(mediaItem, true); mediaItem.image = mediaHelper.resolveFileFromEntity(mediaItem, false); + // set properties to match a media object if (mediaItem.metaData) { mediaItem.properties = []; @@ -399,33 +457,39 @@ angular.module("umbraco") } function getChildren(id) { - $scope.loading = true; - return entityResource.getChildren(id, "Media", $scope.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); - } + vm.loading = true; + return entityResource.getChildren(id, "Media", vm.searchOptions).then(function (data) { + + 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); } - $scope.searchOptions.filter = ""; - $scope.images = data ? data : []; - // set already selected images to selected - preSelectImages(); - $scope.loading = false; - }); + + data[i].filtered = allowedTypes && allowedTypes.indexOf(data[i].metaData.ContentTypeAlias) < 0; + } + + vm.searchOptions.filter = ""; + $scope.images = data ? data : []; + + // set already selected medias to selected + preSelectMedia(); + vm.loading = false; + }); } - function preSelectImages() { - for (var folderImageIndex = 0; folderImageIndex < $scope.images.length; folderImageIndex++) { - var folderImage = $scope.images[folderImageIndex]; + function preSelectMedia() { + for (var folderIndex = 0; folderIndex < $scope.images.length; folderIndex++) { + var folderImage = $scope.images[folderIndex]; var imageIsSelected = false; if ($scope.model && angular.isArray($scope.model.selection)) { - for (var selectedImageIndex = 0; - selectedImageIndex < $scope.model.selection.length; - selectedImageIndex++) { - var selectedImage = $scope.model.selection[selectedImageIndex]; + for (var selectedIndex = 0; + selectedIndex < $scope.model.selection.length; + selectedIndex++) { + var selectedImage = $scope.model.selection[selectedIndex]; if (folderImage.key === selectedImage.key) { imageIsSelected = true; @@ -439,7 +503,7 @@ angular.module("umbraco") } } - $scope.editMediaItem = function (item) { + function editMediaItem(item) { var mediaEditor = { id: item.id, submit: function (model) { @@ -464,6 +528,19 @@ angular.module("umbraco") editorService.mediaEditor(mediaEditor); }; + /** + * Called when the umbImageGravity component updates the focal point value + * @param {any} left + * @param {any} top + */ + function focalPointChanged(left, top) { + // update the model focalpoint value + $scope.target.focalPoint = { + left: left, + top: top + }; + } + function setUpdatedMediaNodes(item) { // add udi to list of updated media items so we easily can update them in other editors if ($scope.model.updatedMediaNodes.indexOf(item.udi) === -1) { @@ -471,17 +548,17 @@ angular.module("umbraco") } } - $scope.submit = function () { - if ($scope.model.submit) { + function submit() { + if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); } - }; + } - $scope.close = function () { - if ($scope.model.close) { + function close() { + if ($scope.model && $scope.model.close) { $scope.model.close($scope.model); } - }; + } onInit(); 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 6eee269cee..373dfbcba7 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,192 +1,227 @@
      - + - - + + - + -
      + -
      +
      + +
      + +
      + + +
      + + +
      +
      + +
      + +
      +
        +
      • + + +
      • + +
      • + + +
      • + +
      • + + + +
      • +
      +
      +
      + + + + + + -
      + ng-if="vm.loading"> - -
      - -
      +
      +
      + +
      +
      - - - - - - -
      - - +
      +
      + +
      +
      - - - - -
      - - -
      -
      - - -
      - -
      - +
      Preview
      - - + {{target.name}} +
      + +
      +
      + Focal point +
      + +
      + + +
      + +
      + +
      + Preview +
      + + + + +
      +
      -
      +
      -
      - - -
      + -
      - - -
      + + + - + + - + + - + + - - + - - - - - - - - - - - -
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js index faca3b3fa0..4d537bd73c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.controller.js @@ -31,6 +31,7 @@ vm.datePickerChange = datePickerChange; vm.submit = submit; vm.close = close; + vm.copyQuery = copyQuery; function onInit() { @@ -120,6 +121,11 @@ query.filters.push({}); } + function copyQuery() { + var copyText = $scope.model.result.queryExpression; + navigator.clipboard.writeText(copyText); + } + function trashFilter(query, filter) { for (var i = 0; i < query.filters.length; i++) { if (query.filters[i] == filter) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html index 8376f50713..f01f325265 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/querybuilder/querybuilder.html @@ -15,164 +15,160 @@ -
      +
      -
      -
      +
      +
      - I want + I want - - from + from - - + + -
      +
      -
      +
      - - where - - - and - + + where + + + and + -
      + +
      -
      +
      - - + + - - - - {{term.name}} - - - + + + + {{term.name}} + + + -
      +
      - + - - - + + + - - - - + + + + - + - - - + + + - - - + + + -
      +
      -
      +
      - order by + order by -
      + +
      - - + + -
      -
      +
      +
      {{model.result.resultCount}} items, returned in {{model.result.executionTime}} ms
      - + -
      {{model.result.queryExpression}}
      +
      {{model.result.queryExpression}}
      + + copy to clipboard + -
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js index afec0ae120..f71eb2c51e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/rollback/rollback.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function RollbackController($scope, contentResource, localizationService, assetsService) { + function RollbackController($scope, contentResource, localizationService, assetsService, dateHelper, userService) { var vm = this; @@ -90,11 +90,15 @@ const culture = $scope.model.node.variants.length > 1 ? vm.currentVersion.language.culture : null; return contentResource.getRollbackVersions(nodeId, culture) - .then(function(data){ - vm.previousVersions = data.map(version => { - version.displayValue = version.versionDate + " - " + version.versionAuthorName; - return version; - }); + .then(function (data) { + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + vm.previousVersions = data.map(version => { + var timestampFormatted = dateHelper.getLocalDate(version.versionDate, currentUser.locale, 'LLL'); + version.displayValue = timestampFormatted + ' - ' + version.versionAuthorName; + return version; + }); + }); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html index 2e88bf709c..33906dcd75 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html @@ -19,10 +19,10 @@
        -
      • -
      • + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js index 36d7c0f4ed..8c728150da 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.controller.js @@ -1,26 +1,42 @@ (function () { "use strict"; - function TemplateSectionsController($scope, formHelper) { + function TemplateSectionsController($scope, formHelper, localizationService) { var vm = this; + vm.labels = {}; + vm.select = select; vm.submit = submit; vm.close = close; $scope.model.mandatoryRenderSection = false; - if(!$scope.model.title) { - $scope.model.title = "Sections"; - } - function onInit() { - if($scope.model.hasMaster) { + if ($scope.model.hasMaster) { $scope.model.insertType = 'addSection'; } else { $scope.model.insertType = 'renderBody'; } + + var labelKeys = [ + "template_insertSections", + "template_sectionMandatory" + ]; + + localizationService.localizeMany(labelKeys).then(function (data) { + vm.labels.title = data[0]; + vm.labels.sectionMandatory = data[1]; + + setTitle(vm.labels.title); + }); + } + + function setTitle(value) { + if (!$scope.model.title) { + $scope.model.title = value; + } } function select(type) { @@ -34,13 +50,12 @@ } function close() { - if($scope.model.close) { + if ($scope.model.close) { $scope.model.close(); } } onInit(); - } angular.module("umbraco").controller("Umbraco.Editors.TemplateSectionsController", TemplateSectionsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html index 5b946976d7..045a1403e2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html @@ -44,10 +44,10 @@
      - - +
      + +
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 31430c81cb..0ff6403761 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -96,7 +96,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } - if (vm.treeAlias === "documentTypes") { + else if (vm.treeAlias === "documentTypes") { vm.entityType = "DocumentType"; if (!$scope.model.title) { localizationService.localize("defaultdialogs_selectContentType").then(function(value){ @@ -107,9 +107,17 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", else if (vm.treeAlias === "member" || vm.section === "member") { vm.entityType = "Member"; if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMember").then(function(value){ + localizationService.localize("defaultdialogs_selectMember").then(function(value) { $scope.model.title = value; - }) + }); + } + } + else if (vm.treeAlias === "memberTypes") { + vm.entityType = "MemberType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMemberType").then(function(value){ + $scope.model.title = value; + }); } } else if (vm.treeAlias === "media" || vm.section === "media") { @@ -120,6 +128,14 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } + else if (vm.treeAlias === "mediaTypes") { + vm.entityType = "MediaType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMediaType").then(function(value){ + $scope.model.title = value; + }); + } + } // TODO: Seems odd this logic is here, i don't think it needs to be and should just exist on the property editor using this if ($scope.model.minNumber) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/confirm/confirm.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/confirm/confirm.html new file mode 100644 index 0000000000..79dd6bd4f6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/confirm/confirm.html @@ -0,0 +1,13 @@ +
      + +
      + {{model.confirmMessage}} +
      + +

      {{model.content}}

      + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index 4af8c83983..531feef892 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -26,8 +26,7 @@ action="togglePasswordFields()" button-style="action" label="Change password" - label-key="general_changePassword" - button-style="success"> + label-key="general_changePassword"> -
      + - + + Link your {{login.caption}} account +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index 93c3a9b50d..c6c4f98e25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -2,7 +2,6 @@
      - @@ -11,19 +10,26 @@
      • -
      • -
      • -
      @@ -152,13 +152,13 @@
      - - + +
      - - + +
      -

      +

      An email will be sent to the address specified with a link to reset your password

      - - + +
      @@ -198,9 +198,11 @@
      -
      + +

      +
      @@ -220,13 +222,13 @@
      - - + +
      - - + +
      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..0aab35ca21 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,31 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html index 37f363f50d..7430d45ce6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-content-header.html @@ -3,32 +3,40 @@
    - +
    +
    +

    + {{a11yMessage}} +

    +
    + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index cc2ef0f2fa..c46efb7b74 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -1,11 +1,12 @@ 
    - -
    + +
    - +
    @@ -21,12 +22,18 @@
    +
    +

    + {{accessibility.a11yMessage}} +

    +
    + + aria-required="true" + aria-invalid="{{contentForm.headerNameForm.headerName.$invalid ? true : false}}" + autocomplete="off" + maxlength="255"/> @@ -61,7 +72,7 @@ localize="placeholder" placeholder="@placeholders_enterDescription" ng-if="!hideDescription && !descriptionLocked" - ng-model="$parent.description" /> + ng-model="$parent.description"/>
    {{ description }}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html index c621670462..fe90fef07a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html @@ -5,15 +5,18 @@ button-style="white" action="dropdown.isOpen = !dropdown.isOpen" label-key="general_actions" - show-caret="true"> + show-caret="true" + has-popup="true" + is-expanded="dropdown.isOpen" + > - + - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html index 68a3da435b..dda8fa70f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -1,19 +1,19 @@ - - + ng-class="{'is-active': vm.item.active, '-has-error': vm.item.hasError}" + class="umb-sub-views-nav-item__action"> + {{ vm.item.name }}
    {{vm.item.badge.count}}
    -
    + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 99f9e96765..2f1286b090 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -3,10 +3,11 @@
    + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html index 877e55a1c5..9ee50bcae1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html @@ -1,13 +1,16 @@ -