diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 06fd873638..431fa7c2fa 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -4,268 +4,56 @@
These contribution guidelines are mostly just that - guidelines, not rules. This is what we've found to work best over the years, but if you choose to ignore them, we still love you! đź’– Use your best judgement, and feel free to propose changes to this document in a pull request.
-## Coding not your thing? Or want more ways to contribute?
+## Getting Started
+We have a guide on [what to consider before you start](contributing-before-you-start.md) and more detailed guides at the end of this article.
-This document covers contributing to the codebase of the CMS but [the community site has plenty of inspiration for other ways to get involved.][get involved]
-
-If you don't feel you'd like to make code changes here, you can visit our [documentation repository][docs repo] and use your experience to contribute to making the docs we have, even better.
-
-We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Collaborators 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.
-
-## Table of contents
-
-- [Before you start](#before-you-start)
- * [Code of Conduct](#code-of-conduct)
- * [What can I contribute?](#what-can-i-contribute)
- + [Making larger changes](#making-larger-changes)
- + [Pull request or package?](#pull-request-or-package)
- + [Unwanted changes](#unwanted-changes)
- + [Ownership and copyright](#ownership-and-copyright)
-- [Finding your first issue: Up for grabs](#finding-your-first-issue-up-for-grabs)
-- [Making your changes](#making-your-changes)
- + [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository)
- + [Style guide](#style-guide)
- + [Questions?](#questions)
-- [Creating a pull request](#creating-a-pull-request)
-- [The review process](#the-review-process)
- * [Dealing with requested changes](#dealing-with-requested-changes)
- + [No longer available?](#no-longer-available)
- * [The Core Collaborators team](#the-core-collaborators-team)
-
-## Before you start
-
-
-### Code of Conduct
-
-This project and everyone participating in it, is governed by the [our Code of Conduct][code of conduct].
-
-### What can I contribute?
-
-We categorise pull requests (PRs) into two categories:
-
-| PR type | Definition |
-| --------- | ------------------------------------------------------------ |
-| 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. |
-| 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’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].
-
-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.
-
-Not all changes are wanted, so on occasion we might close a PR without merging it but if we do, we will give you feedback why we can't accept your changes. **So make sure to [talk to us before making large changes][making larger changes]**, so we can ensure that you don't put all your hard work into something we would not be able to merge.
-
-#### Making larger changes
-
-[making larger changes]: #making-larger-changes
-
-Please make sure to describe your larger ideas in an [issue (bugs)][issues] or [discussion (new features)][discussions], it helps to put in mock up 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 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.
-
-#### Pull request or package?
-
-[pr or package]: #pull-request-or-package
-
-If you're unsure about whether your changes belong in the core Umbraco CMS or if you should turn your idea into a package instead, make sure to [talk to us][making larger changes].
-
-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.
-
-#### Unwanted changes
-While most changes are welcome, there are certain types of changes that are discouraged and might get your pull request refused.
-Of course this will depend heavily on the specific change, but please take the following examples in mind.
-
-- **Breaking changes (code and/or behavioral) đź’Ą** - sometimes it can be a bit hard to know if a change is breaking or not. Fortunately, if it relates to code, the build will fail and warn you.
-- **Large refactors 🤯** - the larger the refactor, the larger the probability of introducing new bugs/issues.
-- **Changes to obsolete code and/or property editors ✍️**
-- **Adding new config options 🦾** - while having more flexibility is (most of the times) better, having too many options can also become overwhelming/confusing, especially if there are other (good/simple) ways to achieve it.
-- **Whitespace changes 🫥** - while some of our files might not follow the formatting/whitespace rules (mostly old ones), changing several of them in one go would cause major merge conflicts with open pull requests or other work in progress. Do feel free to fix these when you are working on another issue/feature and end up "touching" those files!
-- **Adding new extension/helper methods âś‹** - keep in mind that more code also means more to maintain, so if a helper is only meaningful for a few, it might not be worth adding it to the core.
-
-While these are only a few examples, it is important to ask yourself these questions before making a pull request:
-
-- How many will benefit from this change?
-- Are there other ways to achieve this? And if so, how do they compare?
-- How maintainable is the change?
-- What would be the effort to test it properly?
-- Do the benefits outweigh the risks?
-
-#### Ownership and copyright
-
-It is your responsibility to make sure that you're allowed to share the code you're providing us. For example, you should have permission from your employer or customer to share code.
-
-Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS.
-
-If you're not sure, leave a note on your contribution and we will be happy to guide you.
-
-When your contribution has been accepted, it will be [MIT licensed][MIT license] from that time onwards.
-
-## Finding your first issue: Up for grabs
-
-Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. 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.
-
-## Making your changes
-
-Great question! The short version goes like this:
+The following steps are a quick-start guide:
1. **Fork**
- Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo]
+ Create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)

-1. **Clone**
+2. **Clone**
- When GitHub has created your fork, you can clone it in your favorite Git tool
+ When GitHub has created your fork, you can clone it in your favorite Git tool or on the command line with `git clone https://github.com/[YourUsername]/Umbraco-CMS`.

-1. **Switch to the correct branch**
+3. **Switch to the correct branch**
Switch to the `contrib` branch
-1. **Build**
+4. **Build**
- Build your fork of Umbraco locally as described in the build documentation: you can [debug with Visual Studio Code][build - debugging with code] or [with Visual Studio][build - debugging with vs].
+ Build your fork of Umbraco locally [as described in the build documentation](BUILD.md), you can build with any IDE that supports dotnet or the command line.
-1. **Branch**
+5. **Branch**
- 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 issue number `12345`. Don't commit to `contrib`, create a new branch first.
+ 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 issue number `12345`. Don't commit to `contrib`, create a new branch first.
-1. **Change**
+6. **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].
+ Make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](contributing-first-issue.md#questions).
-1. **Commit and push**
+7. **Commit and push**
Done? Yay! 🎉
Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
-#### Keeping your Umbraco fork in sync with the main repository
-[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
+8. **Create pull request**
-Once you've already got a fork and cloned your fork locally, you can skip steps 1 and 2 going forward. Just remember to keep your fork up to date before making further changes.
+ On GitHub, in your forked repository (`https://github.com/[YourUsername]/Umbraco-CMS`) you will see a banner saying that you pushed a new branch and a button to make a pull request. Tap the button and follow the instuctions.
-To sync your fork with this original one, you'll have to add the upstream url. You only have to do this once:
+ Want to read further? [Creating a pull request and what happens next](contributing-creating-a-pr.md).
-```
-git remote add upstream https://github.com/umbraco/Umbraco-CMS.git
-```
+## Further contribution guides
-Then when you want to get the changes from the main repository:
+- [Before you start](contributing-before-you-start.md)
+- [Finding your first issue: Up for grabs](contributing-before-you-start.md)
+- [Contributing to the new backoffice](https://docs.umbraco.com/umbraco-backoffice/)
+- [Unwanted changes](contributing-unwanted-changes.md)
+- [Other ways to contribute](contributing-other-ways-to-contribute.md)
-```
-git fetch upstream
-git rebase upstream/contrib
-```
-
-In this command we're syncing with the `contrib` branch, but you can of course choose another one if needed.
-
-[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
-
-#### Style guide
-
-To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up.
-
-That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc.
-
-#### Questions?
-[questions]: #questions
-
-You can get in touch with [the core contributors team][core collabs] 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.
-- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs].
-- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
-
-## Creating a pull request
-
-Exciting! You're ready to show us your changes.
-
-We recommend you to [sync with our repository][sync fork] before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier.
-
-GitHub will have 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.
-
-
-We like to use [git flow][git flow] as much as possible, 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 `contrib`. This is the branch you should be targeting.
-
-Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise.
-
-## The review process
-[review process]: #the-review-process
-
-You've sent us your first contribution - congratulations! Now what?
-
-The [Core Collaborators team][Core collabs] 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.
-
-You will get an initial automated reply from our [Friendly Umbraco Robot, Umbrabot][Umbrabot], to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. You can take this opportunity to double check everything is in order based off the handy checklist Umbrabot provides.
-
-You will get feedback as soon as the [Core Collaborators team][Core collabs] can after opening the PR. You’ll most likely get feedback within a couple of weeks. 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!). See [making larger changes][making larger changes] and [pull request or package?][pr or package]
-
-### Dealing with requested changes
-
-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!
-
-#### No longer 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.
-
-### The Core Collaborators team
-[Core collabs]: #the-core-collaborators-team
-
-The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan][Sebastiaan], who gets assistance from the following community members who have committed to volunteering their free time:
-
-- [Busra Sengul][Busra Sengul]
-- [Emma Garland][Emma Garland]
-- [George Bidder][George Bidder]
-- [Jason Elkin][Jason Elkin]
-- [Laura Neto][Laura Neto]
-- [Michael Latouche][Michael Latouche]
-- [Sebastiaan][Sebastiaan]
-
-
-These wonderful people aim to provide you with a 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.
-
-
-
-
-
-[MIT license]: ../LICENSE.md "Umbraco's license declaration"
-[build - debugging with vs]: BUILD.md#debugging-with-visual-studio "Details on building and debugging Umbraco with Visual Studio"
-[build - debugging with code]: BUILD.md#debugging-with-vs-code "Details on building and debugging Umbraco with Visual Studio Code"
-
-
-
-[Busra Sengul]: https://github.com/busrasengul "Busra's GitHub profile"
-[Emma Garland]: https://github.com/emmagarland "Emma's GitHub profile"
-[George Bidder]: https://github.com/georgebid "George's GitHub profile"
-[Jason Elkin]: https://github.com/jasonelkin "Jason's GitHub profile"
-[Kyle Eck]: https://github.com/teckspeed "Kyle's GitHub profile"
-[Laura Neto]: https://github.com/lauraneto "Laura's GitHub profile"
-[Michael Latouche]: https://github.com/mikecp "Michael's GitHub profile"
-[Sebastiaan]: https://github.com/nul800sebastiaan "Sebastiaan's GitHub profile"
-[ Umbrabot ]: https://github.com/umbrabot
-[git flow]: https://jeffkreeftmeijer.com/git-flow/ "An explanation of git flow"
-[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
-[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests"
-[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/
-[get involved]: https://community.umbraco.com/get-involved/
-[docs repo]: https://github.com/umbraco/UmbracoDocs
-[code of conduct]: https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md
-[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
-[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS
-[issues]: https://github.com/umbraco/Umbraco-CMS/issues
-[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions
diff --git a/.github/contributing-before-you-start.md b/.github/contributing-before-you-start.md
new file mode 100644
index 0000000000..1668cf9541
--- /dev/null
+++ b/.github/contributing-before-you-start.md
@@ -0,0 +1,54 @@
+## Before you start
+
+
+### Code of Conduct
+
+This project and everyone participating in it, is governed by the [our Code of Conduct][code of conduct].
+
+### What can I contribute?
+
+We categorise pull requests (PRs) into two categories:
+
+| PR type | Definition |
+| --------- | ------------------------------------------------------------ |
+| 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. |
+| 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’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].
+
+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.
+
+Not all changes are wanted, so on occasion we might close a PR without merging it but if we do, we will give you feedback why we can't accept your changes. **So make sure to [talk to us before making large changes][making larger changes]**, so we can ensure that you don't put all your hard work into something we would not be able to merge.
+
+#### Making larger changes
+
+[making larger changes]: #making-larger-changes
+
+Please make sure to describe your larger ideas in an [issue (bugs)][issues] or [discussion (new features)][discussions], it helps to put in mock up 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 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.
+
+#### Pull request or package?
+
+[pr or package]: #pull-request-or-package
+
+If you're unsure about whether your changes belong in the core Umbraco CMS or if you should turn your idea into a package instead, make sure to [talk to us][making larger changes].
+
+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.
+
+#### Ownership and copyright
+
+It is your responsibility to make sure that you're allowed to share the code you're providing us. For example, you should have permission from your employer or customer to share code.
+
+Similarly, if your contribution is copied or adapted from somewhere else, make sure that the license allows you to reuse that for a contribution to Umbraco-CMS.
+
+If you're not sure, leave a note on your contribution and we will be happy to guide you.
+
+When your contribution has been accepted, it will be [MIT licensed][MIT license] from that time onwards.
+
+
+[MIT license]: ../LICENSE.md "Umbraco's license declaration"
+
+
+[issues]: https://github.com/umbraco/Umbraco-CMS/issues
+[discussions]: https://github.com/umbraco/Umbraco-CMS/discussions
diff --git a/.github/contributing-core-collabs-team.md b/.github/contributing-core-collabs-team.md
new file mode 100644
index 0000000000..1e00529ee9
--- /dev/null
+++ b/.github/contributing-core-collabs-team.md
@@ -0,0 +1,28 @@
+### The Core Collaborators team
+[Core collabs]: #the-core-collaborators-team
+
+The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan][Sebastiaan], who gets assistance from the following community members who have committed to volunteering their free time:
+
+- [Busra Sengul][Busra Sengul]
+- [Emma Garland][Emma Garland]
+- [George Bidder][George Bidder]
+- [Jason Elkin][Jason Elkin]
+- [Laura Neto][Laura Neto]
+- [Kyle Eck][Kyle Eck]
+- [Michael Latouche][Michael Latouche]
+- [Sebastiaan][Sebastiaan]
+
+
+These wonderful people aim to provide you with a 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.
+
+
+
+
+[Busra Sengul]: https://github.com/busrasengul "Busra's GitHub profile"
+[Emma Garland]: https://github.com/emmagarland "Emma's GitHub profile"
+[George Bidder]: https://github.com/georgebid "George's GitHub profile"
+[Jason Elkin]: https://github.com/jasonelkin "Jason's GitHub profile"
+[Kyle Eck]: https://github.com/teckspeed "Kyle's GitHub profile"
+[Laura Neto]: https://github.com/lauraneto "Laura's GitHub profile"
+[Michael Latouche]: https://github.com/mikecp "Michael's GitHub profile"
+[Sebastiaan]: https://github.com/nul800sebastiaan "Sebastiaan's GitHub profile"
diff --git a/.github/contributing-creating-a-pr.md b/.github/contributing-creating-a-pr.md
new file mode 100644
index 0000000000..dc1d67ea65
--- /dev/null
+++ b/.github/contributing-creating-a-pr.md
@@ -0,0 +1,51 @@
+## Creating a pull request
+
+Exciting! You're ready to show us your changes.
+
+We recommend you to [sync with our repository][sync fork] before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier.
+
+GitHub will have 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.
+
+
+We like to use [git flow][git flow] as much as possible, 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 `contrib`. This is the branch you should be targeting.
+
+Please note: we are no longer accepting features for v8 and below but will continue to merge security fixes as and when they arise.
+
+## The review process
+[review process]: #the-review-process
+
+You've sent us your contribution - congratulations! Now what?
+
+The [Core Collaborators team][Core collabs] 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.
+
+You will get an initial automated reply from our [Friendly Umbraco Robot, Umbrabot][Umbrabot], to acknowledge that we’ve seen your PR and we’ll pick it up as soon as we can. You can take this opportunity to double check everything is in order based off the handy checklist Umbrabot provides.
+
+You will get feedback as soon as the [Core Collaborators team][Core collabs] can after opening the PR. You’ll most likely get feedback within a couple of weeks. 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!). See [making larger changes][making larger changes] and [pull request or package?][pr or package]
+
+### Dealing with requested changes
+
+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!
+
+#### No longer 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.
+
+
+[ Umbrabot ]: https://github.com/umbrabot
+[git flow]: https://jeffkreeftmeijer.com/git-flow/ "An explanation of git flow"
+
+
+[making larger changes]: contributing-before-you-start.md#making-large-changes
+[pr or package]: contributing-before-you-start.md#pull-request-or-package
+[Core collabs]: contributing-core-collabs-team.md
\ No newline at end of file
diff --git a/.github/contributing-first-issue.md b/.github/contributing-first-issue.md
new file mode 100644
index 0000000000..8027d69519
--- /dev/null
+++ b/.github/contributing-first-issue.md
@@ -0,0 +1,93 @@
+## Finding your first issue: Up for grabs
+
+Umbraco HQ will regularly mark newly created issues on the issue tracker with [the `community/up-for-grabs` tag][up for grabs issues]. 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.
+
+## Making your changes
+
+Great question! The short version goes like this:
+
+1. **Fork**
+
+ Create a fork of [`Umbraco-CMS` on GitHub][Umbraco CMS repo]
+
+ 
+
+1. **Clone**
+
+ When GitHub has created your fork, you can clone it in your favorite Git tool
+
+ 
+
+1. **Switch to the correct branch**
+
+ Switch to the `contrib` branch
+
+1. **Build**
+
+ Build your fork of Umbraco locally as described in the build documentation: you can [debug with Visual Studio Code][build - debugging with code] or [with Visual Studio][build - debugging with vs].
+
+1. **Branch**
+
+ 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 issue number `12345`. Don't commit to `contrib`, create a new branch first.
+
+1. **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].
+
+1. **Commit and push**
+
+ Done? Yay! 🎉
+
+ Remember to commit to your new `temp` branch, and don't commit to `contrib`. Then you can push the changes up to your fork on GitHub.
+
+#### Keeping your Umbraco fork in sync with the main repository
+[sync fork]: #keeping-your-umbraco-fork-in-sync-with-the-main-repository
+
+Once you've already got a fork and cloned your fork locally, you can skip steps 1 and 2 going forward. Just remember to keep your fork up to date before making further changes.
+
+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
+```
+
+Then when you want to get the changes from the main repository:
+
+```
+git fetch upstream
+git rebase upstream/contrib
+```
+
+In this command we're syncing with the `contrib` branch, but you can of course choose another one if needed.
+
+[More information on how this works can be found on the thoughtbot blog.][sync fork ext]
+
+#### Style guide
+
+To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up.
+
+That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc.
+
+#### Questions?
+[questions]: #questions
+
+You can get in touch with [the core contributors team][core collabs] 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.
+- If you want to ask questions on some code you've already written you can create a draft pull request, [detailed in a GitHub blog post][draft prs].
+- Unsure where to start? Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"][contrib forum] forum. The team monitors that one closely, so one of us will be on hand and ready to point you in the right direction.
+
+
+
+
+[build - debugging with vs]: BUILD.md#debugging-with-visual-studio "Details on building and debugging Umbraco with Visual Studio"
+[build - debugging with code]: BUILD.md#debugging-with-vs-code "Details on building and debugging Umbraco with Visual Studio Code"
+
+
+[sync fork ext]: http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated "Details on keeping a git fork updated"
+[draft prs]: https://github.blog/2019-02-14-introducing-draft-pull-requests/ "Github's blog post providing details on draft pull requests"
+[contrib forum]: https://our.umbraco.com/forum/contributing-to-umbraco-cms/
+[Umbraco CMS repo]: https://github.com/umbraco/Umbraco-CMS
+[up for grabs issues]: https://github.com/umbraco/Umbraco-CMS/issues?q=is%3Aissue+is%3Aopen+label%3Acommunity%2Fup-for-grabs
\ No newline at end of file
diff --git a/.github/contributing-other-ways-to-contribute.md b/.github/contributing-other-ways-to-contribute.md
new file mode 100644
index 0000000000..773471b8f1
--- /dev/null
+++ b/.github/contributing-other-ways-to-contribute.md
@@ -0,0 +1,10 @@
+## Coding not your thing? Or want more ways to contribute?
+
+This document covers contributing to the codebase of the CMS but [the community site has plenty of inspiration for other ways to get involved.][get involved]
+
+If you don't feel you'd like to make code changes here, you can visit our [documentation repository][docs repo] and use your experience to contribute to making the docs we have, even better.
+
+We also encourage community members to feel free to comment on others' pull requests and issues - the expertise we have is not limited to the Core Collaborators 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.
+
+[get involved]: https://community.umbraco.com/get-involved/
+[docs repo]: https://github.com/umbraco/UmbracoDocs
\ No newline at end of file
diff --git a/.github/contributing-unwanted-changes.md b/.github/contributing-unwanted-changes.md
new file mode 100644
index 0000000000..c078f4600e
--- /dev/null
+++ b/.github/contributing-unwanted-changes.md
@@ -0,0 +1,18 @@
+## Unwanted changes
+While most changes are welcome, there are certain types of changes that are discouraged and might get your pull request refused.
+Of course this will depend heavily on the specific change, but please take the following examples in mind.
+
+- **Breaking changes (code and/or behavioral) đź’Ą** - sometimes it can be a bit hard to know if a change is breaking or not. Fortunately, if it relates to code, the build will fail and warn you.
+- **Large refactors 🤯** - the larger the refactor, the larger the probability of introducing new bugs/issues.
+- **Changes to obsolete code and/or property editors ✍️**
+- **Adding new config options 🦾** - while having more flexibility is (most of the times) better, having too many options can also become overwhelming/confusing, especially if there are other (good/simple) ways to achieve it.
+- **Whitespace changes 🫥** - while some of our files might not follow the formatting/whitespace rules (mostly old ones), changing several of them in one go would cause major merge conflicts with open pull requests or other work in progress. Do feel free to fix these when you are working on another issue/feature and end up "touching" those files!
+- **Adding new extension/helper methods âś‹** - keep in mind that more code also means more to maintain, so if a helper is only meaningful for a few, it might not be worth adding it to the core.
+
+While these are only a few examples, it is important to ask yourself these questions before making a pull request:
+
+- How many will benefit from this change?
+- Are there other ways to achieve this? And if so, how do they compare?
+- How maintainable is the change?
+- What would be the effort to test it properly?
+- Do the benefits outweigh the risks?
\ No newline at end of file
diff --git a/legacy/Umbraco.Tests/App.config b/legacy/Umbraco.Tests/App.config
deleted file mode 100644
index 3bb668535d..0000000000
--- a/legacy/Umbraco.Tests/App.config
+++ /dev/null
@@ -1,119 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/legacy/Umbraco.Tests/Properties/AssemblyInfo.cs b/legacy/Umbraco.Tests/Properties/AssemblyInfo.cs
deleted file mode 100644
index ec1ddca2f8..0000000000
--- a/legacy/Umbraco.Tests/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Umbraco.Tests")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Umbraco.Tests")]
-[assembly: AssemblyCopyright("Copyright © 2012")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("33ddf9b7-505c-4a12-8370-7fee9de5df6d")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
-
-// Internals must be visible to DynamicProxyGenAssembly2
-// in order to mock loggers loggers with types from the assembly
-// I.E. Mock.Of>()
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/legacy/Umbraco.Tests/Published/ModelTypeTests.cs b/legacy/Umbraco.Tests/Published/ModelTypeTests.cs
deleted file mode 100644
index 2702b82812..0000000000
--- a/legacy/Umbraco.Tests/Published/ModelTypeTests.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System;
-using System.Collections.Generic;
-using NUnit.Framework;
-using Umbraco.Cms.Core.Models.PublishedContent;
-
-namespace Umbraco.Tests.Published
-{
- [TestFixture]
- public class ModelTypeTests
- {
-
- //TODO these is not easy to move to the Unittest project due to underlysing NotImplementedException of Type.IsSZArray
- [Test]
- public void ModelTypeToStringTests()
- {
- var modelType = ModelType.For("alias1");
- var modelTypeArray = modelType.MakeArrayType();
-
- Assert.AreEqual("{alias1}", modelType.ToString());
-
- // there's an "*" there because the arrays are not true SZArray - but that changes when we map
-
- Assert.AreEqual("{alias1}[*]", modelTypeArray.ToString());
- var enumArray = typeof(IEnumerable<>).MakeGenericType(modelTypeArray);
- Assert.AreEqual("System.Collections.Generic.IEnumerable`1[{alias1}[*]]", enumArray.ToString());
- }
-
- [Test]
- public void ModelTypeFullNameTests()
- {
- Assert.AreEqual("{alias1}", ModelType.For("alias1").FullName);
-
- Type type = ModelType.For("alias1");
- Assert.AreEqual("{alias1}", type.FullName);
-
- // there's an "*" there because the arrays are not true SZArray - but that changes when we map
- Assert.AreEqual("{alias1}[*]", ModelType.For("alias1").MakeArrayType().FullName);
- // note the inner assembly qualified name
- Assert.AreEqual("System.Collections.Generic.IEnumerable`1[[{alias1}[*], Umbraco.Core, Version=0.5.0.0, Culture=neutral, PublicKeyToken=null]]", typeof(IEnumerable<>).MakeGenericType(ModelType.For("alias1").MakeArrayType()).FullName);
- }
-
- }
-}
diff --git a/legacy/Umbraco.Tests/Routing/BaseUrlProviderTest.cs b/legacy/Umbraco.Tests/Routing/BaseUrlProviderTest.cs
deleted file mode 100644
index 6be2b72ad1..0000000000
--- a/legacy/Umbraco.Tests/Routing/BaseUrlProviderTest.cs
+++ /dev/null
@@ -1,45 +0,0 @@
-using System.Linq;
-using Microsoft.Extensions.DependencyInjection;
-using Moq;
-using Umbraco.Cms.Core.Configuration.Models;
-using Umbraco.Cms.Core.Models.PublishedContent;
-using Umbraco.Cms.Core.Routing;
-using Umbraco.Cms.Core.Web;
-using Umbraco.Cms.Tests.Common;
-using Umbraco.Tests.TestHelpers;
-
-namespace Umbraco.Tests.Routing
-{
- public abstract class BaseUrlProviderTest : BaseWebTest
- {
- protected IUmbracoContextAccessor UmbracoContextAccessor { get; } = new TestUmbracoContextAccessor();
-
- protected abstract bool HideTopLevelNodeFromPath { get; }
-
- protected override void Compose()
- {
- base.Compose();
- Builder.Services.AddTransient();
- }
-
- protected override void ComposeSettings()
- {
- var contentSettings = new ContentSettings();
- var userPasswordConfigurationSettings = new UserPasswordConfigurationSettings();
-
- Builder.Services.AddTransient(x => Microsoft.Extensions.Options.Options.Create(contentSettings));
- Builder.Services.AddTransient(x => Microsoft.Extensions.Options.Options.Create(userPasswordConfigurationSettings));
- }
-
- protected IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext, DefaultUrlProvider urlProvider)
- {
- var webRoutingSettings = new WebRoutingSettings();
- return new UrlProvider(
- new TestUmbracoContextAccessor(umbracoContext),
- Microsoft.Extensions.Options.Options.Create(webRoutingSettings),
- new UrlProviderCollection(new[] { urlProvider }),
- new MediaUrlProviderCollection(Enumerable.Empty()),
- Mock.Of());
- }
- }
-}
diff --git a/legacy/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/legacy/Umbraco.Tests/Routing/MediaUrlProviderTests.cs
deleted file mode 100644
index ac79dee6cf..0000000000
--- a/legacy/Umbraco.Tests/Routing/MediaUrlProviderTests.cs
+++ /dev/null
@@ -1,206 +0,0 @@
-using System;
-using System.Linq;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-using Moq;
-using Newtonsoft.Json;
-using NUnit.Framework;
-using Umbraco.Cms.Core.Configuration.Models;
-using Umbraco.Cms.Core.IO;
-using Umbraco.Cms.Core.Models;
-using Umbraco.Cms.Core.Models.PublishedContent;
-using Umbraco.Cms.Core.PropertyEditors;
-using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
-using Umbraco.Cms.Core.Routing;
-using Umbraco.Cms.Core.Services;
-using Umbraco.Cms.Core.Strings;
-using Umbraco.Cms.Core.Web;
-using Umbraco.Cms.Tests.Common;
-using Umbraco.Cms.Tests.Common.Testing;
-using Umbraco.Tests.PublishedContent;
-using Umbraco.Tests.TestHelpers;
-using Constants = Umbraco.Cms.Core.Constants;
-
-namespace Umbraco.Tests.Routing
-{
- [TestFixture]
- [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
- public class MediaUrlProviderTests : BaseWebTest
- {
- private DefaultMediaUrlProvider _mediaUrlProvider;
-
- public override void SetUp()
- {
- base.SetUp();
-
- var loggerFactory = NullLoggerFactory.Instance;
- var mediaFileManager = new MediaFileManager(Mock.Of(), Mock.Of(),
- loggerFactory.CreateLogger(), Mock.Of());
- var contentSettings = new ContentSettings();
- var dataTypeService = Mock.Of();
- var propertyEditors = new MediaUrlGeneratorCollection(new IMediaUrlGenerator[]
- {
- new FileUploadPropertyEditor(DataValueEditorFactory, mediaFileManager, Microsoft.Extensions.Options.Options.Create(contentSettings), dataTypeService, LocalizationService, LocalizedTextService, UploadAutoFillProperties, ContentService),
- new ImageCropperPropertyEditor(DataValueEditorFactory, loggerFactory, mediaFileManager, Microsoft.Extensions.Options.Options.Create(contentSettings), dataTypeService, IOHelper, UploadAutoFillProperties, ContentService),
- });
- _mediaUrlProvider = new DefaultMediaUrlProvider(propertyEditors, UriUtility);
- }
-
- public override void TearDown()
- {
- base.TearDown();
-
- _mediaUrlProvider = null;
- }
-
- [Test]
- public void Get_Media_Url_Resolves_Url_From_Upload_Property_Editor()
- {
- const string expected = "/media/rfeiw584/test.jpg";
-
- var umbracoContext = GetUmbracoContext("/");
- var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null);
-
- var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto);
-
- Assert.AreEqual(expected, resolvedUrl);
- }
-
- [Test]
- public void Get_Media_Url_Resolves_Url_From_Image_Cropper_Property_Editor()
- {
- const string expected = "/media/rfeiw584/test.jpg";
-
- var configuration = new ImageCropperConfiguration();
- var imageCropperValue = JsonConvert.SerializeObject(new ImageCropperValue
- {
- Src = expected
- });
-
- var umbracoContext = GetUmbracoContext("/");
- var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.ImageCropper, imageCropperValue, configuration);
-
- var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto);
-
- Assert.AreEqual(expected, resolvedUrl);
- }
-
- [Test]
- public void Get_Media_Url_Can_Resolve_Absolute_Url()
- {
- const string mediaUrl = "/media/rfeiw584/test.jpg";
- var expected = $"http://localhost{mediaUrl}";
-
- var umbracoContext = GetUmbracoContext("http://localhost");
- var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, mediaUrl, null);
-
- var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Absolute);
-
- Assert.AreEqual(expected, resolvedUrl);
- }
-
- [Test]
- public void Get_Media_Url_Returns_Absolute_Url_If_Stored_Url_Is_Absolute()
- {
- const string expected = "http://localhost/media/rfeiw584/test.jpg";
-
- var umbracoContext = GetUmbracoContext("http://localhost");
- var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.UploadField, expected, null);
-
- var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Relative);
-
- Assert.AreEqual(expected, resolvedUrl);
- }
-
- [Test]
- public void Get_Media_Url_Returns_Empty_String_When_PropertyType_Is_Not_Supported()
- {
- var umbracoContext = GetUmbracoContext("/");
- var publishedContent = CreatePublishedContent(Constants.PropertyEditors.Aliases.Boolean, "0", null);
-
- var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Absolute, propertyAlias: "test");
-
- Assert.AreEqual(string.Empty, resolvedUrl);
- }
-
- [Test]
- public void Get_Media_Url_Can_Resolve_Variant_Property_Url()
- {
- var umbracoContext = GetUmbracoContext("http://localhost");
-
- var umbracoFilePropertyType = CreatePropertyType(Constants.PropertyEditors.Aliases.UploadField, null, ContentVariation.Culture);
-
- const string enMediaUrl = "/media/rfeiw584/en.jpg";
- const string daMediaUrl = "/media/uf8ewud2/da.jpg";
-
- var property = new SolidPublishedPropertyWithLanguageVariants
- {
- Alias = "umbracoFile",
- PropertyType = umbracoFilePropertyType,
- };
-
- property.SetSourceValue("en", enMediaUrl, true);
- property.SetSourceValue("da", daMediaUrl);
-
- var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), new [] { umbracoFilePropertyType }, ContentVariation.Culture);
- var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}};
-
- var resolvedUrl = GetPublishedUrlProvider(umbracoContext).GetMediaUrl(publishedContent, UrlMode.Auto, "da");
- Assert.AreEqual(daMediaUrl, resolvedUrl);
- }
-
- private IPublishedUrlProvider GetPublishedUrlProvider(IUmbracoContext umbracoContext)
- {
- var webRoutingSettings = new WebRoutingSettings();
- return new UrlProvider(
- new TestUmbracoContextAccessor(umbracoContext),
- Microsoft.Extensions.Options.Options.Create(webRoutingSettings),
- new UrlProviderCollection(Enumerable.Empty()),
- new MediaUrlProviderCollection(new []{_mediaUrlProvider}),
- Mock.Of()
- );
- }
-
- private static IPublishedContent CreatePublishedContent(string propertyEditorAlias, string propertyValue, object dataTypeConfiguration)
- {
- var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing);
-
- var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(),
- new[] {umbracoFilePropertyType}, ContentVariation.Nothing);
-
- return new SolidPublishedContent(contentType)
- {
- Id = 1234,
- Key = Guid.NewGuid(),
- Properties = new[]
- {
- new SolidPublishedProperty
- {
- Alias = "umbracoFile",
- SolidSourceValue = propertyValue,
- SolidHasValue = true,
- PropertyType = umbracoFilePropertyType
- }
- }
- };
- }
-
- private static PublishedPropertyType CreatePropertyType(string propertyEditorAlias, object dataTypeConfiguration, ContentVariation variation)
- {
- var uploadDataType = new PublishedDataType(1234, propertyEditorAlias, new Lazy(() => dataTypeConfiguration));
-
- var propertyValueConverters = new PropertyValueConverterCollection(new IPropertyValueConverter[]
- {
- new UploadPropertyConverter(),
- new ImageCropperValueConverter(Mock.Of>()),
- });
-
- var publishedModelFactory = Mock.Of();
- var publishedContentTypeFactory = new Mock();
- publishedContentTypeFactory.Setup(x => x.GetDataType(It.IsAny()))
- .Returns(uploadDataType);
-
- return new PublishedPropertyType("umbracoFile", 42, true, variation, propertyValueConverters, publishedModelFactory, publishedContentTypeFactory.Object);
- }
- }
-}
diff --git a/legacy/Umbraco.Tests/Umbraco.Tests.csproj b/legacy/Umbraco.Tests/Umbraco.Tests.csproj
deleted file mode 100644
index a83c0d6453..0000000000
--- a/legacy/Umbraco.Tests/Umbraco.Tests.csproj
+++ /dev/null
@@ -1,199 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- 8.0.30703
- 2.0
- {5D3B8245-ADA6-453F-A008-50ED04BFE770}
- Library
- Properties
- Umbraco.Tests
- Umbraco.Tests
- v4.7.2
- 512
- ..\
- publish\
- true
- Disk
- false
- Foreground
- 7
- Days
- false
- false
- true
- 0
- 1.0.0.%2a
- false
- false
- true
-
-
-
-
-
- true
- true
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
- false
- 8
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
- false
- latest
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 2.0.0
-
-
-
-
- 1.11.31
-
-
-
-
-
-
-
-
-
-
-
- 5.0.0
-
-
-
-
- 5.0.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Designer
-
-
- Always
-
-
-
-
- {29aa69d9-b597-4395-8d42-43b1263c240a}
- Umbraco.Core
-
-
- {0fad7d2a-d7dd-45b1-91fd-488bb6cdacea}
- Umbraco.Examine.Lucene
-
-
- {3ae7bf57-966b-45a5-910a-954d7c554441}
- Umbraco.Infrastructure
-
-
- {33085570-9bf2-4065-a9b0-a29d920d13ba}
- Umbraco.Persistence.SqlCe
-
-
- {f6de8da0-07cc-4ef2-8a59-2bc81dbb3830}
- Umbraco.PublishedCache.NuCache
-
-
- {a499779c-1b3b-48a8-b551-458e582e6e96}
- Umbraco.Tests.Common
-
-
- {651E1350-91B6-44B7-BD60-7207006D7003}
- Umbraco.Web
-
-
-
-
-
-
-
-
- $(NuGetPackageFolders.Split(';')[0])
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/legacy/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs b/legacy/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs
deleted file mode 100644
index a138c6ce94..0000000000
--- a/legacy/Umbraco.Tests/Web/Controllers/AuthenticationControllerTests.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using Moq;
-using NUnit.Framework;
-using Umbraco.Cms.Core.Features;
-using Umbraco.Cms.Core.Services;
-using Umbraco.Cms.Tests.Common.Testing;
-using Umbraco.Extensions;
-using Umbraco.Tests.TestHelpers;
-using Umbraco.Tests.Testing;
-
-namespace Umbraco.Tests.Web.Controllers
-{
- [TestFixture]
- [UmbracoTest(Database = UmbracoTestOptions.Database.None)]
- public class AuthenticationControllerTests : TestWithDatabaseBase
- {
- protected override void ComposeApplication(bool withApplication)
- {
- base.ComposeApplication(withApplication);
- //if (!withApplication) return;
-
- // replace the true IUserService implementation with a mock
- // so that each test can configure the service to their liking
- Builder.Services.AddUnique(f => Mock.Of());
-
- // kill the true IEntityService too
- Builder.Services.AddUnique(f => Mock.Of());
-
- Builder.Services.AddUnique();
- }
-
-
- // TODO Reintroduce when moved to .NET Core
- // [Test]
- // public async System.Threading.Tasks.Task GetCurrentUser_Fips()
- // {
- // ApiController CtrlFactory(HttpRequestMessage message, IUmbracoContextAccessor umbracoContextAccessor)
- // {
- // //setup some mocks
- // var userServiceMock = Mock.Get(ServiceContext.UserService);
- // userServiceMock.Setup(service => service.GetUserById(It.IsAny()))
- // .Returns(() => null);
- //
- // if (Thread.GetDomain().GetData(".appPath") != null)
- // {
- // HttpContext.Current = new HttpContext(new SimpleWorkerRequest("", "", new StringWriter()));
- // }
- // else
- // {
- // var baseDir = IOHelper.MapPath("").TrimEnd(Path.DirectorySeparatorChar);
- // HttpContext.Current = new HttpContext(new SimpleWorkerRequest("/", baseDir, "", "", new StringWriter()));
- // }
- //
- // var usersController = new AuthenticationController(
- // new TestUserPasswordConfig(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // umbracoContextAccessor,
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance(),
- // Factory.GetInstance()
- // );
- // return usersController;
- // }
- //
- // Mock.Get(Current.SqlContext)
- // .Setup(x => x.Query())
- // .Returns(new Query(Current.SqlContext));
- //
- // var syntax = new SqlCeSyntaxProvider();
- //
- // Mock.Get(Current.SqlContext)
- // .Setup(x => x.SqlSyntax)
- // .Returns(syntax);
- //
- // var mappers = new MapperCollection(new[]
- // {
- // new UserMapper(new Lazy(() => Current.SqlContext), new ConcurrentDictionary>())
- // });
- //
- // Mock.Get(Current.SqlContext)
- // .Setup(x => x.Mappers)
- // .Returns(mappers);
- //
- // // Testing what happens if the system were configured to only use FIPS-compliant algorithms
- // var typ = typeof(CryptoConfig);
- // var flds = typ.GetFields(BindingFlags.Static | BindingFlags.NonPublic);
- // var haveFld = flds.FirstOrDefault(f => f.Name == "s_haveFipsAlgorithmPolicy");
- // var isFld = flds.FirstOrDefault(f => f.Name == "s_fipsAlgorithmPolicy");
- // var originalFipsValue = CryptoConfig.AllowOnlyFipsAlgorithms;
- //
- // try
- // {
- // if (!originalFipsValue)
- // {
- // haveFld.SetValue(null, true);
- // isFld.SetValue(null, true);
- // }
- //
- // var runner = new TestRunner(CtrlFactory);
- // var response = await runner.Execute("Authentication", "GetCurrentUser", HttpMethod.Get);
- //
- // var obj = JsonConvert.DeserializeObject(response.Item2);
- // Assert.AreEqual(-1, obj.UserId);
- // }
- // finally
- // {
- // if (!originalFipsValue)
- // {
- // haveFld.SetValue(null, false);
- // isFld.SetValue(null, false);
- // }
- // }
- // }
- }
-}
diff --git a/legacy/Umbraco.Tests/unit-test-log4net.CI.config b/legacy/Umbraco.Tests/unit-test-log4net.CI.config
deleted file mode 100644
index d7035032ef..0000000000
--- a/legacy/Umbraco.Tests/unit-test-log4net.CI.config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/legacy/Umbraco.Tests/unit-test-logger.config b/legacy/Umbraco.Tests/unit-test-logger.config
deleted file mode 100644
index 62fa1353b2..0000000000
--- a/legacy/Umbraco.Tests/unit-test-logger.config
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs
index fcd5ba8ccc..e2ac5ab870 100644
--- a/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs
+++ b/src/Umbraco.Cms.Api.Common/OpenApi/EnumSchemaFilter.cs
@@ -12,6 +12,8 @@ public class EnumSchemaFilter : ISchemaFilter
{
if (context.Type.IsEnum)
{
+ model.Type = "string";
+ model.Format = null;
model.Enum.Clear();
foreach (var name in Enum.GetNames(context.Type))
{
diff --git a/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs b/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs
index f29e0465f5..d4d63acf2b 100644
--- a/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Querying/QueryOptionBase.cs
@@ -10,7 +10,8 @@ public abstract class QueryOptionBase
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IRequestRoutingService _requestRoutingService;
- public QueryOptionBase(IPublishedSnapshotAccessor publishedSnapshotAccessor,
+ public QueryOptionBase(
+ IPublishedSnapshotAccessor publishedSnapshotAccessor,
IRequestRoutingService requestRoutingService)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
diff --git a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs
index 468f52021f..834a68e2e2 100644
--- a/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs
+++ b/src/Umbraco.Cms.Persistence.EFCore.Sqlite/SqliteMigrationProvider.cs
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Umbraco.Cms.Core;
using Umbraco.Cms.Persistence.EFCore.Migrations;
using Umbraco.Extensions;
+using Umbraco.Cms.Persistence.EFCore;
namespace Umbraco.Cms.Persistence.EFCore.Sqlite;
diff --git a/src/Umbraco.Cms.Persistence.EFCore/Constants-ProviderNames.cs b/src/Umbraco.Cms.Persistence.EFCore/Constants-ProviderNames.cs
new file mode 100644
index 0000000000..0f29e5944c
--- /dev/null
+++ b/src/Umbraco.Cms.Persistence.EFCore/Constants-ProviderNames.cs
@@ -0,0 +1,11 @@
+namespace Umbraco.Cms.Persistence.EFCore;
+
+public static partial class Constants
+{
+ public static class ProviderNames
+ {
+ public const string SQLLite = "Microsoft.Data.Sqlite";
+
+ public const string SQLServer = "Microsoft.Data.SqlClient";
+ }
+}
diff --git a/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoRichTextBlock.html b/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoRichTextBlock.html
index adda418723..83e067ac23 100644
--- a/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoRichTextBlock.html
+++ b/src/Umbraco.Cms.StaticAssets/wwwroot/App_Plugins/Umbraco.BlockGridEditor.DefaultCustomViews/umbBlockGridDemoRichTextBlock.html
@@ -21,5 +21,5 @@
-
+
diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs
index e22df693e9..e3b7ddef9b 100644
--- a/src/Umbraco.Core/Composing/TypeFinder.cs
+++ b/src/Umbraco.Core/Composing/TypeFinder.cs
@@ -34,7 +34,7 @@ public class TypeFinder : ITypeFinder
"ServiceStack.", "SqlCE4Umbraco,", "Superpower,", // used by Serilog
"System.", "TidyNet,", "TidyNet.", "WebDriver,", "itextsharp,", "mscorlib,", "NUnit,", "NUnit.", "NUnit3.",
"Selenium.", "ImageProcessor", "MiniProfiler.", "Owin,", "SQLite",
- "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
+ "ReSharperTestRunner", "ReSharperTestRunner32", "ReSharperTestRunner64", // These are used by the Jetbrains Rider IDE and Visual Studio ReSharper Extension
};
private static readonly ConcurrentDictionary
TypeNamesCache = new();
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
index d8623a90a7..19edc6f1ba 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
@@ -17,7 +17,7 @@
Opret gruppe
Slet
Deaktivér
- Edit settings
+ Redigér indstillinger
Tøm papirkurv
Aktivér
Eksportér dokumenttype
@@ -26,7 +26,7 @@
Redigér i Canvas
Log af
Flyt
- Notificeringer
+ Notifikationer
Offentlig adgang
Udgiv
Afpublicér
@@ -84,7 +84,7 @@
Tillad adgang til at oversætte en node
Tillad adgang til at gemme en node
Tillad adgang til at oprette en indholdsskabelon
- Tillad adgang til at oprette notificeringer for noder
+ Tillad adgang til at oprette notifikationer for noder
Indhold
@@ -105,10 +105,10 @@
Domænet '%0%' er nu opdateret
eller rediger nuværende domæner
+ Yderligere understøttes også første niveau af stien efter domænet, f.eks. "example.com/en" eller "/en". ]]>
Nedarv
Sprog
- eller nedarv sprog fra forældre noder. Gælder også
+ eller nedarv sprog fra forældernoder. Gælder også
for den aktuelle node, medmindre et domæne nedenfor også indstiller et sprog.]]>
Domæner
@@ -120,7 +120,7 @@
Fortryd indryk afsnit
Indsæt formularfelt
Indsæt grafisk overskrift
- Redigér Html
+ Redigér HTML
Indryk afsnit
Kursiv
Centrér
@@ -154,7 +154,7 @@
Slet tag
Fortryd
Bekræft
- Flere publiseringsmuligheder
+ Flere publiceringsmuligheder
Indsæt
@@ -243,21 +243,21 @@
Ups: dette dokument er udgivet, men er ikke i cachen (intern fejl)
Kunne ikke hente URL'en
Dette dokument er udgivet, men dets URL ville kollidere med indholdet %0%
- Dette dokument er udgivet, men dets URL kan ikke dirigeres
+ Dette dokument er udgivet, men dets URL kan ikke genereres
Udgiv
Udgivet
Udgivet (Ventede ændringer)
Udgivelsesstatus
- Udgiv med undersider for at udgive %0% og alle sider under og dermed gøre deres indhold offentligt tilgængelige.]]>
+ Udgiv med undersider for at udgive %0% og alle sider under, og dermed gøre deres indhold offentligt tilgængeligt.]]>
- Udgiv med undersider for at udgive de valgte sprog og de samme sprog for sider under og dermed gøre deres indhold offentligt tilgængelige.]]>
+ Udgiv med undersider for at udgive de valgte sprog og de samme sprog for sider under, og dermed gøre deres indhold offentligt tilgængeligt.]]>
Udgivelsesdato
Afpubliceringsdato
Fjern dato
Vælg dato
Sorteringsrækkefølgen er opdateret
- For at sortere, træk siderne eller klik på en af kolonnehovederne. Du kan vælge flere sider
+ For at sortere, træk siderne eller klik på en af kolonneoverskrifterne. Du kan vælge flere sider
ved at holde "shift" eller "control" nede mens du vælger.
Statistik
@@ -298,8 +298,8 @@
Fjern denne tekstboks
Indholdsrod
Inkluder ikke-udgivet indhold.
- Denne værdi er skjult.Hvis du har brug for adgang til at se denne værdi, bedes du
- kontakte din web-administrator.
+ Denne værdi er skjult. Hvis du har brug for adgang til at se denne værdi, bedes du
+ kontakte din administrator.
Denne værdi er skjult.
Hvilke sprog vil du gerne udgive?
@@ -334,11 +334,11 @@
Klik for at uploade
eller klik her for at vælge filer
- Kan ikke uploade denne fil, den har ikke en godkendt filtype
- Maks filstørrelse er
+ Kan ikke uploade denne fil; den har ikke en godkendt filtype
+ Maksimal filstørrelse er
Medie rod
- Overordnet og destinations mappe kan ikke være den samme
- Oprettelse af mappen under parent med id %0% fejlede
+ Overordnet og destinationsmappe kan ikke være den samme
+ Oprettelse af mappen under node med id %0% fejlede
Omdøbning af mappen med id %0% fejlede
Træk dine filer ind i dropzonen for, at uploade dem til
mediebiblioteket.
@@ -348,7 +348,7 @@
Opret et nyt medlem
Alle medlemmer
- Medlemgrupper har ingen yderligere egenskaber til redigering.
+ Medlemsgrupper har ingen yderligere egenskaber til redigering.
Totrinsbekræftelse
@@ -358,7 +358,7 @@
Kopiering af medietypen fejlede
Flytning af medietypen fejlede
- Auto vælg
+ Vælg automatisk
Kopiering af medlemstypen fejlede
@@ -370,18 +370,18 @@
Angiv et navn for mappen
Vælg en type og skriv en titel
- "dokumenttyper".]]>
+ Indstillinger under "Dokumenttyper" .]]>
- Document Types within the Settings section.]]>
+ Dokumenttyper i Indstillinger -sektionen.]]>
Den valgte side i træet tillader ikke at sider oprettes under
den.
Rediger tilladelser for denne dokumenttype.
Opret en ny dokumenttype
- Dokumenttyper inde i Indstillinger sektionen, ved at ændre Tillad på rodniveau indestillingen under Permissions .]]>
+ Dokumenttyper inde i Indstillinger sektionen, ved at ændre Tillad på rodniveau indstillingen under Permissions .]]>
- "media typer".]]>
+ "Medietyper".]]>
Det valgte medie i træet tillader ikke at medier oprettes under det.
Rediger tilladelser for denne medietype.
@@ -436,7 +436,7 @@
Udgivelse vil gøre de valgte sider synlige på sitet.
Afpublicering vil fjerne de valgte sider og deres undersider fra sitet.
- Afpublicering vil fjerne denne side og alle dets undersider fra websitet.
+ Afpublicering vil fjerne denne side og alle dens undersider fra websitet.
Du har ikke-gemte ændringer. Hvis du ændrer dokumenttype, kasseres ændringerne.
@@ -871,6 +871,7 @@
Avatar til
Overskrift
system felt
+ Primær
BlĂĄ
@@ -1084,8 +1085,8 @@
Relater det kopierede element til originalen
- %0%]]>
- Notificeringer er gemt for
+ %0%]]>
+ Notifikationer er gemt for
RET
Opdateringssammendrag:
Hav en fortsat god dag! De bedste hilsner fra Umbraco robotten
]]>
[%0%] Notificering om %1% udført på %2%
- Notificeringer
+ Notifikationer
Handlinger
@@ -2104,25 +2105,25 @@ Mange hilsner fra Umbraco robotten
Test bestĂĄet
Test fejlet
Åben backoffice søgning
- Åben/Luk backoffice hjælp
- Ă…ben/Luk dine profil indstillinger
+ Åben/luk backoffice hjælp
+ Ă…ben/luk dine profilindstillinger
Tilføj domæne på %0%
Opret ny node under %0%
Opsæt offentlig adgang på %0%
Opsæt rettigheder på %0%
- Juster soterings rækkefølgen for %0%
- Opret indholds skabelon baseret pĂĄ %0%
- Ă…ben kontext menu for
+ Juster sorteringsrækkefølgen for %0%
+ Opret indholdsskabelon baseret pĂĄ %0%
+ Ă…ben kontekstmenu for
Aktivt sprog
Skift sprog til
Opret ny mappe
- Delvist View
- Delvist View Macro
+ Partial View
+ Partial View Macro
Medlem
Data type
Søg i viderestillings dashboardet
- Søg i brugergruppe sektionen
- Søg i bruger sektionen
+ Søg i brugergruppesektionen
+ Søg i brugersektionen
Opret element
Opret
Rediger
@@ -2146,16 +2147,16 @@ Mange hilsner fra Umbraco robotten
Referencer
- Denne Data Type har ingen referencer.
- Brugt i Medie Typer
- Brugt i Medlems Typer
- Brugt af
- Brugt i Dokumenter
- Brugt i Medlemmer
- Brugt i Medier
+ Denne Datatype har ingen referencer.
+ Bruges i Medietyper
+ Bruges i Medlemstyper
+ Bruges af
+ Bruges i Dokumenter
+ Bruges i Medlemmer
+ Bruges i Medier
- Slet gemte søgning
+ Slet gemt søgning
Log type
Vælg alle
Fravælg alle
@@ -2187,7 +2188,7 @@ Mange hilsner fra Umbraco robotten
Slet denne søgning
Find logs med request Id
Find logs med Namespace
- Find logs med maskin navn
+ Find logs med maskinnavn
Ă…ben
Henter
Hver 2 sekunder
@@ -2224,19 +2225,19 @@ Mange hilsner fra Umbraco robotten
Redigerings udseende
Data modeller
katalog udseende
- Baggrunds farve
+ Baggrundsfarve
Ikon farve
Indholds model
Label
Speciel visning
Vis speciel visning beskrivelsen
- Overskrift hvordan denne block præsenteres i backoffice interfacet. Vælg en
- .html fil der indeholder din præsensation.
+ Overskriv hvordan denne blok præsenteres i backoffice. Vælg en
+ .html fil der indeholder din visning.
Indstillings model
- Rederings lagets størrelse
+ Redigeringsvinduets størrelse
Tilføj speciel visning
- Tilføj instillinger
+ Tilføj indstillinger
%0%?]]>
@@ -2261,7 +2262,7 @@ Mange hilsner fra Umbraco robotten
Skjul indholdseditoren
Skjul indholds redigerings knappen samt indholdseditoren i Blok Redigerings vinduet
Direkte redigering
- Tilføjer direkte redigering a det første felt. Yderligere felter optræder kun i redigerings vinduet.
+ Tilføjer direkte redigering af det første felt. Yderligere felter optræder kun i redigerings vinduet.
Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem?
Annuller oprettelse?
@@ -2284,11 +2285,11 @@ Mange hilsner fra Umbraco robotten
Alle blokke, der er oprettet i dette omrĂĄde, vil blive slettet.
Layout-opsætning
Struktur
- Størrelses opsætning
+ Størrelsesopsætning
Tilgængelige kolonne-størrelser
- Vælg de forskellige antal kolonner denne blok må optage i layoutet. Dette forhindre ikke blokken i at optræde i et mindre område.
- TIlgængelige række-størrelser
- Vælg hvor mange rækker denne blok på optage i layoutet.
+ Vælg de forskellige antal kolonner denne blok må optage i layoutet. Dette forhindrer ikke blokken i at optræde i et mindre område.
+ Tilgængelige række-størrelser
+ Vælg hvor mange rækker denne blok må optage i layoutet.
Tillad pĂĄ rodniveau
Gør denne blok tilgængelig i layoutets rodniveau. Hvis dette ikke er valgt, kan denne blok kun bruges inden for andre blokkes definerede områder.
Blok-omrĂĄder
@@ -2302,11 +2303,11 @@ Mange hilsner fra Umbraco robotten
-
+
Træk for at skalere
Tilføj indhold label
- Overskriv labellen for tilføj indholds knappen i dette område.
- Tilføj skalerings muligheder
+ Overskriv labelen for tilføj indholds knappen i dette område.
+ Tilføj skaleringsmuligheder
Tilføj Blok
Tilføj gruppe
Tilføj gruppe eller Blok
@@ -2319,14 +2320,14 @@ Mange hilsner fra Umbraco robotten
Avanceret
Tilladelser
Installer demo konfiguration
- Dette indeholder Blokke for Overskrift, Beriget-Tekst, Billede og To-Koloners-Layout.]]>
+ Dette indeholder Blokke for Overskrift, Formateret Tekst, Billede og To-Kolonners-Layout.]]>
Installer
- Sortings tilstand
- Afslut sortings tilstand
+ Sortingstilstand
+ Afslut sortingstilstand
Dette område alias skal være unikt sammenlignet med andre områder af denne Blok.
Konfigurer omrĂĄde
Slet omrĂĄde
- Tilføj mulighed for %0% koloner
+ Tilføj mulighed for %0% kolonner
Indsæt Blok
Vis pĂĄ linje med tekst
@@ -2358,12 +2359,12 @@ Mange hilsner fra Umbraco robotten
Vis i nyt vindue
Ă…ben forhĂĄndsvisning i nyt vindue
ForhĂĄndsvisning af indholdet?
- Du har afslutet forhĂĄndsvisning, vil du starte forhĂĄndsvisning igen for at
+ Du har afsluttet forhĂĄndsvisning, vil du starte forhĂĄndsvisning igen for at
se seneste gemte version af indholdet?
Se udgivet indhold
Se udgivet indhold?
- Du er i forhĂĄndsvisning, vil du afslutte for at se den udgivet
+ Du er i forhĂĄndsvisning, vil du afslutte for at se den udgivne
version?
Se udgivet version
@@ -2373,7 +2374,7 @@ Mange hilsner fra Umbraco robotten
Mappeoprettelse
Filskrivning for pakker
Filskrivning
- Medie mappeoprettelse
+ Mediemappeoprettelse
resultat
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
index 00cb113ddf..35ccb53a3b 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
@@ -56,6 +56,8 @@
Create Content Template
Resend Invitation
Hide unavailable options
+ Change Data Type
+ Edit content
Content
@@ -158,6 +160,7 @@
Confirm
More publishing options
Submit
+ Generate models and close
Media deleted
@@ -198,6 +201,8 @@
Save
Save
History (all variants)
+ Content unpublished for languages: %0%
+ Unpublish
The folder name cannot contain illegal characters.
@@ -325,6 +330,12 @@
Create new
Paste from clipboard
This item is in the Recycle Bin
+ No content can be added for this item
+ Save is not allowed
+ Publish is not allowed
+ Send for approval is not allowed
+ Schedule is not allowed
+ Unpublish is not allowed
%0%]]>
@@ -349,12 +360,19 @@
Failed to rename the folder with id %0%
Drag and drop your file(s) into the area
One or more file security validations have failed
+ Parent and destination folders cannot be the same
+ Upload is not allowed in this location.
Create a new member
All Members
Member groups have no additional properties for editing.
Two-Factor Authentication
+ A member with this login already exists
+ The member is already in group '%0%'
+ The member already has a password set
+ Lockout is not enabled for this member
+ The member is not in group '%0%'
Failed to copy content type
@@ -476,7 +494,6 @@
Link
Anchor / querystring
Name
- Manage hostnames
Close this window
Are you sure you want to delete
%0% of %1% items]]>
@@ -581,6 +598,7 @@
Yes, remove
You are deleting the layout
Modifying layout will result in loss of data for any existing content that is based on this configuration.
+ Select configuration
@@ -689,6 +707,9 @@
Select the folder to move
to in the tree structure below
was moved underneath
+ Changing a property editor on a data type with stored values is disabled. To allow this you can change the Umbraco:CMS:DataTypes:CanBeChanged setting in appsettings.json.
+ %0% will delete the properties and their data from the following items]]>
+ I understand this action will delete the properties and data based on this Data Type
Your data has been saved, but before you can publish this page there are some
@@ -729,6 +750,8 @@
Please place cursor at the left of the two cells you wish to merge
You cannot split a cell that hasn't been merged.
This property is invalid
+ An unknown failure has occurred
+ Optimistic concurrency failure, object has been modified
About
@@ -893,6 +916,13 @@
Last Updated
Skip to menu
Skip to content
+ Primary
+ Change
+ Crop section
+ Generic
+ Media
+ Revert
+ Validate
Blue
@@ -919,6 +949,7 @@
General
Editor
Toggle allow culture variants
+ Add tab
Background colour
@@ -1368,6 +1399,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
so uninstall with caution. If in doubt, contact the package author.]]>
Package version
Verified to work on Umbraco Cloud
+ Package migrations have successfully completed.
+ All package migrations have successfully completed.
Paste with full formatting (Not recommended)
@@ -1425,6 +1458,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Publish to publish %0% and thereby making its content publicly available.
You can publish this page and all its subpages by checking Include unpublished subpages below.
]]>
+ Insufficient user permissions to publish all descendant documents
+ %0% could not be published because the item is in the recycle bin.
+ Validation failed for required language '%0%'. This language was saved but not published.
You have not configured any approved colours
@@ -1517,25 +1553,22 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Rollback to
Select version
View
+
+ Versions
+ Current draft version
+ Current published version
Edit script file
- Concierge
Content
- Courier
- Developer
Forms
- Help
- Umbraco Configuration Wizard
Media
Members
- Newsletters
Packages
Marketplace
Settings
- Statistics
Translation
Users
@@ -1576,6 +1609,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
column headers to sort the entire collection of items
+ This node has no child nodes to sort
Validation
@@ -1587,7 +1621,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Operation was cancelled by a 3rd party add-in
This file is being uploaded as part of a folder, but creating a new folder is not allowed here
Creating a new folder is not allowed here
- Publishing was cancelled by a 3rd party add-in
Property type already exists
Property type created
DataType: %1%]]>
@@ -1601,7 +1634,6 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Stylesheet saved without any errors
Datatype saved
Dictionary item saved
- Publishing failed because the parent page isn't published
Content published
and visible on the website
Content Template saved
@@ -1668,7 +1700,25 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Your system information has successfully been copied to the clipboard
Could not copy your system information to the clipboard
- Webhook saved
+ Webhook saved
+ Saved. To view the changes please reload your browser
+ %0% documents published and visible on the website
+ %0% published and visible on the website
+ %0% documents published for languages %1% and visible on the website
+ A schedule for publishing has been updated
+ %0% saved
+ %0% changes have been sent for approval
+ Content variation %0% unpublished
+ The mandatory language '%0%' was unpublished. All languages for this content item are now unpublished.
+ Cannot publish the document since the required '%0%' is not published
+ Validation failed for language '%0%'
+ The release date cannot be in the past
+ Cannot schedule the document for publishing since the required '%0%' is not published
+ Cannot schedule the document for publishing since the required '%0%' has a publish date later than a non mandatory language
+ The expire date cannot be in the past
+ The expire date cannot be before the release date
+ An error occurred while enabling version cleanup for %0%
+ An error occurred while disabling version cleanup for %0%
Add style
@@ -1932,6 +1982,18 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Prevent cleanup
Enable cleanup
NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]>
+ You can't move the group %0% to this tab because the group will get the same alias as a tab: "%1%". Rename the group to continue.
+ Available configurations
+ Create a new configuration
+ %0%?]]>
+ %0%?]]>
+ %0%?]]>
+ This will also delete all items below this tab.
+ This will also delete all items below this group.
+ Add tab
+ Convert to tab
+ Drag properties here to place directly on the tab
+ Changing a data type with stored values is disabled. To allow this you can change the Umbraco:CMS:DataTypes:CanBeChanged setting in appsettings.json.
Create webhook
@@ -1967,6 +2029,11 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Fall back language
none
+ %0% is shared across languages and segments.]]>
+ %0% is shared across all languages.]]>
+ %0% is shared across all segments.]]>
+ Shared: Languages
+ Shared: Segments
Add parameter
@@ -2320,10 +2387,22 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
This two-factor provider is now disabled
Something went wrong with trying to disable this two-factor provider
Do you want to disable this two-factor provider for this user?
+ A user with this login already exists
+ The password must have at least one digit ('0'-'9')
+ The password must have at least one lowercase ('a'-'z')
+ The password must have at least one non alphanumeric character
+ The password must use at least %0% different characters
+ The password must have at least one uppercase ('A'-'Z')
+ The password must be at least %0% characters long
+ Allow access to all languages
+ The user already has a password set
+ The user is already in group '%0%'
+ Lockout is not enabled for this user
+ The user is not in group '%0%'
+ Configure Two-Factor
Validation
- No validation
Validate as an email address
Validate as a number
Validate as a URL
@@ -2350,6 +2429,14 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
%1% more.]]>
%1% too many.]]>
The content amount requirements are not met for one or more areas.
+ Invalid member group name
+ Invalid user group name
+ Invalid token
+ Invalid username
+ Email '%0%' is already taken
+ User group name '%0%' is already taken
+ Member group name '%0%' is already taken
+ Username '%0%' is already taken
+
+
+
+
+ Show password
+ Hide password
+
+
+
At least {{installer.current.model.minCharLength}} characters
long
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 29294a08c4..5688ea1b12 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
@@ -1,5 +1,5 @@
-
-
+
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 ff647ed411..24a3cb4265 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
@@ -1,8 +1,8 @@
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html
index 5a6a0fc766..48430b579e 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-search.html
@@ -12,7 +12,7 @@
-
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html
index a58aac7b8d..fd2a87d5b2 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-sections.html
@@ -1,5 +1,5 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less
index 1e9538d670..42ba81a6bb 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less
+++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card-grid.less
@@ -1,7 +1,7 @@
.umb-block-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- grid-auto-rows: minmax(30px, auto);
+ grid-auto-rows: minmax(150px, auto);
gap: 20px;
margin-bottom: 20px;
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less
index a40983e07c..7be8008b8a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less
+++ b/src/Umbraco.Web.UI.Client/src/views/components/blockcard/umb-block-card.less
@@ -104,6 +104,7 @@ umb-block-card {
font-size: 14px;
color: @ui-action-type;
margin: 0 16px -1px;
+ word-wrap: break-word;
}
.__subname {
color: @gray-4;
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle-group.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle-group.html
index 4576a30418..637404718b 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle-group.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle-group.html
@@ -4,10 +4,11 @@
checked="item.checked"
disabled="item.disabled"
input-id="{{item.inputId}}"
+ aria-labelledby="{{item.labelId}}"
on-click="change(item)">
-
{{ item.name }}
+
{{ item.name }}
{{ item.description }}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.less b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.less
index 13f934c251..b0ce88a446 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.less
+++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umbimagepreview/umb-image-preview.less
@@ -5,5 +5,6 @@
img {
width: 100%;
+ max-height: 50vh;
}
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html
index 339ee29c90..4462af5429 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-table.html
@@ -6,7 +6,8 @@
-
+ Select all
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html
index 985eec7e99..89f95f688d 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html
@@ -20,7 +20,7 @@
-
+
-
+
- {{item.name}}
+ {{item.name}}
-
+
@@ -63,7 +63,7 @@
Has translation
Missing translation
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js
index 435ece4bb9..df26473720 100644
--- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.controller.js
@@ -9,8 +9,9 @@
function MediaTypesCreateController($scope, $location, navigationService, mediaTypeResource, formHelper, appState, localizationService) {
$scope.model = {
- folderName: "",
- creatingFolder: false
+ allowCreateFolder: $scope.currentNode.parentId === null || $scope.currentNode.nodeType === 'container',
+ folderName: "",
+ creatingFolder: false
};
var node = $scope.currentNode;
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html
index bd6ef087f4..36a0fce9f2 100644
--- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html
+++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html
@@ -10,20 +10,21 @@
-
-
+
+
+ New
+ Media type
+
-
-
+
+
-
+
@@ -35,6 +36,7 @@
Do something else
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js
index 550ad7ed35..4210d162d3 100644
--- a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.controller.js
@@ -9,8 +9,9 @@
function MemberTypesCreateController($scope, $location, navigationService, memberTypeResource, formHelper, appState, localizationService) {
$scope.model = {
- folderName: "",
- creatingFolder: false
+ allowCreateFolder: $scope.currentNode.parentId === null || $scope.currentNode.nodeType === 'container',
+ folderName: "",
+ creatingFolder: false
};
var node = $scope.currentNode;
diff --git a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html
index 5bcf6c42bf..ac6f633db8 100644
--- a/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html
+++ b/src/Umbraco.Web.UI.Client/src/views/memberTypes/create.html
@@ -1,4 +1,5 @@
+
@@ -6,7 +7,16 @@
-
+
+
+
+
+
+
+
-
-
-
-
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
index ade5e9829a..3f0584556c 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.controller.js
@@ -199,12 +199,9 @@
localizationService.localize("blockEditor_headlineCreateBlock").then(function(localizedTitle) {
- const contentTypePicker = {
+ const dialog = {
title: localizedTitle,
- section: "settings",
- treeAlias: "documentTypes",
entityType: "documentType",
- isDialog: true,
filter: function (node) {
if (node.metaData.isElement === true) {
var key = udiService.getKey(node.udi);
@@ -238,8 +235,8 @@
}
]
};
- editorService.treePicker(contentTypePicker);
+ editorService.contentTypePicker(dialog);
});
};
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.less
index 43151fcabb..771d6c2af8 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.less
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.less
@@ -1,32 +1,29 @@
.umb-block-grid-block-configuration {
+ margin-bottom: 20px;
- margin-bottom: 20px;
+ uui-button {
+ --uui-button-border-radius: 6px;
+ font-weight: 700;
+ min-height: 80px;
+ }
- uui-button {
- font-weight: 700;
- --uui-button-border-radius: 6px;
- min-height: 80px;
+ .__get-sample-box {
+ position: relative;
+ border: 1px solid @gray-10;
+ border-radius: 6px;
+ box-shadow: 3px 3px 6px rgba(0, 0, 0, .07);
+ padding-left: 40px;
+ padding-right: 40px;
+ padding-top: 15px;
+ padding-bottom: 20px;
+ margin-bottom: 40px;
+ max-width: 480px;
+
+ umb-button {
+ margin-left: auto;
+ margin-right: 0;
+ display: block;
+ width: fit-content;
}
-
- .__get-sample-box {
- position:relative;
- border: 1px solid @gray-10;
- border-radius: 6px;
- box-shadow: 3px 3px 6px rgba(0, 0, 0, .07);
-
- padding-left: 40px;
- padding-right: 40px;
- padding-top: 15px;
- padding-bottom: 20px;
- margin-bottom: 40px;
- max-width: 480px;
-
- umb-button {
- margin-left: auto;
- margin-right: 0;
- display: block;
- width: fit-content;
- }
- }
-
+ }
}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.less
index 5861cfdda5..e2e422d267 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.less
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockgrid/prevalue/blockgrid.blockconfiguration.overlay.less
@@ -69,7 +69,7 @@
border-radius: @baseBorderRadius;
transition: color 120ms;
&.--hideText {
- color: white;
+ color: @ui-action-discreet-type;
}
&:hover, &:focus {
color: @ui-action-discreet-type-hover;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js
index 63ab76b553..89e62e390d 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js
@@ -103,12 +103,9 @@
localizationService.localize("blockEditor_headlineCreateBlock").then(localizedTitle => {
- const contentTypePicker = {
+ const dialog = {
title: localizedTitle,
- section: "settings",
- treeAlias: "documentTypes",
entityType: "documentType",
- isDialog: true,
filter: function (node) {
if (node.metaData.isElement === true) {
var key = udiService.getKey(node.udi);
@@ -142,7 +139,7 @@
]
};
- editorService.treePicker(contentTypePicker);
+ editorService.contentTypePicker(dialog);
});
};
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html
index 062e2dddee..1f4a696380 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html
@@ -18,8 +18,9 @@
-
- Add
-
+
+ Add Block
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less
index afb4316ce8..9ceadc7a3a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less
@@ -1,5 +1,11 @@
.umb-block-list-block-configuration {
+ uui-button {
+ --uui-button-border-radius: 6px;
+ font-weight: 700;
+ min-height: 80px;
+ }
+
.__add-button {
position: relative;
display: inline-flex;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less
index d2d875aa94..40454189db 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less
@@ -67,7 +67,7 @@
&.--noValue {
text-align: center;
border-radius: @baseBorderRadius;
- color: white;
+ color: @ui-action-discreet-type;
transition: color 120ms;
&:hover, &:focus {
color: @ui-action-discreet-type-hover;
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html
index 69a2e42f19..5f6f784f55 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/colorpicker.prevalues.html
@@ -1,38 +1,46 @@
-
-
+
+
-
-
+
+
- #{{newColor}}
-
-
-
- Add
- Update
- Cancel
-
+
#{{newColor}}
+
-
-
-
-
-
- Edit
- Remove
-
-
+
+ Add
+ Update
+ Cancel
+
+
+
+
+
+
+ Edit
+ Remove
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js
index 5406927d38..b16722e7de 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/colorpicker/multicolorpicker.controller.js
@@ -1,191 +1,228 @@
angular.module("umbraco").controller("Umbraco.PrevalueEditors.MultiColorPickerController",
- function ($scope, angularHelper, $element, eventsService) {
+ function ($scope, angularHelper, $element, eventsService) {
- const vm = this;
+ const vm = this;
- vm.add = add;
- vm.remove = remove;
- vm.edit = edit;
- vm.cancel = cancel;
+ vm.add = add;
+ vm.addOnEnter = addOnEnter;
+ vm.validateLabel = validateLabel;
+ vm.remove = remove;
+ vm.edit = edit;
+ vm.cancel = cancel;
- vm.show = show;
- vm.hide = hide;
- vm.change = change;
+ vm.show = show;
+ vm.hide = hide;
+ vm.change = change;
- vm.labelEnabled = false;
- vm.editItem = null;
+ vm.labelEnabled = false;
+ vm.editItem = null;
- // NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive.
- const defaultColor = "000000";
- const defaultLabel = null;
+ // NOTE: We need to make each color an object, not just a string because you cannot 2-way bind to a primitive.
+ const defaultColor = "000000";
+ const defaultLabel = null;
- $scope.newColor = defaultColor;
- $scope.newLabel = defaultLabel;
- $scope.hasError = false;
- $scope.focusOnNew = false;
+ $scope.newColor = defaultColor;
+ $scope.newLabel = defaultLabel;
+ $scope.colorHasError = false;
+ $scope.labelHasError = false;
+ $scope.focusOnNew = false;
- $scope.options = {
- type: "color",
- color: defaultColor,
- allowEmpty: false,
- showAlpha: false
- };
+ $scope.options = {
+ type: "color",
+ color: defaultColor,
+ allowEmpty: false,
+ showAlpha: false
+ };
- function hide() {
- // show the add button
- $element.find(".btn.add").show();
+ function hide() {
+ // show the add button
+ $element.find(".btn.add").show();
+ }
+
+ function show() {
+ // hide the add button
+ $element.find(".btn.add").hide();
+ }
+
+ function change(color) {
+ angularHelper.safeApply($scope, function () {
+ if (color) {
+ $scope.newColor = color.toHexString().trimStart("#");
+ $scope.colorHasError = !colorIsValid();
}
+ });
+ }
- function show() {
- // hide the add button
- $element.find(".btn.add").hide();
- }
-
- function change(color) {
- angularHelper.safeApply($scope, function () {
- if (color) {
- $scope.newColor = color.toHexString().trimStart("#");
- }
- });
- }
-
- var evts = [];
- evts.push(eventsService.on("toggleValue", function (e, args) {
- if (args.inputId === "useLabel") {
- vm.labelEnabled = args.value;
- }
- }));
-
- $scope.$on('$destroy', function () {
- for (var e in evts) {
- eventsService.unsubscribe(evts[e]);
- }
- });
-
- if (!Utilities.isArray($scope.model.value)) {
- //make an array from the dictionary
- var items = [];
- for (var i in $scope.model.value) {
- var oldValue = $scope.model.value[i];
- if (Object.prototype.hasOwnProperty.call(oldValue, "value")) {
- items.push({
- value: oldValue.value,
- label: oldValue.label,
- sortOrder: oldValue.sortOrder,
- id: i
- });
- } else {
- items.push({
- value: oldValue,
- label: oldValue,
- sortOrder: oldValue.sortOrder,
- id: i
- });
- }
- }
-
- //ensure the items are sorted by the provided sort order
- items.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); });
-
- //now make the editor model the array
- $scope.model.value = items;
- }
-
- // ensure labels
- for (var ii = 0; ii < $scope.model.value.length; ii++) {
- var item = $scope.model.value[ii];
- item.label = Object.prototype.hasOwnProperty.call(item, "label") ? item.label : item.value;
- }
-
- function validLabel(label) {
- return label !== null && typeof label !== "undefined" && label !== "" && label.length && label.length > 0;
- }
-
- function remove(item, evt) {
-
- evt.preventDefault();
-
- $scope.model.value = _.reject($scope.model.value, function (x) {
- return x.value === item.value && x.label === item.label;
- });
-
- setDirty();
- }
-
- function add(evt) {
- evt.preventDefault();
-
- if ($scope.newColor) {
- var newLabel = validLabel($scope.newLabel) ? $scope.newLabel : $scope.newColor;
- var exists = _.find($scope.model.value, function (item) {
- return item != vm.editItem && (item.value.toUpperCase() === $scope.newColor.toUpperCase() || item.label.toUpperCase() === newLabel.toUpperCase());
- });
- if (!exists) {
- if (vm.editItem == null) {
- $scope.model.value.push({
- value: $scope.newColor,
- label: newLabel
- });
- } else {
-
- if(vm.editItem.value === vm.editItem.label && vm.editItem.value === newLabel) {
- vm.editItem.label = $scope.newColor;
-
- }
- else {
- vm.editItem.label = newLabel;
- }
-
- vm.editItem.value = $scope.newColor;
-
- vm.editItem = null;
- }
-
- $scope.newLabel = "";
- $scope.hasError = false;
- $scope.focusOnNew = true;
- setDirty();
- return;
- }
-
- // there was an error, do the highlight (will be set back by the directive)
- $scope.hasError = true;
- }
- }
-
- function edit(item, evt) {
- evt.preventDefault();
-
- vm.editItem = item;
-
- $scope.newColor = item.value;
- $scope.newLabel = item.label;
- }
-
- function cancel(evt) {
- evt.preventDefault();
-
- vm.editItem = null;
- $scope.newColor = defaultColor;
- $scope.newLabel = defaultLabel;
- }
-
- function setDirty() {
- if (vm.modelValueForm) {
- vm.modelValueForm.selectedColor.$setDirty();
- }
- }
-
- $scope.sortableOptions = {
- axis: 'y',
- containment: 'parent',
- cursor: 'move',
- //handle: ".handle, .thumbnail",
- items: '> div.control-group',
- tolerance: 'pointer',
- update: function () {
- setDirty();
- }
- };
+ var evts = [];
+ evts.push(eventsService.on("toggleValue", function (e, args) {
+ if (args.inputId === "useLabel") {
+ vm.labelEnabled = args.value;
+ }
+ }));
+ $scope.$on('$destroy', function () {
+ for (var e in evts) {
+ eventsService.unsubscribe(evts[e]);
+ }
});
+
+ if (!Utilities.isArray($scope.model.value)) {
+ //make an array from the dictionary
+ var items = [];
+ for (var i in $scope.model.value) {
+ var oldValue = $scope.model.value[i];
+ if (Object.prototype.hasOwnProperty.call(oldValue, "value")) {
+ items.push({
+ value: oldValue.value,
+ label: oldValue.label,
+ sortOrder: oldValue.sortOrder,
+ id: i
+ });
+ } else {
+ items.push({
+ value: oldValue,
+ label: oldValue,
+ sortOrder: oldValue.sortOrder,
+ id: i
+ });
+ }
+ }
+
+ //ensure the items are sorted by the provided sort order
+ items.sort(function (a, b) { return (a.sortOrder > b.sortOrder) ? 1 : ((b.sortOrder > a.sortOrder) ? -1 : 0); });
+
+ //now make the editor model the array
+ $scope.model.value = items;
+ }
+
+ // ensure labels
+ for (var ii = 0; ii < $scope.model.value.length; ii++) {
+ var item = $scope.model.value[ii];
+ item.label = Object.prototype.hasOwnProperty.call(item, "label") ? item.label : item.value;
+ }
+
+ function remove(item, evt) {
+
+ evt.preventDefault();
+
+ $scope.model.value = _.reject($scope.model.value, function (x) {
+ return x.value === item.value && x.label === item.label;
+ });
+
+ setDirty();
+ }
+
+ function colorIsValid() {
+ var colorExists = _.find($scope.model.value, function (item) {
+ return item != vm.editItem && item.value.toUpperCase() === $scope.newColor.toUpperCase();
+ });
+
+ return colorExists ? false : true;
+ }
+
+ function getLabel() {
+ var validLabel = $scope.newLabel !== null && typeof $scope.newLabel !== "undefined" && $scope.newLabel !== "" && $scope.newLabel.length && $scope.newLabel.length > 0;
+ return validLabel ? $scope.newLabel : $scope.newColor;
+ }
+
+ function labelIsValid() {
+ var label = getLabel();
+ label = label.toUpperCase();
+
+ var labelExists = _.find($scope.model.value, function (item) {
+ return item != vm.editItem && item.label.toUpperCase() === label;
+ });
+
+ return labelExists ? false : true;
+ }
+
+ function validateLabel() {
+ $scope.labelHasError = !labelIsValid();
+ }
+
+ function addOnEnter(evt) {
+ if (evt.keyCode === 13) {
+ add(evt);
+ }
+ }
+
+ function add(evt) {
+ evt.preventDefault();
+
+ if ($scope.newColor) {
+
+ $scope.colorHasError = !colorIsValid();
+ $scope.labelHasError = !labelIsValid();
+
+ if ($scope.labelHasError || $scope.colorHasError) {
+ return;
+ }
+
+ var newLabel = getLabel();
+
+ if (vm.editItem == null) {
+ $scope.model.value.push({
+ value: $scope.newColor,
+ label: newLabel
+ });
+ } else {
+
+ if (vm.editItem.value === vm.editItem.label && vm.editItem.value === newLabel) {
+ vm.editItem.label = $scope.newColor;
+
+ }
+ else {
+ vm.editItem.label = newLabel;
+ }
+
+ vm.editItem.value = $scope.newColor;
+
+ vm.editItem = null;
+ }
+
+ $scope.newLabel = "";
+ $scope.colorHasError = false;
+ $scope.labelHasError = false;
+ $scope.focusOnNew = true;
+ setDirty();
+ return;
+
+ }
+ }
+
+ function edit(item, evt) {
+ evt.preventDefault();
+
+ vm.editItem = item;
+
+ $scope.newColor = item.value;
+ $scope.newLabel = item.label;
+ }
+
+ function cancel(evt) {
+ evt.preventDefault();
+
+ vm.editItem = null;
+ $scope.newColor = defaultColor;
+ $scope.newLabel = defaultLabel;
+ }
+
+ function setDirty() {
+ if (vm.modelValueForm) {
+ vm.modelValueForm.selectedColor.$setDirty();
+ }
+ }
+
+ $scope.sortableOptions = {
+ axis: 'y',
+ containment: 'parent',
+ cursor: 'move',
+ //handle: ".handle, .thumbnail",
+ items: '> div.control-group',
+ tolerance: 'pointer',
+ update: function () {
+ setDirty();
+ }
+ };
+
+ });
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
index 453347bc1b..80d0cc16c9 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.controller.js
@@ -20,8 +20,9 @@ angular.module('umbraco')
var umbracoSettings = Umbraco.Sys.ServerVariables.umbracoSettings;
$scope.acceptFileExt = mediaHelper.formatFileTypes(umbracoSettings.imageFileTypes);
+
/**
- * Called when the umgImageGravity component updates the focal point value
+ * Called when the umbImageGravity component updates the focal point value
* @param {any} left
* @param {any} top
*/
@@ -81,7 +82,7 @@ angular.module('umbraco')
function imageLoaded(isCroppable, hasDimensions) {
$scope.isCroppable = isCroppable;
$scope.hasDimensions = hasDimensions;
- };
+ }
/**
* Called when the file collection changes
diff --git a/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs
index f498189ff0..3a4715b537 100644
--- a/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs
+++ b/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs
@@ -73,7 +73,7 @@ public class UmbTwoFactorLoginController : SurfaceController
MemberIdentityUser? user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null!)
{
- _logger.LogWarning("PostVerify2FACode :: No verified member found, returning 404");
+ _logger.LogWarning("Verify2FACode :: No verified member found, returning 404");
return NotFound();
}
diff --git a/tests/Umbraco.TestData/LoadTestController.cs b/tests/Umbraco.TestData/LoadTestController.cs
index 741fe4e94b..676b5c0256 100644
--- a/tests/Umbraco.TestData/LoadTestController.cs
+++ b/tests/Umbraco.TestData/LoadTestController.cs
@@ -7,6 +7,7 @@ using System.Threading;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
@@ -99,7 +100,7 @@ public class LoadTestController : Controller
private readonly IDataTypeService _dataTypeService;
private readonly IFileService _fileService;
private readonly IHostApplicationLifetime _hostApplicationLifetime;
- private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly IHostEnvironment _hostEnvironment;
private readonly IShortStringHelper _shortStringHelper;
public LoadTestController(
@@ -108,7 +109,7 @@ public class LoadTestController : Controller
IDataTypeService dataTypeService,
IFileService fileService,
IShortStringHelper shortStringHelper,
- IHostingEnvironment hostingEnvironment,
+ IHostEnvironment hostEnvironment,
IHostApplicationLifetime hostApplicationLifetime)
{
_contentTypeService = contentTypeService;
@@ -116,7 +117,7 @@ public class LoadTestController : Controller
_dataTypeService = dataTypeService;
_fileService = fileService;
_shortStringHelper = shortStringHelper;
- _hostingEnvironment = hostingEnvironment;
+ _hostEnvironment = hostEnvironment;
_hostApplicationLifetime = hostApplicationLifetime;
}
@@ -321,7 +322,7 @@ public class LoadTestController : Controller
public IActionResult ColdBootRestart()
{
Directory.Delete(
- _hostingEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.TempData, "DistCache")),
+ _hostEnvironment.MapPathContentRoot(Path.Combine(Constants.SystemDirectories.TempData, "DistCache")),
true);
DoRestart();
diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
index 28bcb052fc..02c46ac329 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
+++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json
@@ -23,7 +23,7 @@
"prompt": "^1.2.0",
"tslib": "^2.4.0",
"typescript": "^4.8.3",
- "wait-on": "^6.0.1"
+ "wait-on": "^7.2.0"
}
},
"node_modules/@colors/colors": {
@@ -177,12 +177,14 @@
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"node_modules/axios": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
- "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
+ "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dev": true,
"dependencies": {
- "follow-redirects": "^1.14.7"
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
@@ -364,9 +366,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.2",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
- "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+ "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"dev": true,
"funding": [
{
@@ -563,15 +565,15 @@
"dev": true
},
"node_modules/joi": {
- "version": "17.6.3",
- "resolved": "https://registry.npmjs.org/joi/-/joi-17.6.3.tgz",
- "integrity": "sha512-YlQsIaS9MHYekzf1Qe11LjTkNzx9qhYluK3172z38RxYoAUf82XMX1p1DG1H4Wtk2ED/vPdSn9OggqtDu+aTow==",
+ "version": "17.11.0",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz",
+ "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==",
"dev": true,
"dependencies": {
"@hapi/hoek": "^9.0.0",
"@hapi/topo": "^5.0.0",
"@sideway/address": "^4.1.3",
- "@sideway/formula": "^3.0.0",
+ "@sideway/formula": "^3.0.1",
"@sideway/pinpoint": "^2.0.0"
}
},
@@ -635,9 +637,9 @@
}
},
"node_modules/minimist": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
- "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
@@ -777,6 +779,12 @@
"node": ">= 6.0.0"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -867,9 +875,9 @@
}
},
"node_modules/rxjs": {
- "version": "7.5.7",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz",
- "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==",
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
@@ -930,22 +938,22 @@
}
},
"node_modules/wait-on": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-6.0.1.tgz",
- "integrity": "sha512-zht+KASY3usTY5u2LgaNqn/Cd8MukxLGjdcZxT2ns5QzDmTFc4XoWBgC+C/na+sMRZTuVygQoMYwdcVjHnYIVw==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz",
+ "integrity": "sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==",
"dev": true,
"dependencies": {
- "axios": "^0.25.0",
- "joi": "^17.6.0",
+ "axios": "^1.6.1",
+ "joi": "^17.11.0",
"lodash": "^4.17.21",
- "minimist": "^1.2.5",
- "rxjs": "^7.5.4"
+ "minimist": "^1.2.8",
+ "rxjs": "^7.8.1"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
- "node": ">=10.0.0"
+ "node": ">=12.0.0"
}
},
"node_modules/webidl-conversions": {
diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json
index 2e86d70d5e..adcdf64f1d 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/package.json
+++ b/tests/Umbraco.Tests.AcceptanceTest/package.json
@@ -16,7 +16,7 @@
"del": "^6.0.0",
"ncp": "^2.0.0",
"prompt": "^1.2.0",
- "wait-on": "^6.0.1"
+ "wait-on": "^7.2.0"
},
"dependencies": {
"@umbraco/json-models-builders": "^1.0.6",
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/helpLinks.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/helpLinks.spec.ts
index 882eaf1bbf..2239613571 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/helpLinks.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/HelpPanel/helpLinks.spec.ts
@@ -36,6 +36,19 @@ test("Check the youtube link works as expected", async ({ page, umbracoUi }) =>
} else {
await expect(page).toHaveURL(/.*UmbracoLearningBase/);
+ await page.close();
}
});
+ test("Check the Our Umbraco link works as expected", async ({ page, umbracoUi }) => {
+ // Action
+ await umbracoUi.clickElement(umbracoUi.getGlobalHelp());
+ let ourUmbracoLink = await page.locator('[key="help_umbracoForum"]');
+ await ourUmbracoLink.click();
+ let ourUmbraco = page.waitForEvent("popup");
+ let ourUmbracoPopup = await ourUmbraco;
+
+ //Assert
+ await expect(ourUmbracoPopup).toHaveURL(/.*our.umbraco.com/);
+ await ourUmbracoPopup.close();
+ });
});
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs
new file mode 100644
index 0000000000..3447dff634
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/BackOfficeExamineSearcherTests.cs
@@ -0,0 +1,611 @@
+using Examine;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Infrastructure.Examine;
+using Umbraco.Cms.Infrastructure.Examine.DependencyInjection;
+using Umbraco.Cms.Infrastructure.HostedServices;
+using Umbraco.Cms.Infrastructure.Search;
+using Umbraco.Cms.Tests.Common.Builders;
+using Umbraco.Cms.Tests.Common.Builders.Extensions;
+using Umbraco.Cms.Tests.Common.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+using Umbraco.Cms.Web.BackOffice.Security;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine;
+
+[TestFixture]
+[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
+public class BackOfficeExamineSearcherTests : ExamineBaseTest
+{
+ [SetUp]
+ public void Setup()
+ {
+ TestHelper.DeleteDirectory(GetIndexPath(Constants.UmbracoIndexes.InternalIndexName));
+ TestHelper.DeleteDirectory(GetIndexPath(Constants.UmbracoIndexes.ExternalIndexName));
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = Services;
+ Mock.Get(TestHelper.GetHttpContextAccessor()).Setup(x => x.HttpContext).Returns(httpContext);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ // When disposing examine, it does a final write, which ends up locking the file if the indexing is not done yet. So we have this wait to circumvent that.
+ Thread.Sleep(1500);
+ // Sometimes we do not dispose all services in time and the test fails because the log file is locked. Resulting in all other tests failing as well
+ Services.DisposeIfDisposable();
+ }
+
+ private IBackOfficeExamineSearcher BackOfficeExamineSearcher => GetRequiredService
();
+
+ private IContentTypeService ContentTypeService => GetRequiredService();
+
+ private ILocalizationService LocalizationService => GetRequiredService();
+
+ private ContentService ContentService => (ContentService)GetRequiredService();
+
+ private IUserStore BackOfficeUserStore =>
+ GetRequiredService>();
+
+ private IBackOfficeSignInManager BackOfficeSignInManager => GetRequiredService();
+
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.Services.AddUnique();
+ builder.Services.AddUnique();
+ builder
+ .AddNotificationHandler();
+ builder.AddNotificationHandler();
+ builder.AddExamineIndexes();
+ builder.AddBackOfficeIdentity();
+ builder.Services.AddHostedService();
+ }
+
+ private IEnumerable BackOfficeExamineSearch(string query, int pageSize = 20, int pageIndex = 0) =>
+ BackOfficeExamineSearcher.Search(query, UmbracoEntityTypes.Document,
+ pageSize, pageIndex, out _, ignoreUserStartNodes: true);
+
+ private async Task SetupUserIdentity(string userId)
+ {
+ var identity =
+ await BackOfficeUserStore.FindByIdAsync(userId, CancellationToken.None);
+ await BackOfficeSignInManager.SignInAsync(identity, false);
+ }
+
+ private async Task CreateDefaultPublishedContent(string contentName)
+ {
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentTypeService.Save(contentType), Constants.UmbracoIndexes.InternalIndexName);
+
+ var content = new ContentBuilder()
+ .WithId(0)
+ .WithName(contentName)
+ .WithContentType(contentType)
+ .Build();
+
+ var createdContent = await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(content), Constants.UmbracoIndexes.InternalIndexName);
+
+ return createdContent;
+ }
+
+ private async Task CreateDefaultPublishedContentWithTwoLanguages(string englishNodeName, string danishNodeName)
+ {
+ const string usIso = "en-US";
+ const string dkIso = "da";
+
+ var langDa = new LanguageBuilder()
+ .WithCultureInfo(dkIso)
+ .Build();
+ LocalizationService.Save(langDa);
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .WithContentVariation(ContentVariation.Culture)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentTypeService.Save(contentType), Constants.UmbracoIndexes.InternalIndexName);
+
+ var content = new ContentBuilder()
+ .WithId(0)
+ .WithCultureName(usIso, englishNodeName)
+ .WithCultureName(dkIso, danishNodeName)
+ .WithContentType(contentType)
+ .Build();
+ var createdContent = await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(content), Constants.UmbracoIndexes.InternalIndexName);
+
+ return createdContent;
+ }
+
+ [Test]
+ public async Task Search_Published_Content_With_Empty_Query()
+ {
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ // Arrange
+ const string contentName = "TestContent";
+ await CreateDefaultPublishedContent(contentName);
+
+ string query = string.Empty;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ Assert.AreEqual(0, actual.Count());
+ }
+
+ [Test]
+ public async Task Search_Published_Content_With_Query_By_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "TestContent";
+ await CreateDefaultPublishedContent(contentName);
+
+ string query = contentName;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+ Assert.AreEqual(searchResults.First().Values["nodeName"], contentName);
+ }
+
+ [Test]
+ public async Task Search_Published_Content_With_Query_By_Non_Existing_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "TestContent";
+ await CreateDefaultPublishedContent(contentName);
+
+ string query = "ContentTest";
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ Assert.AreEqual(0, actual.Count());
+ }
+
+ [Test]
+ public async Task Search_Published_Content_With_Query_By_Content_Id()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "RandomContentName";
+ PublishResult createdContent = await CreateDefaultPublishedContent(contentName);
+
+ string contentId = createdContent.Content.Id.ToString();
+
+ string query = contentId;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+ Assert.AreEqual(searchResults.First().Values["nodeName"], contentName);
+ Assert.AreEqual(searchResults.First().Id, contentId);
+ }
+
+ [Test]
+ public async Task Search_Two_Published_Content_With_Similar_Names_By_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "TestName Original";
+ const string secondContentName = "TestName Copy";
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentTypeService.Save(contentType), Constants.UmbracoIndexes.InternalIndexName);
+
+ var firstContent = new ContentBuilder()
+ .WithId(0)
+ .WithName(contentName)
+ .WithContentType(contentType)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(firstContent), Constants.UmbracoIndexes.InternalIndexName);
+
+ var secondContent = new ContentBuilder()
+ .WithId(0)
+ .WithName(secondContentName)
+ .WithContentType(contentType)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(secondContent), Constants.UmbracoIndexes.InternalIndexName);
+
+ string query = contentName;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(2, searchResults.Count());
+ // Checks if the first content in the search is the original content
+ Assert.AreEqual(searchResults.First().Id, firstContent.Id.ToString());
+ // Checks if the score for the original name is higher than the score for the copy
+ Assert.Greater(searchResults.First().Score, searchResults.Last().Score);
+ }
+
+ [Test]
+ public async Task Search_For_Child_Published_Content_With_Query_By_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "ParentTestContent";
+ const string childContentName = "ChildTestContent";
+
+ var contentType = new ContentTypeBuilder()
+ .WithName("Document")
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentTypeService.Save(contentType), Constants.UmbracoIndexes.InternalIndexName);
+
+ var content = new ContentBuilder()
+ .WithName(contentName)
+ .WithContentType(contentType)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(content), Constants.UmbracoIndexes.InternalIndexName);
+
+ var childContent = new ContentBuilder()
+ .WithName(childContentName)
+ .WithContentType(contentType)
+ .WithParentId(content.Id)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(childContent), Constants.UmbracoIndexes.InternalIndexName);
+
+ string parentQuery = content.Id.ToString();
+
+ string childQuery = childContent.Id.ToString();
+
+ // Act
+ IEnumerable parentContentActual = BackOfficeExamineSearch(parentQuery);
+ IEnumerable childContentActual = BackOfficeExamineSearch(childQuery);
+
+ // Assert
+ IEnumerable contentActual = parentContentActual.ToArray();
+ IEnumerable searchResults = childContentActual.ToArray();
+ Assert.AreEqual(1, contentActual.Count());
+ Assert.AreEqual(1, searchResults.Count());
+ Assert.AreEqual(contentActual.First().Values["nodeName"], contentName);
+ Assert.AreEqual(searchResults.First().Values["nodeName"], childContentName);
+ }
+
+ [Test]
+ public async Task Search_For_Child_In_Child_Published_Content_With_Query_By_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "ParentTestContent";
+ const string childContentName = "ChildTestContent";
+ const string childChildContentName = "ChildChildTestContent";
+
+ var contentType = new ContentTypeBuilder()
+ .WithName("Document")
+ .Build();
+ ContentTypeService.Save(contentType);
+
+ var content = new ContentBuilder()
+ .WithName(contentName)
+ .WithContentType(contentType)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(content), Constants.UmbracoIndexes.InternalIndexName);
+
+ var childContent = new ContentBuilder()
+ .WithName(childContentName)
+ .WithContentType(contentType)
+ .WithParentId(content.Id)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(childContent), Constants.UmbracoIndexes.InternalIndexName);
+
+ var childChildContent = new ContentBuilder()
+ .WithName(childChildContentName)
+ .WithContentType(contentType)
+ .WithParentId(childContent.Id)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(childChildContent), Constants.UmbracoIndexes.InternalIndexName);
+
+ string parentQuery = content.Id.ToString();
+ string childQuery = childContent.Id.ToString();
+ string childChildQuery = childChildContent.Id.ToString();
+
+ // Act
+ IEnumerable parentContentActual = BackOfficeExamineSearch(parentQuery);
+ IEnumerable childContentActual = BackOfficeExamineSearch(childQuery);
+ IEnumerable childChildContentActual = BackOfficeExamineSearch(childChildQuery);
+
+ IEnumerable parentSearchResults = parentContentActual.ToArray();
+ IEnumerable childSearchResults = childContentActual.ToArray();
+ IEnumerable childChildSearchResults = childChildContentActual.ToArray();
+
+ // Assert
+ Assert.AreEqual(1, parentSearchResults.Count());
+ Assert.AreEqual(1, childSearchResults.Count());
+ Assert.AreEqual(1, childChildSearchResults.Count());
+ Assert.AreEqual(parentSearchResults.First().Values["nodeName"], contentName);
+ Assert.AreEqual(childSearchResults.First().Values["nodeName"], childContentName);
+ Assert.AreEqual(childChildSearchResults.First().Values["nodeName"], childChildContentName);
+ }
+
+ [Test]
+ public async Task Search_Published_Content_With_Query_With_Content_Name_No_User_Logged_In()
+ {
+ // Arrange
+ const string contentName = "TestContent";
+
+ PublishResult createdContent = await CreateDefaultPublishedContent(contentName);
+
+ string query = createdContent.Content.Id.ToString();
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ Assert.AreEqual(0, actual.Count());
+ }
+
+ // Multiple Languages
+ [Test]
+ public async Task Search_Published_Content_By_Content_Name_With_Two_Languages()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string usIso = "en-US";
+ const string dkIso = "da";
+ const string englishNodeName = "EnglishNode";
+ const string danishNodeName = "DanishNode";
+
+ var langDa = new LanguageBuilder()
+ .WithCultureInfo(dkIso)
+ .Build();
+ LocalizationService.Save(langDa);
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .WithContentVariation(ContentVariation.Culture)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentTypeService.Save(contentType), Constants.UmbracoIndexes.InternalIndexName);
+
+ var content = new ContentBuilder()
+ .WithId(0)
+ .WithCultureName(usIso, englishNodeName)
+ .WithCultureName(dkIso, danishNodeName)
+ .WithContentType(contentType)
+ .Build();
+ PublishResult createdContent = await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(content), Constants.UmbracoIndexes.InternalIndexName);
+
+ string query = createdContent.Content.Id.ToString();
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+ var nodeNameEn = searchResults.First().Values["nodeName_en-us"];
+ var nodeNameDa = searchResults.First().Values["nodeName_da"];
+ Assert.AreEqual(englishNodeName, nodeNameEn);
+ Assert.AreEqual(danishNodeName, nodeNameDa);
+ }
+
+ [Test]
+ public async Task Search_For_Published_Content_Name_With_Two_Languages_By_Default_Language_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string englishNodeName = "EnglishNode";
+ const string danishNodeName = "DanishNode";
+
+ await CreateDefaultPublishedContentWithTwoLanguages(englishNodeName, danishNodeName);
+
+ string query = englishNodeName;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+ var nodeNameEn = searchResults.First().Values["nodeName_en-us"];
+ var nodeNameDa = searchResults.First().Values["nodeName_da"];
+ Assert.AreEqual(englishNodeName, nodeNameEn);
+ Assert.AreEqual(danishNodeName, nodeNameDa);
+ }
+
+ [Test]
+ public async Task Search_For_Published_Content_Name_With_Two_Languages_By_Non_Default_Language_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string englishNodeName = "EnglishNode";
+ const string danishNodeName = "DanishNode";
+
+ await CreateDefaultPublishedContentWithTwoLanguages(englishNodeName, danishNodeName);
+
+ string query = danishNodeName;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+ var nodeNameDa = searchResults.First().Values["nodeName_da"];
+ var nodeNameEn = searchResults.First().Values["nodeName_en-us"];
+ Assert.AreEqual(englishNodeName, nodeNameEn);
+ Assert.AreEqual(danishNodeName, nodeNameDa);
+ }
+
+ [Test]
+ public async Task Search_Published_Content_With_Two_Languages_By_Id()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string englishNodeName = "EnglishNode";
+ const string danishNodeName = "DanishNode";
+
+ var contentNode = await CreateDefaultPublishedContentWithTwoLanguages(englishNodeName, danishNodeName);
+
+ string query = contentNode.Content.Id.ToString();
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+ var nodeNameDa = searchResults.First().Values["nodeName_da"];
+ var nodeNameEn = searchResults.First().Values["nodeName_en-us"];
+ Assert.AreEqual(englishNodeName, nodeNameEn);
+ Assert.AreEqual(danishNodeName, nodeNameDa);
+ }
+
+ // Check All Indexed Values
+ [Test]
+ public async Task Check_All_Indexed_Values_For_Published_Content_With_No_Properties()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "TestContent";
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .Build();
+ ContentTypeService.Save(contentType);
+
+ var contentNode = new ContentBuilder()
+ .WithId(0)
+ .WithName(contentName)
+ .WithContentType(contentType)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(contentNode), Constants.UmbracoIndexes.InternalIndexName);
+
+ string query = contentName;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+
+ string contentNodePublish = string.Empty;
+ if (contentNode.Published)
+ {
+ contentNodePublish = "y";
+ }
+
+ string contentTypeCultureVariations = string.Empty;
+
+ if (contentType.Variations == ContentVariation.Nothing)
+ {
+ contentTypeCultureVariations = "n";
+ }
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(searchResults.First().Values["__NodeId"], contentNode.Id.ToString());
+ Assert.AreEqual(searchResults.First().Values["__IndexType"], "content");
+ Assert.AreEqual(searchResults.First().Values["__NodeTypeAlias"], contentNode.ContentType.Alias);
+ Assert.AreEqual(searchResults.First().Values["__Published"], contentNodePublish);
+ Assert.AreEqual(searchResults.First().Values["id"], contentNode.Id.ToString());
+ Assert.AreEqual(searchResults.First().Values["__Key"], contentNode.Key.ToString());
+ Assert.AreEqual(searchResults.First().Values["parentID"], contentNode.ParentId.ToString());
+ Assert.AreEqual(searchResults.First().Values["nodeName"], contentNode.Name);
+ Assert.AreEqual(searchResults.First().Values["__VariesByCulture"], contentTypeCultureVariations);
+ Assert.AreEqual(searchResults.First().Values["__Icon"], contentNode.ContentType.Icon);
+ });
+ }
+
+ [Test]
+ public async Task Check_All_Indexed_Values_For_Published_Content_With_Properties()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ const string contentName = "TestContent";
+ const string propertyEditorName = "TestBox";
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .AddPropertyType()
+ .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
+ .WithName(propertyEditorName)
+ .WithAlias("testBox")
+ .Done()
+ .Build();
+ ContentTypeService.Save(contentType);
+
+ var contentNode = new ContentBuilder()
+ .WithId(0)
+ .WithName(contentName)
+ .WithContentType(contentType)
+ .WithPropertyValues(new { testBox = "TestValue" })
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(contentNode), Constants.UmbracoIndexes.InternalIndexName);
+
+ string query = contentName;
+
+ // Act
+ IEnumerable actual = BackOfficeExamineSearch(query);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+
+ string contentNodePublish = string.Empty;
+ string contentTypeCultureVariations = string.Empty;
+
+ if (contentNode.Published)
+ {
+ contentNodePublish = "y";
+ }
+
+ if (contentType.Variations == ContentVariation.Nothing)
+ {
+ contentTypeCultureVariations = "n";
+ }
+
+ Assert.Multiple(() =>
+ {
+ Assert.AreEqual(searchResults.First().Values["__NodeId"], contentNode.Id.ToString());
+ Assert.AreEqual(searchResults.First().Values["__IndexType"], "content");
+ Assert.AreEqual(searchResults.First().Values["__NodeTypeAlias"], contentNode.ContentType.Alias);
+ Assert.AreEqual(searchResults.First().Values["__Published"], contentNodePublish);
+ Assert.AreEqual(searchResults.First().Values["id"], contentNode.Id.ToString());
+ Assert.AreEqual(searchResults.First().Values["__Key"], contentNode.Key.ToString());
+ Assert.AreEqual(searchResults.First().Values["parentID"], contentNode.ParentId.ToString());
+ Assert.AreEqual(searchResults.First().Values["nodeName"], contentNode.Name);
+ Assert.AreEqual(searchResults.First().Values["__VariesByCulture"], contentTypeCultureVariations);
+ Assert.AreEqual(searchResults.First().Values["__Icon"], contentNode.ContentType.Icon);
+ });
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs
index 381464faf3..3d97f4ede0 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineBaseTest.cs
@@ -1,4 +1,5 @@
using System.Data;
+using Examine;
using Examine.Lucene.Providers;
using Microsoft.Extensions.DependencyInjection;
using Moq;
@@ -25,6 +26,8 @@ public abstract class ExamineBaseTest : UmbracoIntegrationTest
protected IRuntimeState RunningRuntimeState { get; } = Mock.Of(x => x.Level == RuntimeLevel.Run);
+ protected IExamineManager ExamineManager => GetRequiredService();
+
protected override void ConfigureTestServices(IServiceCollection services)
=> services.AddSingleton();
@@ -113,6 +116,43 @@ public abstract class ExamineBaseTest : UmbracoIntegrationTest
return new DisposableWrapper(syncMode, index, luceneDir);
}
+ private AutoResetEvent indexingHandle = new(false);
+
+ protected async Task ExecuteAndWaitForIndexing(Action indexUpdatingAction, string indexName) =>
+ await ExecuteAndWaitForIndexing(
+ () =>
+ {
+ indexUpdatingAction();
+ return null;
+ }, indexName);
+
+ ///
+ /// Performs and action and waits for the specified index to be done indexing.
+ ///
+ /// The action that causes the index to be updated.
+ /// The name of the index to wait for rebuild.
+ /// The type returned from the action.
+ /// The result of the action.
+ protected async Task ExecuteAndWaitForIndexing (Func indexUpdatingAction, string indexName)
+ {
+ // Set up an action to release the handle when the index is populated.
+ if (ExamineManager.TryGetIndex(indexName, out IIndex index) is false)
+ {
+ throw new InvalidOperationException($"Could not find index: {indexName}");
+ }
+
+ index.IndexOperationComplete += (_, _) =>
+ {
+ indexingHandle.Set();
+ };
+
+ // Perform the action, and wait for the handle to be freed, meaning the index is done populating.
+ var result = indexUpdatingAction();
+ await indexingHandle.WaitOneAsync();
+
+ return result;
+ }
+
private class DisposableWrapper : IDisposable
{
private readonly IDisposable[] _disposables;
@@ -127,4 +167,10 @@ public abstract class ExamineBaseTest : UmbracoIntegrationTest
}
}
}
+
+ protected string GetIndexPath(string indexName)
+ {
+ var root = TestContext.CurrentContext.TestDirectory.Split("Umbraco.Tests.Integration")[0];
+ return Path.Combine(root, "Umbraco.Tests.Integration", "umbraco", "Data", "TEMP", "ExamineIndexes", indexName);
+ }
}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexSearcherTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexSearcherTest.cs
new file mode 100644
index 0000000000..38eb270b3c
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexSearcherTest.cs
@@ -0,0 +1,417 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Globalization;
+using System.Text;
+using System.Text.RegularExpressions;
+using Examine;
+using Examine.Search;
+using Lucene.Net.QueryParsers.Classic;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Mapping;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Models.Entities;
+using Umbraco.Cms.Core.Models.Membership;
+using Umbraco.Cms.Core.Routing;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Infrastructure.Examine;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine;
+
+public class ExamineExternalIndexSearcherTest : IExamineExternalIndexSearcherTest
+{
+ private readonly AppCaches _appCaches;
+ private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
+ private readonly IEntityService _entityService;
+ private readonly IExamineManager _examineManager;
+ private readonly ILocalizationService _languageService;
+ private readonly IPublishedUrlProvider _publishedUrlProvider;
+ private readonly IUmbracoTreeSearcherFields _treeSearcherFields;
+ private readonly IUmbracoMapper _umbracoMapper;
+
+ public ExamineExternalIndexSearcherTest(
+ IExamineManager examineManager,
+ ILocalizationService languageService,
+ IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
+ IEntityService entityService,
+ IUmbracoTreeSearcherFields treeSearcherFields,
+ AppCaches appCaches,
+ IUmbracoMapper umbracoMapper,
+ IPublishedUrlProvider publishedUrlProvider)
+ {
+ _examineManager = examineManager;
+ _languageService = languageService;
+ _backOfficeSecurityAccessor = backOfficeSecurityAccessor;
+ _entityService = entityService;
+ _treeSearcherFields = treeSearcherFields;
+ _appCaches = appCaches;
+ _umbracoMapper = umbracoMapper;
+ _publishedUrlProvider = publishedUrlProvider;
+ }
+
+ public IEnumerable Search(
+ string query,
+ UmbracoEntityTypes entityType,
+ int pageSize,
+ long pageIndex,
+ out long totalFound,
+ string? searchFrom = null,
+ bool ignoreUserStartNodes = false)
+ {
+ var sb = new StringBuilder();
+
+ string type;
+ var indexName = Constants.UmbracoIndexes.ExternalIndexName;
+ var fields = _treeSearcherFields.GetBackOfficeFields().ToList();
+
+ ISet fieldsToLoad = new HashSet(_treeSearcherFields.GetBackOfficeFieldsToLoad());
+
+ // TODO: WE should try to allow passing in a lucene raw query, however we will still need to do some manual string
+ // manipulation for things like start paths, member types, etc...
+ //if (Examine.ExamineExtensions.TryParseLuceneQuery(query))
+ //{
+
+ //}
+
+ //special GUID check since if a user searches on one specifically we need to escape it
+ if (Guid.TryParse(query, out Guid g))
+ {
+ query = "\"" + g + "\"";
+ }
+
+ IUser? currentUser = _backOfficeSecurityAccessor?.BackOfficeSecurity?.CurrentUser;
+
+ switch (entityType)
+ {
+ case UmbracoEntityTypes.Member:
+ indexName = Constants.UmbracoIndexes.MembersIndexName;
+ type = "member";
+ fields.AddRange(_treeSearcherFields.GetBackOfficeMembersFields());
+ foreach (var field in _treeSearcherFields.GetBackOfficeMembersFieldsToLoad())
+ {
+ fieldsToLoad.Add(field);
+ }
+
+ if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId &&
+ searchFrom.Trim() != "-1")
+ {
+ sb.Append("+__NodeTypeAlias:");
+ sb.Append(searchFrom);
+ sb.Append(" ");
+ }
+
+ break;
+ case UmbracoEntityTypes.Media:
+ type = "media";
+ fields.AddRange(_treeSearcherFields.GetBackOfficeMediaFields());
+ foreach (var field in _treeSearcherFields.GetBackOfficeMediaFieldsToLoad())
+ {
+ fieldsToLoad.Add(field);
+ }
+
+ var allMediaStartNodes = currentUser != null
+ ? currentUser.CalculateMediaStartNodeIds(_entityService, _appCaches)
+ : Array.Empty();
+ AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
+ break;
+ case UmbracoEntityTypes.Document:
+ type = "content";
+ fields.AddRange(_treeSearcherFields.GetBackOfficeDocumentFields());
+ foreach (var field in _treeSearcherFields.GetBackOfficeDocumentFieldsToLoad())
+ {
+ fieldsToLoad.Add(field);
+ }
+
+ var allContentStartNodes = currentUser != null
+ ? currentUser.CalculateContentStartNodeIds(_entityService, _appCaches)
+ : Array.Empty();
+ AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, ignoreUserStartNodes, _entityService);
+ break;
+ default:
+ throw new NotSupportedException("The " + typeof(BackOfficeExamineSearcher) +
+ " currently does not support searching against object type " +
+ entityType);
+ }
+
+ if (!_examineManager.TryGetIndex(indexName, out IIndex? index))
+ {
+ throw new InvalidOperationException("No index found by name " + indexName);
+ }
+
+ if (!BuildQuery(sb, query, searchFrom, fields, type))
+ {
+ totalFound = 0;
+ return Enumerable.Empty();
+ }
+
+ ISearchResults? result = index.Searcher
+ .CreateQuery()
+ .NativeQuery(sb.ToString())
+ .SelectFields(fieldsToLoad)
+ //only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested
+ .Execute(QueryOptions.SkipTake(Convert.ToInt32(pageSize * pageIndex), pageSize));
+
+ totalFound = result.TotalItemCount;
+
+ return result;
+ }
+
+ private bool BuildQuery(StringBuilder sb, string query, string? searchFrom, List fields, string type)
+ {
+ //build a lucene query:
+ // the nodeName will be boosted 10x without wildcards
+ // then nodeName will be matched normally with wildcards
+ // the rest will be normal without wildcards
+
+ var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode.ToLowerInvariant()).ToList();
+
+ // the chars [*-_] in the query will mess everything up so let's remove those
+ // However we cannot just remove - and _ since these signify a space, so we instead replace them with that.
+ query = Regex.Replace(query, "[\\*]", string.Empty);
+ query = Regex.Replace(query, "[\\-_]", " ");
+
+
+ //check if text is surrounded by single or double quotes, if so, then exact match
+ var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$")
+ || Regex.IsMatch(query, "^\'.*?\'$");
+
+ if (surroundedByQuotes)
+ {
+ //strip quotes, escape string, the replace again
+ query = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote);
+
+ query = QueryParserBase.Escape(query);
+
+ //nothing to search
+ if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace())
+ {
+ return false;
+ }
+
+ //update the query with the query term
+ if (query.IsNullOrWhiteSpace() == false)
+ {
+ //add back the surrounding quotes
+ query = string.Format("{0}{1}{0}", "\"", query);
+
+ sb.Append("+(");
+
+ AppendNodeNamePhraseWithBoost(sb, query, allLangs);
+
+ foreach (var f in fields)
+ {
+ //additional fields normally
+ sb.Append(f);
+ sb.Append(": (");
+ sb.Append(query);
+ sb.Append(") ");
+ }
+
+ sb.Append(") ");
+ }
+ }
+ else
+ {
+ var trimmed = query.Trim(Constants.CharArrays.DoubleQuoteSingleQuote);
+
+ //nothing to search
+ if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace())
+ {
+ return false;
+ }
+
+ //update the query with the query term
+ if (trimmed.IsNullOrWhiteSpace() == false)
+ {
+ query = QueryParserBase.Escape(query);
+
+ var querywords = query.Split(Constants.CharArrays.Space, StringSplitOptions.RemoveEmptyEntries);
+
+ sb.Append("+(");
+
+ AppendNodeNameExactWithBoost(sb, query, allLangs);
+
+ AppendNodeNameWithWildcards(sb, querywords, allLangs);
+
+ foreach (var f in fields)
+ {
+ var queryWordsReplaced = new string[querywords.Length];
+
+ // when searching file names containing hyphens we need to replace the hyphens with spaces
+ if (f.Equals(UmbracoExamineFieldNames.UmbracoFileFieldName))
+ {
+ for (var index = 0; index < querywords.Length; index++)
+ {
+ queryWordsReplaced[index] =
+ querywords[index].Replace("\\-", " ").Replace("_", " ").Trim(" ");
+ }
+ }
+ else
+ {
+ queryWordsReplaced = querywords;
+ }
+
+ //additional fields normally
+ sb.Append(f);
+ sb.Append(":");
+ sb.Append("(");
+ foreach (var w in queryWordsReplaced)
+ {
+ sb.Append(w.ToLower());
+ sb.Append("* ");
+ }
+
+ sb.Append(")");
+ sb.Append(" ");
+ }
+
+ sb.Append(") ");
+ }
+ }
+
+ //must match index type
+ sb.Append("+__IndexType:");
+ sb.Append(type);
+
+ return true;
+ }
+
+ private void AppendNodeNamePhraseWithBoost(StringBuilder sb, string query, IEnumerable allLangs)
+ {
+ //node name exactly boost x 10
+ sb.Append("nodeName: (");
+ sb.Append(query.ToLower());
+ sb.Append(")^10.0 ");
+
+ //also search on all variant node names
+ foreach (var lang in allLangs)
+ {
+ //node name exactly boost x 10
+ sb.Append($"nodeName_{lang}: (");
+ sb.Append(query.ToLower());
+ sb.Append(")^10.0 ");
+ }
+ }
+
+ private void AppendNodeNameExactWithBoost(StringBuilder sb, string query, IEnumerable allLangs)
+ {
+ //node name exactly boost x 10
+ sb.Append("nodeName:");
+ sb.Append("\"");
+ sb.Append(query.ToLower());
+ sb.Append("\"");
+ sb.Append("^10.0 ");
+ //also search on all variant node names
+ foreach (var lang in allLangs)
+ {
+ //node name exactly boost x 10
+ sb.Append($"nodeName_{lang}:");
+ sb.Append("\"");
+ sb.Append(query.ToLower());
+ sb.Append("\"");
+ sb.Append("^10.0 ");
+ }
+ }
+
+ private void AppendNodeNameWithWildcards(StringBuilder sb, string[] querywords, IEnumerable allLangs)
+ {
+ //node name normally with wildcards
+ sb.Append("nodeName:");
+ sb.Append("(");
+ foreach (var w in querywords)
+ {
+ sb.Append(w.ToLower());
+ sb.Append("* ");
+ }
+
+ sb.Append(") ");
+ //also search on all variant node names
+ foreach (var lang in allLangs)
+ {
+ //node name normally with wildcards
+ sb.Append($"nodeName_{lang}:");
+ sb.Append("(");
+ foreach (var w in querywords)
+ {
+ sb.Append(w.ToLower());
+ sb.Append("* ");
+ }
+
+ sb.Append(") ");
+ }
+ }
+
+ private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[]? startNodeIds, string? searchFrom, bool ignoreUserStartNodes, IEntityService entityService)
+ {
+ if (sb == null)
+ {
+ throw new ArgumentNullException(nameof(sb));
+ }
+
+ if (entityService == null)
+ {
+ throw new ArgumentNullException(nameof(entityService));
+ }
+
+ UdiParser.TryParse(searchFrom, true, out Udi? udi);
+ searchFrom = udi == null ? searchFrom : entityService.GetId(udi).Result.ToString();
+
+ TreeEntityPath? entityPath =
+ int.TryParse(searchFrom, NumberStyles.Integer, CultureInfo.InvariantCulture, out var searchFromId) &&
+ searchFromId > 0
+ ? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault()
+ : null;
+ if (entityPath != null)
+ {
+ // find... only what's underneath
+ sb.Append("+__Path:");
+ AppendPath(sb, entityPath.Path, false);
+ sb.Append(" ");
+ }
+ else if (startNodeIds?.Length == 0)
+ {
+ // make sure we don't find anything
+ sb.Append("+__Path:none ");
+ }
+ else if (startNodeIds?.Contains(-1) == false && ignoreUserStartNodes == false) // -1 = no restriction
+ {
+ IEnumerable entityPaths = entityService.GetAllPaths(objectType, startNodeIds);
+
+ // for each start node, find the start node, and what's underneath
+ // +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...)
+ sb.Append("+__Path:(");
+ var first = true;
+ foreach (TreeEntityPath ep in entityPaths)
+ {
+ if (first)
+ {
+ first = false;
+ }
+ else
+ {
+ sb.Append(" ");
+ }
+
+ AppendPath(sb, ep.Path, true);
+ }
+
+ sb.Append(") ");
+ }
+ }
+
+ private void AppendPath(StringBuilder sb, string path, bool includeThisNode)
+ {
+ path = path.Replace("-", "\\-").Replace(",", "\\,");
+ if (includeThisNode)
+ {
+ sb.Append(path);
+ sb.Append(" ");
+ }
+
+ sb.Append(path);
+ sb.Append("\\,*");
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexTests.cs
new file mode 100644
index 0000000000..41b66a193c
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/ExamineExternalIndexTests.cs
@@ -0,0 +1,137 @@
+using Examine;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Cache;
+using Umbraco.Cms.Core.Models.ContentEditing;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Security;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Infrastructure.Examine.DependencyInjection;
+using Umbraco.Cms.Infrastructure.HostedServices;
+using Umbraco.Cms.Infrastructure.Search;
+using Umbraco.Cms.Tests.Common.Builders;
+using Umbraco.Cms.Tests.Common.Builders.Extensions;
+using Umbraco.Cms.Tests.Common.Testing;
+using Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services;
+using Umbraco.Cms.Web.BackOffice.Security;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine;
+
+[TestFixture]
+[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
+public class ExamineExternalIndexTests : ExamineBaseTest
+{
+ private const string ContentName = "TestContent";
+
+ [SetUp]
+ public void Setup()
+ {
+ TestHelper.DeleteDirectory(GetIndexPath(Constants.UmbracoIndexes.InternalIndexName));
+ TestHelper.DeleteDirectory(GetIndexPath(Constants.UmbracoIndexes.ExternalIndexName));
+ var httpContext = new DefaultHttpContext();
+ httpContext.RequestServices = Services;
+ Mock.Get(TestHelper.GetHttpContextAccessor()).Setup(x => x.HttpContext).Returns(httpContext);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ // When disposing examine, it does a final write, which ends up locking the file if the indexing is not done yet. So we have this wait to circumvent that.
+ Thread.Sleep(1500);
+ // Sometimes we do not dispose all services in time and the test fails because the log file is locked. Resulting in all other tests failing as well
+ Services.DisposeIfDisposable();
+ }
+
+ private IExamineExternalIndexSearcherTest ExamineExternalIndexSearcher =>
+ GetRequiredService();
+
+ private IContentTypeService ContentTypeService => GetRequiredService();
+
+ private ContentService ContentService => (ContentService)GetRequiredService();
+
+ private IUserStore BackOfficeUserStore =>
+ GetRequiredService>();
+
+ private IBackOfficeSignInManager BackOfficeSignInManager => GetRequiredService();
+
+ protected override void CustomTestSetup(IUmbracoBuilder builder)
+ {
+ builder.Services.AddUnique();
+ builder.Services.AddUnique();
+ builder
+ .AddNotificationHandler();
+ builder.AddNotificationHandler();
+ builder.AddExamineIndexes();
+ builder.AddBackOfficeIdentity();
+ builder.Services.AddHostedService();
+ }
+
+ private IEnumerable ExamineExternalIndexSearch(string query, int pageSize = 20, int pageIndex = 0) =>
+ ExamineExternalIndexSearcher.Search(query, UmbracoEntityTypes.Document,
+ pageSize, pageIndex, out _, ignoreUserStartNodes: true);
+
+ private async Task SetupUserIdentity(string userId)
+ {
+ var identity =
+ await BackOfficeUserStore.FindByIdAsync(userId, CancellationToken.None);
+ await BackOfficeSignInManager.SignInAsync(identity, false);
+ }
+
+ [Test]
+ public async Task Search_Published_Content_With_Query_By_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .Build();
+ ContentTypeService.Save(contentType);
+
+ var content = new ContentBuilder()
+ .WithId(0)
+ .WithName(ContentName)
+ .WithContentType(contentType)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.SaveAndPublish(content), Constants.UmbracoIndexes.ExternalIndexName);
+
+ // Act
+ IEnumerable actual = ExamineExternalIndexSearch(ContentName);
+
+ // Assert
+ IEnumerable searchResults = actual.ToArray();
+ Assert.AreEqual(1, searchResults.Count());
+ Assert.AreEqual(searchResults.First().Values["nodeName"], ContentName);
+ }
+
+ [Test]
+ public async Task Search_Unpublished_Content_With_Query_By_Content_Name()
+ {
+ // Arrange
+ await SetupUserIdentity(Constants.Security.SuperUserIdAsString);
+
+ var contentType = new ContentTypeBuilder()
+ .WithId(0)
+ .Build();
+ ContentTypeService.Save(contentType);
+
+ var content = new ContentBuilder()
+ .WithId(0)
+ .WithName(ContentName)
+ .WithContentType(contentType)
+ .Build();
+ await ExecuteAndWaitForIndexing(() => ContentService.Save(content), Constants.UmbracoIndexes.ExternalIndexName);
+
+ // Act
+ IEnumerable actual = ExamineExternalIndexSearch(ContentName);
+
+ // Assert
+ Assert.AreEqual(0, actual.Count());
+ }
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IExamineExternalIndexSearcherTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IExamineExternalIndexSearcherTest.cs
new file mode 100644
index 0000000000..eba1ebf7bb
--- /dev/null
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IExamineExternalIndexSearcherTest.cs
@@ -0,0 +1,18 @@
+using Examine;
+using Umbraco.Cms.Core.Models.ContentEditing;
+
+namespace Umbraco.Cms.Tests.Integration.Umbraco.Examine.Lucene.UmbracoExamine;
+
+public interface IExamineExternalIndexSearcherTest
+{
+
+ IEnumerable Search(
+ string query,
+ UmbracoEntityTypes entityType,
+ int pageSize,
+ long pageIndex,
+ out long totalFound,
+ string? searchFrom = null,
+ bool ignoreUserStartNodes = false);
+
+}
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs
index 01ca9284f7..21a06c3643 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Examine.Lucene/UmbracoExamine/IndexInitializer.cs
@@ -19,7 +19,6 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Infrastructure.Persistence;
-using Umbraco.Cms.Web.Common.DependencyInjection;
using Directory = Lucene.Net.Store.Directory;
using StaticServiceProvider = Umbraco.Cms.Core.DependencyInjection.StaticServiceProvider;
@@ -37,6 +36,7 @@ public class IndexInitializer
private readonly PropertyEditorCollection _propertyEditors;
private readonly IScopeProvider _scopeProvider;
private readonly IShortStringHelper _shortStringHelper;
+ private readonly IContentTypeService _contentTypeService;
public IndexInitializer(
IShortStringHelper shortStringHelper,
@@ -45,7 +45,8 @@ public class IndexInitializer
IScopeProvider scopeProvider,
ILoggerFactory loggerFactory,
IOptions contentSettings,
- ILocalizationService localizationService)
+ ILocalizationService localizationService,
+ IContentTypeService contentTypeService)
{
_shortStringHelper = shortStringHelper;
_propertyEditors = propertyEditors;
@@ -54,6 +55,7 @@ public class IndexInitializer
_loggerFactory = loggerFactory;
_contentSettings = contentSettings;
_localizationService = localizationService;
+ _contentTypeService = contentTypeService;
}
public IndexInitializer(
@@ -62,7 +64,7 @@ public class IndexInitializer
MediaUrlGeneratorCollection mediaUrlGenerators,
IScopeProvider scopeProvider,
ILoggerFactory loggerFactory,
- IOptions contentSettings)
+ IOptions contentSettings, IContentTypeService contentTypeService)
: this(
shortStringHelper,
propertyEditors,
@@ -70,7 +72,7 @@ public class IndexInitializer
scopeProvider,
loggerFactory,
contentSettings,
- StaticServiceProvider.Instance.GetRequiredService())
+ StaticServiceProvider.Instance.GetRequiredService(), contentTypeService)
{
}
@@ -83,7 +85,9 @@ public class IndexInitializer
_shortStringHelper,
_scopeProvider,
publishedValuesOnly,
- _localizationService);
+ _localizationService,
+ _contentTypeService,
+ _loggerFactory.CreateLogger());
return contentValueSetBuilder;
}