diff --git a/.editorconfig b/.editorconfig index 2a15465c59..5f3b4d684a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,10 +1,17 @@ -root=true +# editorconfig.org +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation [*] -end_of_line = crlf +insert_final_newline = true +end_of_line = lf indent_style = space indent_size = 4 -trim_trailing_whitespace = true -[*.{cs,cshtml,csx,vb,vbx,vbhtml,fs,fsx,txt,ps1,sql}] -indent_size = 4 +# Trim trailing whitespace, limited support. +# https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a664be3a85 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,49 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto merge=union +*.vbproj text=auto merge=union +*.fsproj text=auto merge=union +*.dbproj text=auto merge=union +*.sln text=auto eol=crlf merge=union diff --git a/.gitignore b/.gitignore index c5d51abd0d..e03ef6f408 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,7 @@ src/Umbraco.Tests/media tools/docfx/* apidocs/_site/* src/*/project.lock.json +src/.idea/* apidocs/api/* build/docs.zip @@ -149,4 +150,4 @@ src/PrecompiledWeb/* build.out/ build.tmp/ build/hooks/ -build/temp/ \ No newline at end of file +build/temp/ diff --git a/BUILD.md b/BUILD.md index 9515e26654..30ea950778 100644 --- a/BUILD.md +++ b/BUILD.md @@ -14,6 +14,43 @@ By default, this builds the current version. It is possible to specify a differe Valid version strings are defined in the `Set-UmbracoVersion` documentation below. +## PowerShell Quirks + +There is a good chance that running `build.ps1` ends up in error, with messages such as + +>The file ...\build\build.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies. + +PowerShell has *Execution Policies* that may prevent the script from running. You can check the current policies with: + + PS> Get-ExecutionPolicy -List + + Scope ExecutionPolicy + ----- --------------- + MachinePolicy Undefined + UserPolicy Undefined + Process Undefined + CurrentUser Undefined + LocalMachine RemoteSigned + +Policies can be `Restricted`, `AllSigned`, `RemoteSigned`, `Unrestricted` and `Bypass`. Scopes can be `MachinePolicy`, `UserPolicy`, `Process`, `CurrentUser`, `LocalMachine`. You need the current policy to be `RemoteSigned`—as long as it is `Undefined`, the script cannot run. You can change the current user policy with: + + PS> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned + +Alternatively, you can do it at machine level, from within an elevated PowerShell session: + + PS> Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned + +And *then* the script should run. It *might* however still complain about executing scripts, with messages such as: + +>Security warning - Run only scripts that you trust. While scripts from the internet can be useful, this script can potentially harm your computer. If you trust this script, use the Unblock-File cmdlet to allow the script to run without this warning message. Do you want to run ...\build\build.ps1? +[D] Do not run [R] Run once [S] Suspend [?] Help (default is "D"): + +This is usually caused by the scripts being *blocked*. And that usually happens when the source code has been downloaded as a Zip file. When Windows downloads Zip files, they are marked as *blocked* (technically, they have a Zone.Identifier alternate data stream, with a value of "3" to indicate that they were downloaded from the Internet). And when such a Zip file is un-zipped, each and every single file is also marked as blocked. + +The best solution is to unblock the Zip file before un-zipping: right-click the files, open *Properties*, and there should be a *Unblock* checkbox at the bottom of the dialog. If, however, the Zip file has already been un-zipped, it is possible to recursively unblock all files from PowerShell with: + + PS> Get-ChildItem -Recurse *.* | Unblock-File + ## Notes Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..3baa5dbe66 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,80 @@ +# Code of Conduct + +## 1. Purpose + +A primary goal of Umbraco CMS is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof). + +This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. + +We invite all those who participate in Umbraco CMS to help us create safe and positive experiences for everyone. + +## 2. Open Source Citizenship + +A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community. + +Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society. + +If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know. + +## 3. Expected Behavior + +The following behaviors are expected and requested of all community members: + +* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community. +* Exercise consideration and respect in your speech and actions. +* Attempt collaboration before conflict. +* Refrain from demeaning, discriminatory, or harassing behavior and speech. +* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential. +* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations. + +## 4. Unacceptable Behavior + +The following behaviors are considered harassment and are unacceptable within our community: + +* Violence, threats of violence or violent language directed against another person. +* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language. +* Posting or displaying sexually explicit or violent material. +* Posting or threatening to post other people’s personally identifying information ("doxing"). +* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability. +* Inappropriate photography or recording. +* Inappropriate physical contact. You should have someone’s consent before touching them. +* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances. +* Deliberate intimidation, stalking or following (online or in person). +* Advocating for, or encouraging, any of the above behavior. +* Sustained disruption of community events, including talks and presentations. + +## 5. Consequences of Unacceptable Behavior + +Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. + +If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event). + +## 6. Reporting Guidelines + +If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. Please contact Sebastiaan Janssen - [sj@umbraco.dk](mailto:sj@umbraco.dk). + +Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress. + +## 7. Addressing Grievances + +If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Umbraco with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies. + +## 8. Scope + +We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business. + +This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members. + +## 9. Contact info + +Sebastiaan Janssen - [sj@umbraco.dk](mailto:sj@umbraco.dk) + +## 10. License and attribution + +This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/). + +Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy). + +Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..0e87f9824d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,207 @@ +# Contributing to Umbraco CMS + +👍🎉 First off, thanks for taking the time to contribute! 🎉👍 + +The following is a set of guidelines for contributing to Umbraco CMS. + +These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you 💖. + +#### Table Of Contents + +[Code of Conduct](#code-of-conduct) + +[How Can I Contribute?](#how-can-i-contribute) + * [Reporting Bugs](#reporting-bugs) + * [Suggesting Enhancements](#suggesting-enhancements) + * [Your First Code Contribution](#your-first-code-contribution) + * [Pull Requests](#pull-requests) + +[Styleguides](#styleguides) + +[What should I know before I get started?](#what-should-i-know-before-i-get-started) + * [Working with the source code](#working-with-the-source-code) + * [What branch should I target for my contributions?](#what-branch-should-i-target-for-my-contributions) + * [Building Umbraco from source code](#building-umbraco-from-source-code) + * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository) + +[How do I even begin?](#how-do-i-even-begin) + +[Problems?](#problems) + +[Credits](#credits) + +## Code of Conduct + +This project and everyone participating in it is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk). + +## How Can I Contribute? + +### Reporting Bugs +This section guides you through submitting a bug report for Umbraco CMS. Following these guidelines helps maintainers and the community understand your report 📝, reproduce the behavior 💻 💻, and find related reports 🔎. + +Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](ISSUE_TEMPLATE.md), the information it asks for helps us resolve issues faster. + +> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. + +##### Before Submitting A Bug Report + + * Most importantly, check **if you can reproduce the problem** in the [latest version of Umbraco](https://our.umbraco.org/download/). We might have already fixed your particular problem. + * It also helps tremendously to check if the issue you're experiencing is present in **a clean install** of the Umbraco version you're currently using. Custom code can have side-effects that don't occur in a clean install. + * **Use the Google**. Whatever you're experiencing, Google it plus "Umbraco" - usually you can get some pretty good hints from the search results, including open issues and further troubleshooting hints. + * If you do find and existing issue has **and the issue is still open**, add a comment to the existing issue if you have additional information. If you have the same problem and no new info to add, just "star" the issue. + +Explain the problem and include additional details to help maintainers reproduce the problem. The following is a long description which we've boiled down into a few very simple question in the issue tracker when you create a new issue. We're listing the following hints to indicate that the most successful reports usually have a lot of this ground covered: + + * **Use a clear and descriptive title** for the issue to identify the problem. + * **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining which steps you took in the backoffice to get to a certain undesireable result, e.g. you created a document type, inherting 3 levels deep, added a certain datatype, tried to save it and you got an error. + * **Provide specific examples to demonstrate the steps**. If you wrote some code, try to provide a code sample as specific as possible to be able to reproduce the behavior. + * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. + * **Explain which behavior you expected to see instead and why.** + +Provide more context by answering these questions: + + * **Can you reproduce the problem** when `debug="false"` in your `web.config` file? + * **Did the problem start happening recently** (e.g. after updating to a new version of Umbraco) or was this always a problem? + * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. + +Include details about your configuration and environment: + + * **Which version of Umbraco are you using?** + * **What is the environment you're using Umbraco in?** Is this a problem on your local machine or on a server. Tell us about your configuration: Windows version, IIS/IISExpress, database type, etc. + * **Which packages do you have installed?** + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for Atom, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion 📝 and find related suggestions 🔎. + +Most of the suggestions in the [reporting bugs](#reporting-bugs) section also count for suggesting enhancements. + +Some additional hints that may be helpful: + + * **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Umbraco which the suggestion is related to. + * **Explain why this enhancement would be useful to most Umbraco users** and isn't something that can or should be implemented as a [community package](https://our.umbraco.org/projects/). + +### Your First Code Contribution + +Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` and issues](http://issues.umbraco.org/issues/U4?q=%28project%3A+%7BU4%7D+Difficulty%3A+%7BVery+Easy%7D+%23Easy+%23Unresolved+Priority%3A+Normal+%23Major+%23Show-stopper+State%3A+-%7BIn+Progress%7D+sort+by%3A+votes+Affected+versions%3A+-6.*+Affected+versions%3A+-4.*%29+OR+%28tag%3A+%7BUp+For+Grabs%7D+%23Unresolved+%29). + +The issue list is sorted by total number of upvotes. While not perfect, number of upvotes is a reasonable proxy for impact a given change will have. + +### Pull Requests + +The most successful pull requests usually look a like this: + + * Fill in the required template + * Include screenshots and animated GIFs in your pull request whenever possible. + * Unit tests, while optional are awesome, thank you! + * New code is commented with documentation from which [the reference documentation](https://our.umbraco.org/documentation/Reference/) is generated + +Again, these are guidelines, not strict requirements. + +## Styleguides + +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. + +## What should I know before I get started? + +### Working with the source code + +Some parts of our source code is over 10 years old now. And when we say "old", we mean "mature" of course! + +There's two big areas that you should know about: + + 1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes. + You may need to run the following commands to set up gulp properly: + ``` + npm cache clean + npm install -g bower + npm install -g gulp + npm install -g gulp-cli + npm install + gulp build + ``` + 2. "The rest" is a C# based codebase, with some traces of our WebForms past but mostly ASP.NET MVC based these days. You can make changes, build them in Visual Studio, and hit `F5` to see the result. + +To find the general areas of something you're looking to fix or improve, have a look at the following two parts of the API documentation. + + * [The AngularJS based backoffice files](https://our.umbraco.org/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`) + * [The rest](https://our.umbraco.org/apidocs/csharp/) + +### What branch should I target for my contributions? + +We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `dev-v7`. Whatever the default is, that's where we'd like you to target your contributions. + +![What branch do you want me to target?](tools/contributing/defaultbranch.png) + +### Building Umbraco from source code + +The easiest way to get started is to run `build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details. + +Alternatively, you can open `src\umbraco.sln` in Visual Studio 2017 ([the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile. + +![Gulp build in Visual Studio](tools/contributing/gulpbuild.png) + +After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser, follow the directions there to get a working Umbraco install up and running. + +### Keeping your Umbraco fork in sync with the main repository + +We recommend you sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier. + +Also, if you've submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course. + +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/dev-v7 +``` + +In this command we're syncing with the `dev-v7` branch, but you can of course choose another one if needed. + +(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated)) + +## How do I even begin? + +Great question! The short version goes like this: + + * **Fork** - create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS) + + ![Fork the repository](tools/contributing/forkrepository.png) + + * **Clone** - when GitHub has created your fork, you can clone it in your favorite Git tool + + ![Clone the fork](tools/contributing/clonefork.png) + + * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](#building-umbraco-from-source-code) + * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will happily give feedback + * **Commit** - done? Yay! 🎉 It is recommended to create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-U4-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `U4-12345` + * **Push** - great, now you can push the changes up to your fork on GitHub + * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go. + + ![Create a pull request](tools/contributing/createpullrequest.png) + +The Umbraco development team can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request you to make some additional changes. + +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! + +Not all changes are wanted so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes at this and we'll be nice about it, thanking you for spending your valueable time. + +Remember, if an issue is in the `Up for grabs` list or you've asked for some feedback before you send us a PR, your PR will not be closed as unwanted. + +## Problems? + +Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.org/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely! + +## Credits + +This contribution guide borrows heavily from the excellent work on [the Atom contribution guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md). A big [#h5yr](http://h5yr.com/) to them! diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..db1e5c88bd --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +### Prerequisites + +- [ ] I have written a descriptive pull-request title +- [ ] I have linked this PR to an issue on the tracker at http://issues.umbraco.org + +### Description + + + + + diff --git a/README.md b/README.md index 69aba2f06a..045c91fae8 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,44 @@ Umbraco CMS =========== -The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 350,000 websites worldwide: [https://umbraco.com](https://umbraco.com) +The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com) [![ScreenShot](vimeo.png)](https://vimeo.com/172382998/) -## Umbraco CMS ## +## Umbraco CMS Umbraco is a free open source Content Management System built on the ASP.NET platform. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social. - -## Building Umbraco from source ## - -The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. - -Note that you can always [download a nightly build](http://nightly.umbraco.org/?container=umbraco-750) so you don't have to build the code yourself. - -## Watch an introduction video ## +## Watch an introduction video [![ScreenShot](http://umbraco.com/images/whatisumbraco.png)](https://umbraco.tv/videos/umbraco-v7/content-editor/basics/introduction/cms-explanation/) -## Umbraco - The Friendly CMS ## +## Umbraco - The Friendly CMS For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. Umbraco is not only loved by developers, but is a content editors dream. Enjoy intuitive editing tools, media management, responsive views and approval workflows to send your content live. -Used by more than 350,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 200,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. +Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. To view more examples, please visit [https://umbraco.com/why-umbraco/#caseStudies](https://umbraco.com/why-umbraco/#caseStudies) -## Why Open Source? ## +## Why Open Source? As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible and community contributions and packages are available for all to use. -## Downloading ## +## Trying out Umbraco CMS -The downloadable Umbraco releases live at [https://our.umbraco.org/download](https://our.umbraco.org/download). +[Umbraco Cloud](https://umbraco.com) is the easiest and fastest way to use Umbraco yet with full support for all your custom .NET code and intergrations. You're up and running in less than a minute and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14 day trial, no credit card needed. -## Forums ## +If you want to DIY you can [download Umbraco](https://our.umbraco.org/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Xloud, but you'll need to find a place to host yourself and handling deployments and upgrades is all down to you. -Peer-to-peer support is available 24/7 at the community forum on [https://our.umbraco.org](https://our.umbraco.org). +## Community -## Contribute to Umbraco ## +Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.org). Our Umbraco feature forums for questions and answers, documentation, downloadable plugins for Umbraco and a rich collection of community resources. -Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](https://our.umbraco.org/contribute). +## Contribute to Umbraco -## Found a bug? ## +Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](CONTRIBUTING.md). + +## Found a bug? Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](https://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). diff --git a/build/Azure/azuregalleryrelease.ps1 b/build/Azure/azuregalleryrelease.ps1 new file mode 100644 index 0000000000..502ca3010e --- /dev/null +++ b/build/Azure/azuregalleryrelease.ps1 @@ -0,0 +1,59 @@ +Param( + [string]$GitHubPersonalAccessToken, + [string]$Directory +) +$workingDirectory = $Directory +CD $workingDirectory + +# Clone repo +$fullGitUrl = "https://$env:GIT_URL/$env:GIT_REPOSITORYNAME.git" +git clone $fullGitUrl 2>&1 | % { $_.ToString() } + +# Remove everything so that unzipping the release later will update everything +# Don't remove the readme file nor the git directory +Write-Host "Cleaning up git directory before adding new version" +Remove-Item -Recurse $workingDirectory\$env:GIT_REPOSITORYNAME\* -Exclude README.md,.git + +# Find release zip +$zipsDir = "$workingDirectory\$env:BUILD_DEFINITIONNAME\zips" +$pattern = "UmbracoCms.([0-9]{1,2}.[0-9]{1,3}.[0-9]{1,3}).zip" +Write-Host "Searching for Umbraco release files in $workingDirectory\$zipsDir for a file with pattern $pattern" +$file = (Get-ChildItem $zipsDir | Where-Object { $_.Name -match "$pattern" }) + +if($file) +{ + # Get release name + $version = [regex]::Match($file.Name, $pattern).captures.groups[1].value + $releaseName = "Umbraco $version" + Write-Host "Found $releaseName" + + # Unzip into repository to update release + Add-Type -AssemblyName System.IO.Compression.FileSystem + Write-Host "Unzipping $($file.FullName) to $workingDirectory\$env:GIT_REPOSITORYNAME" + [System.IO.Compression.ZipFile]::ExtractToDirectory("$($file.FullName)", "$workingDirectory\$env:GIT_REPOSITORYNAME") + + # Telling git who we are + git config --global user.email "coffee@umbraco.com" 2>&1 | % { $_.ToString() } + git config --global user.name "Umbraco HQ" 2>&1 | % { $_.ToString() } + + # Commit + CD $env:GIT_REPOSITORYNAME + Write-Host "Committing Umbraco $version Release from Build Output" + + git add . 2>&1 | % { $_.ToString() } + git commit -m " Release $releaseName from Build Output" 2>&1 | % { $_.ToString() } + + # Tag the release + git tag -a "v$version" -m "v$version" + + # Push release to master + $fullGitAuthUrl = "https://$($env:GIT_USERNAME):$GitHubPersonalAccessToken@$env:GIT_URL/$env:GIT_REPOSITORYNAME.git" + git push $fullGitAuthUrl 2>&1 | % { $_.ToString() } + + #Push tag to master + git push $fullGitAuthUrl --tags 2>&1 | % { $_.ToString() } +} +else +{ + Write-Error "Umbraco release file not found, searched in $workingDirectory\$zipsDir for a file with pattern $pattern - cancelling" +} diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index cbab1536b0..7775aaabbe 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -24,10 +24,8 @@ - - - + @@ -36,8 +34,8 @@ - - + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 1b2ad2d6bf..a8fb9cf0c3 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -31,6 +31,7 @@ + diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt index e85b22a902..2b6da733a1 100644 --- a/build/NuSpecs/tools/ReadmeUpgrade.txt +++ b/build/NuSpecs/tools/ReadmeUpgrade.txt @@ -8,7 +8,7 @@ ---------------------------------------------------- -*** IMPORTANT NOTICE FOR 7.7 UPGRADES *** +*** IMPORTANT NOTICE FOR UPGRADES FROM VERSIONS BELOW 7.7.0 *** Be sure to read the version specific upgrade information before proceeding: https://our.umbraco.org/documentation/Getting-Started/Setup/Upgrading/version-specific#version-7-7-0 diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1 index 0e62fb0749..592dc951e0 100644 --- a/build/NuSpecs/tools/install.ps1 +++ b/build/NuSpecs/tools/install.ps1 @@ -98,10 +98,51 @@ if ($project) { $umbracoUIXMLDestination = Join-Path $projectPath "Umbraco\Config\Create\UI.xml" Copy-Item $umbracoUIXMLSource $umbracoUIXMLDestination -Force } else { + # This part only runs for upgrades + $upgradeViewSource = Join-Path $umbracoFolderSource "Views\install\*" $upgradeView = Join-Path $umbracoFolder "Views\install\" Write-Host "Copying2 ${upgradeViewSource} to ${upgradeView}" Copy-Item $upgradeViewSource $upgradeView -Force + + Try + { + # Disable tours for upgrades, presumably Umbraco experience is already available + $umbracoSettingsConfigPath = Join-Path $configFolder "umbracoSettings.config" + $content = (Get-Content $umbracoSettingsConfigPath).Replace('','') + # Saves with UTF-8 encoding without BOM which makes sure Umbraco can still read it + # Reference: https://stackoverflow.com/a/32951824/5018 + [IO.File]::WriteAllLines($umbracoSettingsConfigPath, $content) + } + Catch + { + # Not a big problem if this fails, let it go + } + + Try + { + $uiXmlConfigPath = Join-Path $umbracoFolder -ChildPath "Config" | Join-Path -ChildPath "create" | Join-Path -ChildPath "UI.xml" + $uiXmlFile = Join-Path $umbracoFolder -ChildPath "Config" | Join-Path -ChildPath "create" | Join-Path -ChildPath "UI.xml" + + $uiXml = New-Object System.Xml.XmlDocument + $uiXml.PreserveWhitespace = $true + + $uiXml.Load($uiXmlFile) + $createExists = $uiXml.SelectNodes("//nodeType[@alias='macros']/tasks/create") + + if($createExists.Count -eq 0) + { + $macrosTasksNode = $uiXml.SelectNodes("//nodeType[@alias='macros']/tasks") + + #Creating: + $createNode = $uiXml.CreateElement("create") + $createNode.SetAttribute("assembly", "umbraco") + $createNode.SetAttribute("type", "macroTasks") + $macrosTasksNode.AppendChild($createNode) + $uiXml.Save($uiXmlFile) + } + } + Catch { } } $installFolder = Join-Path $projectPath "Install" diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt index 65aa9c2b53..5a549e3fd8 100644 --- a/build/NuSpecs/tools/trees.config.install.xdt +++ b/build/NuSpecs/tools/trees.config.install.xdt @@ -43,7 +43,7 @@ xdt:Locator="Match(application,alias)" xdt:Transform="InsertIfMissing" /> - - - - diff --git a/build/build.ps1 b/build/build.ps1 index ae4fcb679b..20eeb1a8ea 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -328,15 +328,7 @@ $this.CopyFiles("$src\Umbraco.Web.UI\umbraco\js", "*", "$tmp\WebApp\umbraco\js") $this.CopyFiles("$src\Umbraco.Web.UI\umbraco\lib", "*", "$tmp\WebApp\umbraco\lib") $this.CopyFiles("$src\Umbraco.Web.UI\umbraco\views", "*", "$tmp\WebApp\umbraco\views") - $this.CopyFiles("$src\Umbraco.Web.UI\umbraco\preview", "*", "$tmp\WebApp\umbraco\preview") - - # prepare WebPI - Write-Host "Prepare WebPI" - $this.RemoveDirectory("$tmp\WebPi") - mkdir "$tmp\WebPi" > $null - mkdir "$tmp\WebPi\umbraco" > $null - $this.CopyFiles("$tmp\WebApp", "*", "$tmp\WebPi\umbraco") - $this.CopyFiles("$src\WebPi", "*", "$tmp\WebPi") + $this.CopyFiles("$src\Umbraco.Web.UI\umbraco\preview", "*", "$tmp\WebApp\umbraco\preview") }) $ubuild.DefineMethod("PackageZip", @@ -359,19 +351,7 @@ "$tmp\WebApp\*" ` "-x!dotless.Core.*" "-x!Content_Types.xml" "-x!*.pdb" "-x!Umbraco.Compat7.*" ` > $null - if (-not $?) { throw "Failed to zip UmbracoCms." } - - Write-Host "Zip WebPI" - &$this.BuildEnv.Zip a -r "$out\UmbracoCms.WebPI.$($this.Version.Semver).zip" "-x!*.pdb" ` - "$tmp\WebPi\*" ` - "-x!dotless.Core.*" "-x!Umbraco.Compat7.*" ` - > $null - if (-not $?) { throw "Failed to zip UmbracoCms.WebPI." } - - # hash the webpi file - Write-Host "Hash WebPI" - $hash = $this.GetFileHash("$out\UmbracoCms.WebPI.$($this.Version.Semver).zip") - Write-Output $hash | out-file "$out\webpihash.txt" -encoding ascii + if (-not $?) { throw "Failed to zip UmbracoCms." } }) $ubuild.DefineMethod("PrepareBuild", @@ -450,6 +430,12 @@ if ($this.OnError()) { return } }) + $ubuild.DefineMethod("PrepareAzureGallery", + { + Write-Host "Prepare Azure Gallery" + $this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput) + }) + $ubuild.DefineMethod("Build", { $this.PrepareBuild() @@ -475,6 +461,8 @@ if ($this.OnError()) { return } $this.PackageNuGet() if ($this.OnError()) { return } + $this.PrepareAzureGallery() + if ($this.OnError()) { return } }) # ################################################################ diff --git a/src/NuGet.Config b/src/NuGet.Config index dcccfdd5c1..89e79db976 100644 --- a/src/NuGet.Config +++ b/src/NuGet.Config @@ -2,6 +2,6 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index 236527907c..2aaa4fc1d4 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -26,63 +26,48 @@ "jquery-migrate": "1.4.0", "angular-dynamic-locale": "0.1.28", "ng-file-upload": "~7.3.8", - "tinymce": "~4.5.3", + "tinymce": "~4.7.1", "codemirror": "~5.3.0", "angular-local-storage": "~0.2.3", "moment": "~2.10.3", "ace-builds": "^1.2.3", - "font-awesome": "~4.2", - "clipboard": "1.7.1" + "clipboard": "1.7.1", + "font-awesome": "~4.2" }, - "install": { - "path": "lib-bower", - "ignore": [ "font-awesome", "angular", "bootstrap", "codemirror" ], - "sources": { "moment": "bower_components/moment/min/moment-with-locales.js", - "underscore": [ "bower_components/underscore/underscore-min.js", "bower_components/underscore/underscore-min.map" ], - "jquery": [ "bower_components/jquery/dist/jquery.min.js", "bower_components/jquery/dist/jquery.min.map" ], - "angular-dynamic-locale": [ "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js", "bower_components/angular-dynamic-locale/tmhDynamicLocale.min.js.map" ], - "angular-local-storage": [ "bower_components/angular-local-storage/dist/angular-local-storage.min.js", "bower_components/angular-local-storage/dist/angular-local-storage.min.js.map" ], - "tinymce": [ "bower_components/tinymce/tinymce.min.js" ], - "typeahead.js": "bower_components/typeahead.js/dist/typeahead.bundle.min.js", - - "rgrove-lazyload":"bower_components/rgrove-lazyload/lazyload.js", - - "ng-file-upload":"bower_components/ng-file-upload/ng-file-upload.min.js", - - "jquery-ui":"bower_components/jquery-ui/jquery-ui.min.js", - - "jquery-migrate":"bower_components/jquery-migrate/jquery-migrate.min.js", - + "rgrove-lazyload": "bower_components/rgrove-lazyload/lazyload.js", + "ng-file-upload": "bower_components/ng-file-upload/ng-file-upload.min.js", + "jquery-ui": "bower_components/jquery-ui/jquery-ui.min.js", + "jquery-migrate": "bower_components/jquery-migrate/jquery-migrate.min.js", "clipboard": "bower_components/clipboard/dist/clipboard.min.js" } } diff --git a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less index 513039cca4..c81b6398ed 100644 --- a/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less +++ b/src/Umbraco.Web.UI.Client/lib/bootstrap/less/thumbnails.less @@ -31,15 +31,17 @@ display: block; padding: 4px; line-height: @baseLineHeight; - border: 1px solid #ddd; + border: 1px solid @gray-8; .border-radius(@baseBorderRadius); .box-shadow(0 1px 3px rgba(0,0,0,.055)); .transition(all .2s ease-in-out); } -// Add a hover/focus state for linked versions only -a.thumbnail:hover, -a.thumbnail:focus { - border-color: @linkColor; +// Add a hover/focus state for linked versions only. +a.thumbnail:hover, +a.thumbnail:focus, +a div.thumbnail:hover, +a div.thumbnail:focus { + border-color: @turquoise; .box-shadow(0 1px 4px rgba(0,105,214,.25)); } diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js b/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js index cc2b949d22..84651510be 100644 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js +++ b/src/Umbraco.Web.UI.Client/lib/umbraco/LegacyUmbClientMgr.js @@ -350,11 +350,29 @@ Umbraco.Sys.registerNamespace("Umbraco.Application"); rootScope : function(){ return getRootScope(); }, - - reloadLocation: function() { - var injector = getRootInjector(); - var $route = injector.get("$route"); - $route.reload(); + + /** + This will reload the content frame based on it's current route, if pathToMatch is specified it will only reload it if the current + location matches the path + */ + reloadLocation: function(pathToMatch) { + + var injector = getRootInjector(); + var doChange = true; + if (pathToMatch) { + var $location = injector.get("$location"); + var path = $location.path(); + if (path != pathToMatch) { + doChange = false; + } + } + + if (doChange) { + var $route = injector.get("$route"); + $route.reload(); + var $rootScope = injector.get("$rootScope"); + $rootScope.$apply(); + } }, closeModalWindow: function(rVal) { diff --git a/src/Umbraco.Web.UI.Client/src/app.js b/src/Umbraco.Web.UI.Client/src/app.js index e7b7288f43..4a44959b15 100644 --- a/src/Umbraco.Web.UI.Client/src/app.js +++ b/src/Umbraco.Web.UI.Client/src/app.js @@ -11,7 +11,7 @@ var app = angular.module('umbraco', [ 'ngMobile', 'tmh.dynamicLocale', 'ngFileUpload', - 'LocalStorageModule' + 'LocalStorageModule' ]); var packages = angular.module("umbraco.packages", []); @@ -22,12 +22,21 @@ var packages = angular.module("umbraco.packages", []); //module is initilized. angular.module("umbraco.views", ["umbraco.viewcache"]); angular.module("umbraco.viewcache", []) - .run(function($rootScope, $templateCache) { + .run(function ($rootScope, $templateCache, localStorageService) { /** For debug mode, always clear template cache to cut down on dev frustration and chrome cache on templates */ if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) { $templateCache.removeAll(); } + else { + var storedVersion = localStorageService.get("umbVersion"); + if (!storedVersion || storedVersion !== Umbraco.Sys.ServerVariables.application.cacheBuster) { + //if the stored version doesn't match our cache bust version, clear the template cache + $templateCache.removeAll(); + //store the current version + localStorageService.set("umbVersion", Umbraco.Sys.ServerVariables.application.cacheBuster); + } + } }) .config([ //This ensures that all of our angular views are cache busted, if the path starts with views/ and ends with .html, then diff --git a/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js b/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js index 1a1ad6b325..6a8d7564dc 100644 --- a/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js +++ b/src/Umbraco.Web.UI.Client/src/canvasdesigner/canvasdesigner.controller.js @@ -7,12 +7,22 @@ var app = angular.module("Umbraco.canvasdesigner", ['colorpicker', 'ui.slider', .controller("Umbraco.canvasdesignerController", function ($scope, $http, $window, $timeout, $location, dialogService) { + var isInit = $location.search().init; + if (isInit === "true") { + //do not continue, this is the first load of this new window, if this is passed in it means it's been + //initialized by the content editor and then the content editor will actually re-load this window without + //this flag. This is a required trick to get around chrome popup mgr. We don't want to double load preview.aspx + //since that will double prepare the preview documents + return; + } + $scope.isOpen = false; $scope.frameLoaded = false; $scope.enableCanvasdesigner = 0; $scope.googleFontFamilies = {}; - $scope.pageId = $location.search().id; - $scope.pageUrl = "../dialogs/Preview.aspx?id=" + $location.search().id; + var pageId = $location.search().id; + $scope.pageId = pageId; + $scope.pageUrl = "../dialogs/Preview.aspx?id=" + pageId; $scope.valueAreLoaded = false; $scope.devices = [ { name: "desktop", css: "desktop", icon: "icon-display", title: "Desktop" }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbbackdrop.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbbackdrop.directive.js new file mode 100644 index 0000000000..ced59653dd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbbackdrop.directive.js @@ -0,0 +1,116 @@ +(function () { + "use strict"; + + function BackdropDirective($timeout, $http) { + + function link(scope, el, attr, ctrl) { + + var events = []; + + scope.clickBackdrop = function(event) { + if(scope.disableEventsOnClick === true) { + event.preventDefault(); + event.stopPropagation(); + } + }; + + function onInit() { + + if (scope.highlightElement) { + setHighlight(); + } + + } + + function setHighlight () { + + scope.loading = true; + + $timeout(function () { + + // The element to highlight + var highlightElement = angular.element(scope.highlightElement); + + if(highlightElement && highlightElement.length > 0) { + + var offset = highlightElement.offset(); + var width = highlightElement.outerWidth(); + var height = highlightElement.outerHeight(); + + // Rounding numbers + var topDistance = offset.top.toFixed(); + var topAndHeight = (offset.top + height).toFixed(); + var leftDistance = offset.left.toFixed(); + var leftAndWidth = (offset.left + width).toFixed(); + + // The four rectangles + var rectTop = el.find(".umb-backdrop__rect--top"); + var rectRight = el.find(".umb-backdrop__rect--right"); + var rectBottom = el.find(".umb-backdrop__rect--bottom"); + var rectLeft = el.find(".umb-backdrop__rect--left"); + + // Add the css + scope.rectTopCss = { "height": topDistance, "left": leftDistance + "px", opacity: scope.backdropOpacity }; + scope.rectRightCss = { "left": leftAndWidth + "px", "top": topDistance + "px", "height": height, opacity: scope.backdropOpacity }; + scope.rectBottomCss = { "height": "100%", "top": topAndHeight + "px", "left": leftDistance + "px", opacity: scope.backdropOpacity }; + scope.rectLeftCss = { "width": leftDistance, opacity: scope.backdropOpacity }; + + // Prevent interaction in the highlighted area + if(scope.highlightPreventClick) { + var preventClickElement = el.find(".umb-backdrop__highlight-prevent-click"); + preventClickElement.css({ "width": width, "height": height, "left": offset.left, "top": offset.top }); + } + + } + + scope.loading = false; + + }); + + } + + function resize() { + setHighlight(); + } + + events.push(scope.$watch("highlightElement", function (newValue, oldValue) { + if(!newValue) {return;} + if(newValue === oldValue) {return;} + setHighlight(); + })); + + $(window).on("resize.umbBackdrop", resize); + + scope.$on("$destroy", function () { + // unbind watchers + for (var e in events) { + events[e](); + } + $(window).off("resize.umbBackdrop"); + }); + + onInit(); + + } + + var directive = { + transclude: true, + restrict: "E", + replace: true, + templateUrl: "views/components/application/umb-backdrop.html", + link: link, + scope: { + backdropOpacity: "=?", + highlightElement: "=?", + highlightPreventClick: "=?", + disableEventsOnClick: "=?", + } + }; + + return directive; + + } + + angular.module("umbraco.directives").directive("umbBackdrop", BackdropDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawer.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawer.directive.js new file mode 100644 index 0000000000..e2e94e466f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawer.directive.js @@ -0,0 +1,109 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbDrawer +@restrict E +@scope + +@description +The drawer component is a global component and is already added to the umbraco markup. It is registered in globalState and can be opened and configured by raising events. + +

Markup example - how to open the drawer

+
+    
+ + + + +
+
+ +

Controller example - how to open the drawer

+
+    (function () {
+        "use strict";
+
+        function DrawerController(appState) {
+
+            var vm = this;
+
+            vm.toggleDrawer = toggleDrawer;
+
+            function toggleDrawer() {
+
+                var showDrawer = appState.getDrawerState("showDrawer");            
+
+                var model = {
+                    firstName: "Super",
+                    lastName: "Man"
+                };
+
+                appState.setDrawerState("view", "/App_Plugins/path/to/drawer.html");
+                appState.setDrawerState("model", model);
+                appState.setDrawerState("showDrawer", !showDrawer);
+                
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.DrawerController", DrawerController);
+
+    })();
+
+ +

Use the following components in the custom drawer to render the content

+
    +
  • {@link umbraco.directives.directive:umbDrawerView umbDrawerView}
  • +
  • {@link umbraco.directives.directive:umbDrawerHeader umbDrawerHeader}
  • +
  • {@link umbraco.directives.directive:umbDrawerView umbDrawerContent}
  • +
  • {@link umbraco.directives.directive:umbDrawerFooter umbDrawerFooter}
  • +
+ +@param {string} view (binding): Set the drawer view +@param {string} model (binding): Pass in custom data to the drawer + +**/ + +function Drawer($location, $routeParams, helpService, userService, localizationService, dashboardResource) { + + return { + + restrict: "E", // restrict to an element + replace: true, // replace the html element with the template + templateUrl: 'views/components/application/umbdrawer/umb-drawer.html', + transclude: true, + scope: { + view: "=?", + model: "=?" + }, + + link: function (scope, element, attr, ctrl) { + + function onInit() { + setView(); + } + + function setView() { + if (scope.view) { + //we do this to avoid a hidden dialog to start loading unconfigured views before the first activation + var configuredView = scope.view; + if (scope.view.indexOf(".html") === -1) { + var viewAlias = scope.view.toLowerCase(); + configuredView = "views/common/drawers/" + viewAlias + "/" + viewAlias + ".html"; + } + if (configuredView !== scope.configuredView) { + scope.configuredView = configuredView; + } + } + } + + onInit(); + + } + + }; + } + + angular.module('umbraco.directives').directive("umbDrawer", Drawer); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawercontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawercontent.directive.js new file mode 100644 index 0000000000..d4aa76f70e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawercontent.directive.js @@ -0,0 +1,59 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbDrawerContent +@restrict E +@scope + +@description +Use this directive to render drawer content + +

Markup example

+
+	
+        
+        
+        
+
+        
+            
+            
{{ model | json }}
+
+ + + + + +
+
+ + +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbDrawerView umbDrawerView}
  • +
  • {@link umbraco.directives.directive:umbDrawerHeader umbDrawerHeader}
  • +
  • {@link umbraco.directives.directive:umbDrawerFooter umbDrawerFooter}
  • +
+ +**/ + +(function() { + 'use strict'; + + function DrawerContentDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/application/umbdrawer/umb-drawer-content.html' + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbDrawerContent', DrawerContentDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerfooter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerfooter.directive.js new file mode 100644 index 0000000000..f76bca3a77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerfooter.directive.js @@ -0,0 +1,58 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbDrawerFooter +@restrict E +@scope + +@description +Use this directive to render a drawer footer + +

Markup example

+
+	
+        
+        
+        
+
+        
+            
+            
{{ model | json }}
+
+ + + + + +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbDrawerView umbDrawerView}
  • +
  • {@link umbraco.directives.directive:umbDrawerHeader umbDrawerHeader}
  • +
  • {@link umbraco.directives.directive:umbDrawerContent umbDrawerContent}
  • +
+ +**/ + +(function() { + 'use strict'; + + function DrawerFooterDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/application/umbdrawer/umb-drawer-footer.html' + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbDrawerFooter', DrawerFooterDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerheader.directive.js new file mode 100644 index 0000000000..78237a539e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerheader.directive.js @@ -0,0 +1,63 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbDrawerHeader +@restrict E +@scope + +@description +Use this directive to render a drawer header + +

Markup example

+
+	
+        
+        
+        
+
+        
+            
+            
{{ model | json }}
+
+ + + + + +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbDrawerView umbDrawerView}
  • +
  • {@link umbraco.directives.directive:umbDrawerContent umbDrawerContent}
  • +
  • {@link umbraco.directives.directive:umbDrawerFooter umbDrawerFooter}
  • +
+ +@param {string} title (attribute): Set a drawer title. +@param {string} description (attribute): Set a drawer description. +**/ + +(function() { + 'use strict'; + + function DrawerHeaderDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/application/umbdrawer/umb-drawer-header.html', + scope: { + "title": "@?", + "description": "@?" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbDrawerHeader', DrawerHeaderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerview.directive.js new file mode 100644 index 0000000000..54cfea0857 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbdrawer/umbdrawerview.directive.js @@ -0,0 +1,58 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbDrawerView +@restrict E +@scope + +@description +Use this directive to render drawer view + +

Markup example

+
+	
+        
+        
+        
+
+        
+            
+            
{{ model | json }}
+
+ + + + + +
+
+ +

Use in combination with

+
    +
  • {@link umbraco.directives.directive:umbDrawerHeader umbDrawerHeader}
  • +
  • {@link umbraco.directives.directive:umbDrawerContent umbDrawerContent}
  • +
  • {@link umbraco.directives.directive:umbDrawerFooter umbDrawerFooter}
  • +
+ +**/ + +(function() { + 'use strict'; + + function DrawerViewDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/application/umbdrawer/umb-drawer-view.html' + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbDrawerView', DrawerViewDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js index 436dcfcd0d..87e0cb62f4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbsections.directive.js @@ -3,7 +3,7 @@ * @name umbraco.directives.directive:umbSections * @restrict E **/ -function sectionsDirective($timeout, $window, navigationService, treeService, sectionService, appState, eventsService, $location) { +function sectionsDirective($timeout, $window, navigationService, treeService, sectionService, appState, eventsService, $location, historyService) { return { restrict: "E", // restrict to an element replace: true, // replace the html element with the template @@ -111,30 +111,13 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se scope.userDialog = null; } - scope.helpClick = function(){ - - if(scope.userDialog) { - closeUserDialog(); - } - - if(!scope.helpDialog) { - scope.helpDialog = { - view: "help", - show: true, - close: function(oldModel) { - closeHelpDialog(); - } - }; - } else { - closeHelpDialog(); - } - - }; - - function closeHelpDialog() { - scope.helpDialog.show = false; - scope.helpDialog = null; - } + //toggle the help dialog by raising the global app state to toggle the help drawer + scope.helpClick = function () { + var showDrawer = appState.getDrawerState("showDrawer"); + var drawer = { view: "help", show: !showDrawer }; + appState.setDrawerState("view", drawer.view); + appState.setDrawerState("showDrawer", drawer.show); + }; scope.sectionClick = function (event, section) { @@ -149,19 +132,19 @@ function sectionsDirective($timeout, $window, navigationService, treeService, se if (scope.userDialog) { closeUserDialog(); } - if (scope.helpDialog) { - closeHelpDialog(); - } + navigationService.hideSearch(); - navigationService.showTree(section.alias); + navigationService.showTree(section.alias); //in some cases the section will have a custom route path specified, if there is one we'll use it if (section.routePath) { $location.path(section.routePath); } else { - $location.path(section.alias).search(''); + var lastAccessed = historyService.getLastAccessedItemForSection(section.alias); + var path = lastAccessed != null ? lastAccessed.link : section.alias; + $location.path(path).search(''); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js new file mode 100644 index 0000000000..6dc066b282 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -0,0 +1,552 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbTour +@restrict E +@scope + +@description +Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. +In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. +You can easily add you own tours to the Help-drawer or show and start tours from +anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install The Starter Kit in Umbraco 7.8 + +

Extending the help drawer with custom tours

+The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. +Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be +picked up by Umbraco and shown in the Help-drawer. + +

The tour object

+The tour object consist of two parts - The overall tour configuration and a list of tour steps. We have split up the tour object for a better overview. +
+// The tour config object
+{
+    "name": "My Custom Tour", // (required)
+    "alias": "myCustomTour", // A unique tour alias (required)
+    "group": "My Custom Group" // Used to group tours in the help drawer
+    "groupOrder": 200 // Control the order of tour groups
+    "allowDisable": // Adds a "Don't" show this tour again"-button to the intro step
+    "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load.   
+    "steps": [] // tour steps - see next example
+}
+
+
+// A tour step object
+{
+    "title": "Title",
+    "content": "

Step content

", + "type": "intro" // makes the step an introduction step, + "element": "[data-element='my-table-row']", // the highlighted element + "event": "click" // forces the user to click the UI to go to next step + "eventElement": "[data-element='my-table-row'] [data-element='my-tour-button']" // specify an element to click inside a highlighted element + "elementPreventClick": false // prevents user interaction in the highlighted element + "backdropOpacity": 0.4 // the backdrop opacity + "view": "" // add a custom view + "customProperties" : {} // add any custom properties needed for the custom view +} +
+ +

Adding tours to other parts of the Umbraco backoffice

+It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, +as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service. + +

Using the tour service

+

Markup example - show custom tour

+
+    
+ +
{{vm.tour.name}}
+ + + + + +
+
+ +

Controller example - show custom tour

+
+    (function () {
+        "use strict";
+
+        function TourController(tourService) {
+
+            var vm = this;
+
+            vm.tour = {
+                "name": "My Custom Tour",
+                "alias": "myCustomTour",
+                "steps": [
+                    {
+                        "title": "Welcome to My Custom Tour",
+                        "content": "",
+                        "type": "intro"
+                    },
+                    {
+                        "element": "[data-element='my-tour-button']",
+                        "title": "Click the button",
+                        "content": "Click the button",
+                        "event": "click"
+                    }
+                ]
+            };
+
+            vm.startTour = startTour;
+
+            function startTour() {
+                tourService.startTour(vm.tour);
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.TourController", TourController);
+
+    })();
+
+ +

Custom step views

+In some cases you will need a custom view for one of your tour steps. +This could be for validation or for running any other custom logic for that step. +We have added a couple of helper components to make it easier to get the step scaffolding to look like a regular tour step. +In the following example you see how to run some custom logic before a step goes to the next step. + +

Markup example - custom step view

+
+    
+ + + + + + + + + + + + + + + + + +
+ + +
+ +
+ +
+ +
+
+ +

Controller example - custom step view

+
+    (function () {
+        "use strict";
+
+        function StepController() {
+
+            var vm = this;
+            
+            vm.initNextStep = initNextStep;
+
+            function initNextStep() {
+                // run logic here before going to the next step
+                $scope.model.nextStep();
+            }
+
+        }
+
+        angular.module("umbraco").controller("My.TourStep", StepController);
+
+    })();
+
+ + +

Related services

+
    +
  • {@link umbraco.services.tourService tourService}
  • +
+ +@param {string} model (binding): Tour object + +**/ + +(function () { + 'use strict'; + + function TourDirective($timeout, $http, $q, tourService, backdropService) { + + function link(scope, el, attr, ctrl) { + + var popover; + var pulseElement; + var pulseTimer; + + scope.loadingStep = false; + scope.elementNotFound = false; + + scope.model.nextStep = function() { + nextStep(); + }; + + scope.model.endTour = function() { + unbindEvent(); + tourService.endTour(scope.model); + backdropService.close(); + }; + + scope.model.completeTour = function() { + unbindEvent(); + tourService.completeTour(scope.model).then(function() { + backdropService.close(); + }); + }; + + scope.model.disableTour = function() { + unbindEvent(); + tourService.disableTour(scope.model).then(function() { + backdropService.close(); + }); + } + + function onInit() { + popover = el.find(".umb-tour__popover"); + pulseElement = el.find(".umb-tour__pulse"); + popover.hide(); + scope.model.currentStepIndex = 0; + backdropService.open({disableEventsOnClick: true}); + startStep(); + } + + function setView() { + if (scope.model.currentStep.view && scope.model.alias) { + //we do this to avoid a hidden dialog to start loading unconfigured views before the first activation + var configuredView = scope.model.currentStep.view; + if (scope.model.currentStep.view.indexOf(".html") === -1) { + var viewAlias = scope.model.currentStep.view.toLowerCase(); + var tourAlias = scope.model.alias.toLowerCase(); + configuredView = "views/common/tours/" + tourAlias + "/" + viewAlias + "/" + viewAlias + ".html"; + } + if (configuredView !== scope.configuredView) { + scope.configuredView = configuredView; + } + } else { + scope.configuredView = null; + } + } + + function nextStep() { + + popover.hide(); + pulseElement.hide(); + $timeout.cancel(pulseTimer); + scope.model.currentStepIndex++; + + // make sure we don't go too far + if(scope.model.currentStepIndex !== scope.model.steps.length) { + startStep(); + // tour completed - final step + } else { + scope.loadingStep = true; + + waitForPendingRerequests().then(function(){ + scope.loadingStep = false; + // clear current step + scope.model.currentStep = {}; + // set popover position to center + setPopoverPosition(null); + // remove backdrop hightlight and custom opacity + backdropService.setHighlight(null); + backdropService.setOpacity(null); + }); + } + } + + function startStep() { + scope.loadingStep = true; + backdropService.setOpacity(scope.model.steps[scope.model.currentStepIndex].backdropOpacity); + backdropService.setHighlight(null); + + waitForPendingRerequests().then(function() { + + scope.model.currentStep = scope.model.steps[scope.model.currentStepIndex]; + + setView(); + + // if highlight element is set - find it + findHighlightElement(); + + // if a custom event needs to be bound we do it now + if(scope.model.currentStep.event) { + bindEvent(); + } + + scope.loadingStep = false; + + }); + } + + function findHighlightElement() { + + scope.elementNotFound = false; + + $timeout(function () { + + // if an element isn't set - show the popover in the center + if(scope.model.currentStep && !scope.model.currentStep.element) { + setPopoverPosition(null); + return; + } + + var element = angular.element(scope.model.currentStep.element); + + // we couldn't find the element in the dom - abort and show error + if(element.length === 0) { + scope.elementNotFound = true; + setPopoverPosition(null); + return; + } + + var scrollParent = element.scrollParent(); + var scrollToCenterOfContainer = element[0].offsetTop - (scrollParent[0].clientHeight / 2 ) + (element[0].clientHeight / 2); + + // Detect if scroll is needed + if (element[0].offsetTop > scrollParent[0].clientHeight) { + scrollParent.animate({ + scrollTop: scrollToCenterOfContainer + }, function () { + // Animation complete. + setPopoverPosition(element); + setPulsePosition(); + backdropService.setHighlight(scope.model.currentStep.element, scope.model.currentStep.elementPreventClick); + }); + } else { + setPopoverPosition(element); + setPulsePosition(); + backdropService.setHighlight(scope.model.currentStep.element, scope.model.currentStep.elementPreventClick); + } + + }); + + } + + function setPopoverPosition(element) { + + $timeout(function () { + + var position = "center"; + var margin = 20; + var css = {}; + + var popoverWidth = popover.outerWidth(); + var popoverHeight = popover.outerHeight(); + var popoverOffset = popover.offset(); + var documentWidth = angular.element(document).width(); + var documentHeight = angular.element(document).height(); + + if(element) { + + var offset = element.offset(); + var width = element.outerWidth(); + var height = element.outerHeight(); + + // messure available space on each side of the target element + var space = { + "top": offset.top, + "right": documentWidth - (offset.left + width), + "bottom": documentHeight - (offset.top + height), + "left": offset.left + }; + + // get the posistion with most available space + position = findMax(space); + + if (position === "top") { + if (offset.left < documentWidth / 2) { + css.top = offset.top - popoverHeight - margin; + css.left = offset.left; + } else { + css.top = offset.top - popoverHeight - margin; + css.left = offset.left - popoverWidth + width; + } + } + + if (position === "right") { + if (offset.top < documentHeight / 2) { + css.top = offset.top; + css.left = offset.left + width + margin; + } else { + css.top = offset.top + height - popoverHeight; + css.left = offset.left + width + margin; + } + } + + if (position === "bottom") { + if (offset.left < documentWidth / 2) { + css.top = offset.top + height + margin; + css.left = offset.left; + } else { + css.top = offset.top + height + margin; + css.left = offset.left - popoverWidth + width; + } + } + + if (position === "left") { + if (offset.top < documentHeight / 2) { + css.top = offset.top; + css.left = offset.left - popoverWidth - margin; + } else { + css.top = offset.top + height - popoverHeight; + css.left = offset.left - popoverWidth - margin; + } + } + + } else { + // if there is no dom element center the popover + css.top = "calc(50% - " + popoverHeight/2 + "px)"; + css.left = "calc(50% - " + popoverWidth/2 + "px)"; + } + + popover.css(css).fadeIn("fast"); + + }); + + + } + + function setPulsePosition() { + if(scope.model.currentStep.event) { + + pulseTimer = $timeout(function(){ + + var clickElementSelector = scope.model.currentStep.eventElement ? scope.model.currentStep.eventElement : scope.model.currentStep.element; + var clickElement = $(clickElementSelector); + + var offset = clickElement.offset(); + var width = clickElement.outerWidth(); + var height = clickElement.outerHeight(); + + pulseElement.css({ "width": width, "height": height, "left": offset.left, "top": offset.top }); + pulseElement.fadeIn(); + + }, 1000); + } + } + + function waitForPendingRerequests() { + var deferred = $q.defer(); + var timer = window.setInterval(function(){ + // check for pending requests both in angular and on the document + if($http.pendingRequests.length === 0 && document.readyState === "complete") { + $timeout(function(){ + deferred.resolve(); + clearInterval(timer); + }); + } + }, 50); + return deferred.promise; + } + + function findMax(obj) { + var keys = Object.keys(obj); + var max = keys[0]; + for (var i = 1, n = keys.length; i < n; ++i) { + var k = keys[i]; + if (obj[k] > obj[max]) { + max = k; + } + } + return max; + } + + function bindEvent() { + + var bindToElement = scope.model.currentStep.element; + var eventName = scope.model.currentStep.event + ".step-" + scope.model.currentStepIndex; + var removeEventName = "remove.step-" + scope.model.currentStepIndex; + var handled = false; + + if(scope.model.currentStep.eventElement) { + bindToElement = scope.model.currentStep.eventElement; + } + + $(bindToElement).on(eventName, function(){ + if(!handled) { + unbindEvent(); + nextStep(); + handled = true; + } + }); + + // Hack: we do this to handle cases where ng-if is used and removes the element we need to click. + // for some reason it seems the elements gets removed before the event is raised. This is a temp solution which assumes: + // "if you ask me to click on an element, and it suddenly gets removed from the dom, let's go on to the next step". + $(bindToElement).on(removeEventName, function () { + if(!handled) { + unbindEvent(); + nextStep(); + handled = true; + } + }); + + } + + function unbindEvent() { + var eventName = scope.model.currentStep.event + ".step-" + scope.model.currentStepIndex; + var removeEventName = "remove.step-" + scope.model.currentStepIndex; + + if(scope.model.currentStep.eventElement) { + angular.element(scope.model.currentStep.eventElement).off(eventName); + angular.element(scope.model.currentStep.eventElement).off(removeEventName); + } else { + angular.element(scope.model.currentStep.element).off(eventName); + angular.element(scope.model.currentStep.element).off(removeEventName); + } + } + + function resize() { + findHighlightElement(); + } + + onInit(); + + $(window).on('resize.umbTour', resize); + + scope.$on('$destroy', function () { + $(window).off('resize.umbTour'); + unbindEvent(); + $timeout.cancel(pulseTimer); + }); + + } + + var directive = { + transclude: true, + restrict: 'E', + replace: true, + templateUrl: 'views/components/application/umb-tour.html', + link: link, + scope: { + model: "=" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbTour', TourDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js new file mode 100644 index 0000000000..08e0a44c0b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstep.directive.js @@ -0,0 +1,35 @@ +(function() { + 'use strict'; + + function TourStepDirective() { + + function link(scope, element, attrs, ctrl) { + + scope.close = function() { + if(scope.onClose) { + scope.onClose(); + } + } + + } + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/application/umbtour/umb-tour-step.html', + scope: { + size: "@?", + onClose: "&?", + hideClose: "=?" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbTourStep', TourStepDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js new file mode 100644 index 0000000000..52ed358b61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcontent.directive.js @@ -0,0 +1,22 @@ +(function() { + 'use strict'; + + function TourStepContentDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/application/umbtour/umb-tour-step-content.html', + scope: { + content: "=" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbTourStepContent', TourStepContentDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js new file mode 100644 index 0000000000..7e04ef5d00 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepcounter.directive.js @@ -0,0 +1,22 @@ +(function() { + 'use strict'; + + function TourStepCounterDirective() { + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/application/umbtour/umb-tour-step-counter.html', + scope: { + currentStep: "=", + totalSteps: "=" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbTourStepCounter', TourStepCounterDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js new file mode 100644 index 0000000000..fedb527972 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepfooter.directive.js @@ -0,0 +1,19 @@ +(function() { + 'use strict'; + + function TourStepFooterDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/application/umbtour/umb-tour-step-footer.html' + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbTourStepFooter', TourStepFooterDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js new file mode 100644 index 0000000000..9d32ad87a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour/umbtourstepheader.directive.js @@ -0,0 +1,22 @@ +(function() { + 'use strict'; + + function TourStepHeaderDirective() { + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'views/components/application/umbtour/umb-tour-step-header.html', + scope: { + title: "=" + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbTourStepHeader', TourStepHeaderDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js index b503648317..4b0bdb6c71 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -149,7 +149,8 @@ Use this directive to render an umbraco button. The directive can be used to gen labelKey: "@?", icon: "@?", disabled: "=", - size: "@?" + size: "@?", + alias: "@?" } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 7d60a83b89..1ecb2d7403 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -1,7 +1,9 @@ (function () { 'use strict'; - function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { + function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, $location, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http, eventsService, relationResource) { + + var evts = []; //setup scope vars $scope.defaultButton = null; @@ -14,22 +16,13 @@ $scope.page.menu.currentSection = appState.getSectionState("currentSection"); $scope.page.listViewPath = null; $scope.page.isNew = $scope.isNew ? true : false; - $scope.page.buttonGroupState = "init"; + $scope.page.buttonGroupState = "init"; + $scope.allowOpen = true; + function init(content) { - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: $scope.page.isNew, - content: content, - methods: { - saveAndPublish: $scope.saveAndPublish, - sendToPublish: $scope.sendToPublish, - save: $scope.save, - unPublish: $scope.unPublish - } - }); - $scope.defaultButton = buttons.defaultButton; - $scope.subButtons = buttons.subButtons; + createButtons(content); editorState.set($scope.content); @@ -42,6 +35,70 @@ }); } } + + evts.push(eventsService.on("editors.content.changePublishDate", function (event, args) { + createButtons(args.node); + })); + + evts.push(eventsService.on("editors.content.changeUnpublishDate", function (event, args) { + createButtons(args.node); + })); + + // We don't get the info tab from the server from version 7.8 so we need to manually add it + contentEditingHelper.addInfoTab($scope.content.tabs); + + } + + function getNode() { + + $scope.page.loading = true; + + //we are editing so get the content item from the server + $scope.getMethod()($scope.contentId) + .then(function (data) { + + $scope.content = data; + + if (data.isChildOfListView && data.trashed === false) { + $scope.page.listViewPath = ($routeParams.page) ? + "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : + "/content/content/edit/" + data.parentId; + } + + init($scope.content); + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + + syncTreeNode($scope.content, data.path, true); + + resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + + }); + + } + + function createButtons(content) { + $scope.page.buttonGroupState = "init"; + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $scope.page.isNew, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: $scope.unPublish + } + }); + + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; + } /** Syncs the content item to it's tree node - this occurs on first load and after saving */ @@ -130,35 +187,8 @@ } else { - $scope.page.loading = true; + getNode(); - //we are editing so get the content item from the server - $scope.getMethod()($scope.contentId) - .then(function (data) { - - $scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - $scope.page.listViewPath = ($routeParams.page) ? - "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : - "/content/content/edit/" + data.parentId; - } - - init($scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - syncTreeNode($scope.content, data.path, true); - - resetLastListPageNumber($scope.content); - - $scope.page.loading = false; - - }); } @@ -185,6 +215,8 @@ $scope.page.buttonGroupState = "success"; + }, function(err) { + $scope.page.buttonGroupState = 'error'; }); } @@ -208,9 +240,9 @@ if (!$scope.busy) { // Chromes popup blocker will kick in if a window is opened - // outwith the initial scoped request. This trick will fix that. + // without the initial scoped request. This trick will fix that. // - var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); + var previewWindow = $window.open('preview/?init=true&id=' + content.id, 'umbpreview'); // Build the correct path so both /#/ and #/ work. var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + content.id; @@ -230,6 +262,87 @@ }; + $scope.restore = function (content) { + + $scope.page.buttonRestore = "busy"; + + relationResource.getByChildId(content.id, "relateParentDocumentOnDelete").then(function (data) { + + var relation = null; + var target = null; + var error = { headline: "Cannot automatically restore this item", content: "Use the Move menu item to move it manually"}; + + if (data.length == 0) { + notificationsService.error(error.headline, "There is no 'restore' relation found for this node. Use the Move menu item to move it manually."); + $scope.page.buttonRestore = "error"; + return; + } + + relation = data[0]; + + if (relation.parentId == -1) { + target = { id: -1, name: "Root" }; + moveNode(content, target); + } else { + contentResource.getById(relation.parentId).then(function (data) { + target = data; + + // make sure the target item isn't in the recycle bin + if(target.path.indexOf("-20") !== -1) { + notificationsService.error(error.headline, "The item you want to restore it under (" + target.name + ") is in the recycle bin. Use the Move menu item to move the item manually."); + $scope.page.buttonRestore = "error"; + return; + } + + moveNode(content, target); + + }, function (err) { + $scope.page.buttonRestore = "error"; + notificationsService.error(error.headline, error.content); + }); + } + + }, function (err) { + $scope.page.buttonRestore = "error"; + notificationsService.error(error.headline, error.content); + }); + + + }; + + function moveNode(node, target) { + + contentResource.move({ "parentId": target.id, "id": node.id }) + .then(function (path) { + + // remove the node that we're working on + if($scope.page.menu.currentNode) { + treeService.removeNode($scope.page.menu.currentNode); + } + + // sync the destination node + navigationService.syncTree({ tree: "content", path: path, forceReload: true, activate: false }); + + $scope.page.buttonRestore = "success"; + notificationsService.success("Successfully restored " + node.name + " to " + target.name); + + // reload the node + getNode(); + + }, function (err) { + $scope.page.buttonRestore = "error"; + notificationsService.error("Cannot automatically restore this item", err); + }); + + } + + //ensure to unregister from all events! + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + } function createDirective() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js new file mode 100644 index 0000000000..2ce0c5a22e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -0,0 +1,293 @@ +(function () { + 'use strict'; + + function ContentNodeInfoDirective($timeout, $location, logResource, eventsService, userService, localizationService, dateHelper) { + + function link(scope, element, attrs, ctrl) { + + var evts = []; + var isInfoTab = false; + scope.publishStatus = {}; + + function onInit() { + + scope.allowOpen = true; + + scope.datePickerConfig = { + pickDate: true, + pickTime: true, + useSeconds: false, + format: "YYYY-MM-DD HH:mm", + icons: { + time: "icon-time", + date: "icon-calendar", + up: "icon-chevron-up", + down: "icon-chevron-down" + } + }; + + scope.auditTrailOptions = { + "id": scope.node.id + }; + + // get available templates + scope.availableTemplates = scope.node.allowedTemplates; + + // get document type details + scope.documentType = scope.node.documentType; + + // make sure dates are formatted to the user's locale + formatDatesToLocal(); + + setNodePublishStatus(scope.node); + + } + + scope.auditTrailPageChange = function (pageNumber) { + scope.auditTrailOptions.pageNumber = pageNumber; + loadAuditTrail(); + }; + + scope.openDocumentType = function (documentType) { + var url = "/settings/documenttypes/edit/" + documentType.id; + $location.url(url); + }; + + scope.updateTemplate = function (templateAlias) { + + // update template value + scope.node.template = templateAlias; + + }; + + scope.datePickerChange = function (event, type) { + if (type === 'publish') { + setPublishDate(event.date.format("YYYY-MM-DD HH:mm")); + } else if (type === 'unpublish') { + setUnpublishDate(event.date.format("YYYY-MM-DD HH:mm")); + } + }; + + scope.clearPublishDate = function () { + clearPublishDate(); + }; + + scope.clearUnpublishDate = function () { + clearUnpublishDate(); + }; + + function loadAuditTrail() { + + scope.loadingAuditTrail = true; + + logResource.getPagedEntityLog(scope.auditTrailOptions) + .then(function (data) { + + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + angular.forEach(data.items, function(item) { + item.timestampFormatted = dateHelper.getLocalDate(item.timestamp, currentUser.locale, 'LLL'); + }); + }); + + scope.auditTrail = data.items; + scope.auditTrailOptions.pageNumber = data.pageNumber; + scope.auditTrailOptions.pageSize = data.pageSize; + scope.auditTrailOptions.totalItems = data.totalItems; + scope.auditTrailOptions.totalPages = data.totalPages; + + setAuditTrailLogTypeColor(scope.auditTrail); + + scope.loadingAuditTrail = false; + }); + + } + + function setAuditTrailLogTypeColor(auditTrail) { + angular.forEach(auditTrail, function (item) { + switch (item.logType) { + case "Publish": + item.logTypeColor = "success"; + break; + case "UnPublish": + case "Delete": + item.logTypeColor = "danger"; + break; + default: + item.logTypeColor = "gray"; + } + }); + } + + function setNodePublishStatus(node) { + + // deleted node + if(node.trashed === true) { + scope.publishStatus.label = localizationService.localize("general_deleted"); + scope.publishStatus.color = "danger"; + } + + // unpublished node + if(node.published === false && node.trashed === false) { + scope.publishStatus.label = localizationService.localize("content_unpublished"); + scope.publishStatus.color = "gray"; + } + + // published node + if(node.hasPublishedVersion === true && node.publishDate && node.published === true) { + scope.publishStatus.label = localizationService.localize("content_published"); + scope.publishStatus.color = "success"; + } + + // published node with pending changes + if(node.hasPublishedVersion === true && node.publishDate && node.published === false) { + scope.publishStatus.label = localizationService.localize("content_publishedPendingChanges"); + scope.publishStatus.color = "success" + } + + } + + function setPublishDate(date) { + + if (!date) { + return; + } + + //The date being passed in here is the user's local date/time that they have selected + //we need to convert this date back to the server date on the model. + + var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset); + + // update publish value + scope.node.releaseDate = serverTime; + + // make sure dates are formatted to the user's locale + formatDatesToLocal(); + + // emit event + var args = { node: scope.node, date: date }; + eventsService.emit("editors.content.changePublishDate", args); + + } + + function clearPublishDate() { + + // update publish value + scope.node.releaseDate = null; + + // emit event + var args = { node: scope.node, date: null }; + eventsService.emit("editors.content.changePublishDate", args); + + } + + function setUnpublishDate(date) { + + if (!date) { + return; + } + + //The date being passed in here is the user's local date/time that they have selected + //we need to convert this date back to the server date on the model. + + var serverTime = dateHelper.convertToServerStringTime(moment(date), Umbraco.Sys.ServerVariables.application.serverTimeOffset); + + // update publish value + scope.node.removeDate = serverTime; + + // make sure dates are formatted to the user's locale + formatDatesToLocal(); + + // emit event + var args = { node: scope.node, date: date }; + eventsService.emit("editors.content.changeUnpublishDate", args); + + } + + function clearUnpublishDate() { + + // update publish value + scope.node.removeDate = null; + + // emit event + var args = { node: scope.node, date: null }; + eventsService.emit("editors.content.changeUnpublishDate", args); + + } + + function ucfirst(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + + function formatDatesToLocal() { + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + scope.node.createDateFormatted = dateHelper.getLocalDate(scope.node.createDate, currentUser.locale, 'LLL'); + + scope.node.releaseDateYear = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'YYYY')) : null; + scope.node.releaseDateMonth = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'MMMM')) : null; + scope.node.releaseDateDayNumber = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'DD')) : null; + scope.node.releaseDateDay = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'dddd')) : null; + scope.node.releaseDateTime = scope.node.releaseDate ? ucfirst(dateHelper.getLocalDate(scope.node.releaseDate, currentUser.locale, 'HH:mm')) : null; + + scope.node.removeDateYear = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'YYYY')) : null; + scope.node.removeDateMonth = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'MMMM')) : null; + scope.node.removeDateDayNumber = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'DD')) : null; + scope.node.removeDateDay = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'dddd')) : null; + scope.node.removeDateTime = scope.node.removeDate ? ucfirst(dateHelper.getLocalDate(scope.node.removeDate, currentUser.locale, 'HH:mm')) : null; + }); + } + + // load audit trail when on the info tab + evts.push(eventsService.on("app.tabChange", function (event, args) { + $timeout(function(){ + if (args.id === -1) { + isInfoTab = true; + loadAuditTrail(); + } else { + isInfoTab = false; + } + }); + })); + + // watch for content updates - reload content when node is saved, published etc. + scope.$watch('node.updateDate', function(newValue, oldValue){ + + if(!newValue) { return; } + if(newValue === oldValue) { return; } + + if(isInfoTab) { + loadAuditTrail(); + formatDatesToLocal(); + setNodePublishStatus(scope.node); + } + }); + + //ensure to unregister from all events! + scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + + onInit(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/content/umb-content-node-info.html', + scope: { + node: "=" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbContentNodeInfo', ContentNodeInfoDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index de497ccffe..6f4111373d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -156,7 +156,7 @@ angular.module('umbraco.directives') if(els.indexOf(el) >= 0){return;} // ignore clicks on new overlay - var parents = $(event.target).parents("a,button,.umb-overlay"); + var parents = $(event.target).parents("a,button,.umb-overlay,.umb-tour"); if(parents.length > 0){ return; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index e3a50b482c..90daac73db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -115,7 +115,9 @@ angular.module("umbraco.directives") toolbar: toolbar, content_css: stylesheets, style_formats: styleFormats, - autoresize_bottom_margin: 0 + autoresize_bottom_margin: 0, + //see http://archive.tinymce.com/wiki.php/Configuration:cache_suffix + cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster }; @@ -366,6 +368,9 @@ angular.module("umbraco.directives") // element might still be there even after the modal has been hidden. scope.$on('$destroy', function () { unsubscribe(); + if (tinyMceEditor !== undefined && tinyMceEditor != null) { + tinyMceEditor.destroy() + } }); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js new file mode 100644 index 0000000000..1aa026cb3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/media/umbmedianodeinfo.directive.js @@ -0,0 +1,67 @@ +(function () { + 'use strict'; + + function MediaNodeInfoDirective($timeout, $location, eventsService, userService, dateHelper) { + + function link(scope, element, attrs, ctrl) { + + var evts = []; + + function onInit() { + scope.allowOpenMediaType = true; + // get document type details + scope.mediaType = scope.node.contentType; + // get node url + scope.nodeUrl = scope.node.mediaLink; + // make sure dates are formatted to the user's locale + formatDatesToLocal(); + } + + function formatDatesToLocal() { + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + scope.node.createDateFormatted = dateHelper.getLocalDate(scope.node.createDate, currentUser.locale, 'LLL'); + scope.node.updateDateFormatted = dateHelper.getLocalDate(scope.node.updateDate, currentUser.locale, 'LLL'); + }); + } + + scope.openMediaType = function (mediaType) { + // remove first "#" from url if it is prefixed else the path won't work + var url = "/settings/mediaTypes/edit/" + mediaType.id; + $location.path(url); + }; + + // watch for content updates - reload content when node is saved, published etc. + scope.$watch('node.updateDate', function(newValue, oldValue){ + if(!newValue) { return; } + if(newValue === oldValue) { return; } + formatDatesToLocal(); + }); + + //ensure to unregister from all events! + scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + + onInit(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/media/umb-media-node-info.html', + scope: { + node: "=" + }, + link: link + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbMediaNodeInfo', MediaNodeInfoDirective); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index d728865015..6e705da52d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -59,7 +59,6 @@

General Options

-Lorem ipsum dolor sit amet.. @@ -74,7 +73,7 @@ Lorem ipsum dolor sit amet.. - + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index adbc3a96a7..69457a6f10 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -4,29 +4,32 @@ * @restrict E **/ angular.module("umbraco.directives") - .directive('umbProperty', function (umbPropEditorHelper) { + .directive('umbProperty', function (umbPropEditorHelper, userService) { return { scope: { property: "=" }, transclude: true, restrict: 'E', - replace: true, + replace: true, templateUrl: 'views/components/property/umb-property.html', - link: function(scope) { - scope.propertyAlias = Umbraco.Sys.ServerVariables.isDebuggingEnabled === true ? scope.property.alias : null; + link: function (scope) { + userService.getCurrentUser().then(function (u) { + var isAdmin = u.userGroups.indexOf('admin') !== -1; + scope.propertyAlias = (Umbraco.Sys.ServerVariables.isDebuggingEnabled === true || isAdmin) ? scope.property.alias : null; + }); }, //Define a controller for this directive to expose APIs to other directives controller: function ($scope, $timeout) { - + var self = this; //set the API properties/methods - + self.property = $scope.property; - self.setPropertyError = function(errorMsg) { + self.setPropertyError = function (errorMsg) { $scope.property.propertyErrorMessage = errorMsg; }; } }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js index aa23b80665..494c4a3f7d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabs.directive.js @@ -8,7 +8,7 @@ angular.module("umbraco.directives") .directive('umbTabs', function () { return { restrict: 'A', - controller: function ($scope, $element, $attrs) { + controller: function ($scope, $element, $attrs, eventsService) { var callbacks = []; this.onTabShown = function(cb) { @@ -19,14 +19,21 @@ angular.module("umbraco.directives") var curr = $(event.target); // active tab var prev = $(event.relatedTarget); // previous tab + + // emit tab change event + var tabId = Number(curr.context.hash.replace("#tab", "")); + var args = { id: tabId, hash: curr.context.hash }; + + eventsService.emit("app.tabChange", args); $scope.$apply(); for (var c in callbacks) { callbacks[c].apply(this, [{current: curr, previous: prev}]); } + } - + //NOTE: it MUST be done this way - binding to an ancestor element that exists // in the DOM to bind to the dynamic elements that will be created. // It would be nicer to create this event handler as a directive for which child diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 6ed1a21306..cce4e0d792 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -31,10 +31,10 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat //var showheader = (attrs.showheader !== 'false'); var hideoptions = (attrs.hideoptions === 'true') ? "hide-options" : ""; var template = '
  • '; - template += '
    ' + + template += '
    ' + '
    ' + ' {{tree.name}}
    ' + - '' + + '' + '
    '; template += '
      ' + '' + @@ -148,13 +148,13 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat userService.getCurrentUser().then(function (userData) { - var startNodes = []; + var startNodes = []; for (var i = 0; i < userData.startContentIds; i++) { startNodes.push(userData.startContentIds[i]); } for (var j = 0; j < userData.startMediaIds; j++) { startNodes.push(userData.startMediaIds[j]); - } + } _.each(startNodes, function (i) { var found = _.find(args.path, function (p) { @@ -317,17 +317,17 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat } //TODO: This is called constantly because as a method in a template it's re-evaluated pretty much all the time - // it would be better if we could cache the processing. The problem is that some of these things are dynamic. + // it would be better if we could cache the processing. The problem is that some of these things are dynamic. var css = []; if (node.cssClasses) { _.each(node.cssClasses, function (c) { css.push(c); }); - } + } return css.join(" "); - }; + }; scope.selectEnabledNodeClass = function (node) { return node ? @@ -406,7 +406,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat if (n.metaData && n.metaData.noAccess === true) { ev.preventDefault(); return; - } + } //on tree select we need to remove the current node - // whoever handles this will need to make sure the correct node is selected diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index 25c1becc87..0bb888a59f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -35,15 +35,15 @@ angular.module("umbraco.directives") //TODO: Remove more of the binding from this template and move the DOM manipulation to be manually done in the link function, // this will greatly improve performance since there's potentially a lot of nodes being rendered = a LOT of watches! - template: '
    • ' + - '
      ' + + template: '
    • ' + + '
      ' + //NOTE: This ins element is used to display the search icon if the node is a container/listview and the tree is currently in dialog //'' + - ' ' + + ' ' + '' + '' + //NOTE: These are the 'option' elipses - '' + + '' + '
      ' + '
      ' + '
    • ', @@ -96,6 +96,14 @@ angular.module("umbraco.directives") if (node.style) { element.find("i:first").attr("style", node.style); } + + // add a unique data element to each tree item so it is easy to navigate with code + if(!node.metaData.treeAlias) { + node.dataElement = node.name; + } else { + node.dataElement = node.metaData.treeAlias; + } + } //This will deleteAnimations to true after the current digest diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js index 7fad3e8a74..7dd2f0d7a3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbavatar.directive.js @@ -66,9 +66,13 @@ Use this directive to render an avatar. function getNameInitials(name) { if(name) { - var initials = name.match(/\b\w/g) || []; - initials = ((initials.shift() || '') + (initials.pop() || '')).toUpperCase(); - return initials; + var names = name.split(' '), + initials = names[0].substring(0, 1); + + if (names.length > 1) { + initials += names[names.length - 1].substring(0, 1); + } + return initials.toUpperCase(); } return null; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js index d7e44df113..7047fdb0a2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbdatetimepicker.directive.js @@ -84,7 +84,13 @@ Use this directive to render a date time picker function link(scope, element, attrs, ctrl) { + scope.hasTranscludedContent = false; + function onInit() { + + // check for transcluded content so we can hide the defualt markup + scope.hasTranscludedContent = element.find('.js-datePicker__transcluded-content')[0].children.length > 0; + // load css file for the date picker assetsService.loadCss('lib/datetimepicker/bootstrap-datetimepicker.min.css'); @@ -148,7 +154,7 @@ Use this directive to render a date time picker .on("dp.show", onShow) .on("dp.change", onChange) .on("dp.error", onError) - .on("dp.update", onUpdate); + .on("dp.update", onUpdate); } onInit(); @@ -158,6 +164,7 @@ Use this directive to render a date time picker var directive = { restrict: 'E', replace: true, + transclude: true, templateUrl: 'views/components/umb-date-time-picker.html', scope: { options: "=", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index caa79439be..c0be05addf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -489,7 +489,7 @@ scope.editPropertyTypeSettings = function(property, group) { - if (!property.inherited && !property.locked) { + if (!property.inherited) { scope.propertySettingsDialogModel = {}; scope.propertySettingsDialogModel.title = "Property settings"; @@ -547,6 +547,7 @@ property.validation.pattern = oldModel.property.validation.pattern; property.showOnMemberProfile = oldModel.property.showOnMemberProfile; property.memberCanEdit = oldModel.property.memberCanEdit; + property.isSensitiveValue = oldModel.property.isSensitiveValue; // because we set state to active, to show a preview, we have to check if has been filled out // label is required so if it is not filled we know it is a placeholder diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js index c0bd7a4eff..12179076cd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbmediagrid.directive.js @@ -110,7 +110,7 @@ Use this directive to generate a thumbnail grid of media items. itemMinWidth = scope.itemMinWidth; } - if (scope.itemMinWidth) { + if (scope.itemMinHeight) { itemMinHeight = scope.itemMinHeight; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js index d98246ac42..556019857b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnodepreview.directive.js @@ -77,7 +77,7 @@ @param {string} icon (binding): The node icon. @param {string} name (binding): The node name. -@param {boolean} published (binding): The node pusblished state. +@param {boolean} published (binding): The node published state. @param {string} description (binding): A short description. @param {boolean} sortable (binding): Will add a move cursor on the node preview. Can used in combination with ui-sortable. @param {boolean} allowRemove (binding): Show/Hide the remove button. diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js new file mode 100644 index 0000000000..aac1b8dac1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpasswordtoggle.directive.js @@ -0,0 +1,37 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbPasswordToggle +@restrict E +@scope + +@description +Added in Umbraco v. 7.7.4: Use this directive to render a password toggle. + +**/ + +(function () { + 'use strict'; + + // comes from https://codepen.io/jakob-e/pen/eNBQaP + // works fine with Angular 1.6.5 - alas not with 1.1.5 - binding issue + + function PasswordToggleDirective($compile) { + + var directive = { + restrict: 'A', + scope: {}, + link: function(scope, elem, attrs) { + scope.tgl = function () { elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); } + var lnk = angular.element("Toggle"); + $compile(lnk)(scope); + elem.wrap("
      ").after(lnk); + } + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbPasswordToggle', PasswordToggleDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbprogresscircle.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbprogresscircle.directive.js new file mode 100644 index 0000000000..ad79cb2e3b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbprogresscircle.directive.js @@ -0,0 +1,88 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbProgressCircle +@restrict E +@scope + +@description +Use this directive to render a circular progressbar. + +

      Markup example

      +
      +    
      + + + + +
      +
      + +@param {string} size (attribute): This parameter defines the width and the height of the circle in pixels. +@param {string} percentage (attribute): Takes a number between 0 and 100 and applies it to the circle's highlight length. +@param {string} color (attribute): the color of the highlight (primary, secondary, success, warning, danger). Success by default. +**/ + +(function (){ + 'use strict'; + + function ProgressCircleDirective($http, $timeout) { + + function link(scope, element, $filter) { + + function onInit() { + + // making sure we get the right numbers + var percent = scope.percentage; + + if (percent > 100) { + percent = 100; + } + else if (percent < 0) { + percent = 0; + } + + // calculating the circle's highlight + var circle = element.find(".umb-progress-circle__highlight"); + var r = circle.attr('r'); + var strokeDashArray = (r*Math.PI)*2; + + // Full circle length + scope.strokeDashArray = strokeDashArray; + + var strokeDashOffsetDifference = (percent/100)*strokeDashArray; + var strokeDashOffset = strokeDashArray - strokeDashOffsetDifference; + + // Distance for the highlight dash's offset + scope.strokeDashOffset = strokeDashOffset; + + // set font size + scope.percentageSize = (scope.size * 0.3) + "px"; + + } + + onInit(); + } + + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-progress-circle.html', + scope: { + size: "@?", + percentage: "@", + color: "@" + }, + link: link + + }; + + return directive; + } + + angular.module('umbraco.directives').directive('umbProgressCircle', ProgressCircleDirective); + +})(); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js index 17d4dd93ff..c45a9f78e5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtable.directive.js @@ -1,3 +1,114 @@ +/** +@ngdoc directive +@name umbraco.directives.directive:umbTable +@restrict E +@scope + +@description +Added in Umbraco v. 7.4: Use this directive to render a data table. + +

      Markup example

      +
      +    
      + + + + +
      +
      + +

      Controller example

      +
      +    (function () {
      +        "use strict";
      +    
      +        function Controller() {
      +    
      +            var vm = this;
      +    
      +            vm.items = [
      +                {
      +                    "icon": "icon-document",
      +                    "name": "My node 1",
      +                    "published": true,
      +                    "description": "A short description of my node",
      +                    "author": "Author 1"
      +                },
      +                {
      +                    "icon": "icon-document",
      +                    "name": "My node 2",
      +                    "published": true,
      +                    "description": "A short description of my node",
      +                    "author": "Author 2"
      +                }
      +            ];
      +
      +            vm.options = {
      +                includeProperties: [
      +                    { alias: "description", header: "Description" },
      +                    { alias: "author", header: "Author" }
      +                ]
      +            };
      +    
      +            vm.selectItem = selectItem;
      +            vm.clickItem = clickItem;
      +            vm.selectAll = selectAll;
      +            vm.isSelectedAll = isSelectedAll;
      +            vm.isSortDirection = isSortDirection;
      +            vm.sort = sort;
      +
      +            function selectAll($event) {
      +                alert("select all");
      +            }
      +
      +            function isSelectedAll() {
      +                
      +            }
      +    
      +            function clickItem(item) {
      +                alert("click node");
      +            }
      +
      +            function selectItem(selectedItem, $index, $event) {
      +                alert("select node");
      +            }
      +            
      +            function isSortDirection(col, direction) {
      +                
      +            }
      +            
      +            function sort(field, allow, isSystem) {
      +                
      +            }
      +    
      +        }
      +    
      +        angular.module("umbraco").controller("My.TableController", Controller);
      +    
      +    })();
      +
      + +@param {string} icon (binding): The node icon. +@param {string} name (binding): The node name. +@param {string} published (binding): The node published state. +@param {function} onSelect (expression): Callback function when the row is selected. +@param {function} onClick (expression): Callback function when the "Name" column link is clicked. +@param {function} onSelectAll (expression): Callback function when selecting all items. +@param {function} onSelectedAll (expression): Callback function when all items are selected. +@param {function} onSortingDirection (expression): Callback function when sorting direction is changed. +@param {function} onSort (expression): Callback function when sorting items. +**/ + (function () { 'use strict'; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js index f027d7a12f..104736530f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js @@ -9,9 +9,14 @@ function noDirtyCheck() { restrict: 'A', require: 'ngModel', link: function (scope, elm, attrs, ctrl) { - elm.focus(function () { - ctrl.$pristine = false; - }); + + var alwaysFalse = { + get: function () { return false; }, + set: function () { } + }; + Object.defineProperty(ctrl, '$pristine', alwaysFalse); + Object.defineProperty(ctrl, '$dirty', alwaysFalse); + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js index 8574d01f5a..273ab07593 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js @@ -47,8 +47,8 @@ function valEmail(valEmailExpression) { angular.module('umbraco.directives.validation') .directive("valEmail", valEmail) .factory('valEmailExpression', function () { - //NOTE: This is the fixed regex which is part of the newer angular + var emailRegex = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; return { - EMAIL_REGEXP: /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i + EMAIL_REGEXP: emailRegex }; }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js index 233d510c3e..0a6b3cd6bc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/currentuser.resource.js @@ -10,6 +10,30 @@ function currentUserResource($q, $http, umbRequestHelper, umbDataFormatter) { //the factory object returned return { + saveTourStatus: function (tourStatus) { + + if (!tourStatus) { + return angularHelper.rejectedPromise({ errorMsg: 'tourStatus cannot be empty' }); + } + + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "PostSetUserTour"), + tourStatus), + 'Failed to save tour status'); + }, + + getTours: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "currentUserApiBaseUrl", + "GetUserTours")), 'Failed to get tours'); + }, + performSetInvitedUserPassword: function (newPassword) { if (!newPassword) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index f5e2748360..5dd353d9e0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -41,7 +41,7 @@ function entityResource($q, $http, umbRequestHelper) { if (!value) { return ""; } - + value = value.replace("#", ""); return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js index a5e8149952..cb676511a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/log.resource.js @@ -9,7 +9,73 @@ function logResource($q, $http, umbRequestHelper) { //the factory object returned return { - + + getPagedEntityLog: function (options) { + + var defaults = { + pageSize: 10, + pageNumber: 1, + orderDirection: "Descending" + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + if (options.id === undefined || options.id === null) { + throw "options.id is required"; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "logApiBaseUrl", + "GetPagedEntityLog", + options)), + 'Failed to retrieve log data for id'); + }, + + getPagedUserLog: function (options) { + + var defaults = { + pageSize: 10, + pageNumber: 1, + orderDirection: "Descending" + }; + if (options === undefined) { + options = {}; + } + //overwrite the defaults if there are any specified + angular.extend(defaults, options); + //now copy back to the options we will use + options = defaults; + //change asc/desct + if (options.orderDirection === "asc") { + options.orderDirection = "Ascending"; + } + else if (options.orderDirection === "desc") { + options.orderDirection = "Descending"; + } + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "logApiBaseUrl", + "GetPagedEntityLog", + options)), + 'Failed to retrieve log data for id'); + }, + /** * @ngdoc method * @name umbraco.resources.logResource#getEntityLog diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js index d1810f56ee..93a92db1ec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/media.resource.js @@ -493,7 +493,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) { * @methodOf umbraco.resources.mediaResource * * @description - * Empties the media recycle bin + * Paginated search for media items starting on the supplied nodeId * * ##usage *
      @@ -506,7 +506,7 @@ function mediaResource($q, $http, umbDataFormatter, umbRequestHelper) {
                 * @param {string} query The search query
                 * @param {int} pageNumber The page number
                 * @param {int} pageSize The number of media items on a page
      -          * @param {int} searchFrom Id to search from
      +          * @param {int} searchFrom NodeId to search from (-1 for root)
                 * @returns {Promise} resourcePromise object.
                 *
                 */
      diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js
      index 2073307db9..353fe12cb8 100644
      --- a/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js
      +++ b/src/Umbraco.Web.UI.Client/src/common/resources/member.resource.js
      @@ -10,8 +10,8 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) {
       
               return umbRequestHelper.postSaveContent({
                   restApiUrl: umbRequestHelper.getApiUrl(
      -                   "memberApiBaseUrl",
      -                   "PostSave"),
      +                "memberApiBaseUrl",
      +                "PostSave"),
                   content: content,
                   action: action,
                   files: files,
      @@ -22,8 +22,7 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) {
           }
       
           return {
      -
      -        getPagedResults: function (memberTypeAlias, options) {
      +        getPagedResults: function(memberTypeAlias, options) {
       
                   if (memberTypeAlias === 'all-members') {
                       memberTypeAlias = null;
      @@ -67,35 +66,35 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) {
                   }
       
                   var params = [
      -                   { pageNumber: options.pageNumber },
      -                   { pageSize: options.pageSize },
      -                   { orderBy: options.orderBy },
      -                   { orderDirection: options.orderDirection },
      -                   { orderBySystemField: toBool(options.orderBySystemField) },
      -                   { filter: options.filter }
      +                { pageNumber: options.pageNumber },
      +                { pageSize: options.pageSize },
      +                { orderBy: options.orderBy },
      +                { orderDirection: options.orderDirection },
      +                { orderBySystemField: toBool(options.orderBySystemField) },
      +                { filter: options.filter }
                   ];
                   if (memberTypeAlias != null) {
                       params.push({ memberTypeAlias: memberTypeAlias });
                   }
       
                   return umbRequestHelper.resourcePromise(
      -                  $http.get(
      -                        umbRequestHelper.getApiUrl(
      -                              "memberApiBaseUrl",
      -                              "GetPagedResults",
      -                              params)),
      -                  'Failed to retrieve member paged result');
      +                $http.get(
      +                    umbRequestHelper.getApiUrl(
      +                        "memberApiBaseUrl",
      +                        "GetPagedResults",
      +                        params)),
      +                'Failed to retrieve member paged result');
               },
       
      -        getListNode: function (listName) {
      +        getListNode: function(listName) {
       
                   return umbRequestHelper.resourcePromise(
      -                  $http.get(
      -                        umbRequestHelper.getApiUrl(
      -                              "memberApiBaseUrl",
      -                              "GetListNodeDisplay",
      -                              [{ listName: listName }])),
      -                  'Failed to retrieve data for member list ' + listName);
      +                $http.get(
      +                    umbRequestHelper.getApiUrl(
      +                        "memberApiBaseUrl",
      +                        "GetListNodeDisplay",
      +                        [{ listName: listName }])),
      +                'Failed to retrieve data for member list ' + listName);
               },
       
               /**
      @@ -119,15 +118,15 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) {
                 * @returns {Promise} resourcePromise object containing the member item.
                 *
                 */
      -        getByKey: function (key) {
      +        getByKey: function(key) {
       
                   return umbRequestHelper.resourcePromise(
      -                  $http.get(
      -                        umbRequestHelper.getApiUrl(
      -                              "memberApiBaseUrl",
      -                              "GetByKey",
      -                              [{ key: key }])),
      -                  'Failed to retrieve data for member id ' + key);
      +                $http.get(
      +                    umbRequestHelper.getApiUrl(
      +                        "memberApiBaseUrl",
      +                        "GetByKey",
      +                        [{ key: key }])),
      +                'Failed to retrieve data for member id ' + key);
               },
       
               /**
      @@ -150,14 +149,14 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) {
                 * @returns {Promise} resourcePromise object.
                 *
                 */
      -        deleteByKey: function (key) {
      +        deleteByKey: function(key) {
                   return umbRequestHelper.resourcePromise(
      -                   $http.post(
      -                         umbRequestHelper.getApiUrl(
      -                               "memberApiBaseUrl",
      -                               "DeleteByKey",
      -                               [{ key: key }])),
      -                   'Failed to delete item ' + key);
      +                $http.post(
      +                    umbRequestHelper.getApiUrl(
      +                        "memberApiBaseUrl",
      +                        "DeleteByKey",
      +                        [{ key: key }])),
      +                'Failed to delete item ' + key);
               },
       
               /**
      @@ -190,24 +189,24 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) {
                 * @returns {Promise} resourcePromise object containing the member scaffold.
                 *
                 */
      -        getScaffold: function (alias) {
      +        getScaffold: function(alias) {
       
                   if (alias) {
                       return umbRequestHelper.resourcePromise(
      -                        $http.get(
      -                              umbRequestHelper.getApiUrl(
      -                                    "memberApiBaseUrl",
      -                                    "GetEmpty",
      -                                    [{ contentTypeAlias: alias }])),
      -                        'Failed to retrieve data for empty member item type ' + alias);
      +                    $http.get(
      +                        umbRequestHelper.getApiUrl(
      +                            "memberApiBaseUrl",
      +                            "GetEmpty",
      +                            [{ contentTypeAlias: alias }])),
      +                    'Failed to retrieve data for empty member item type ' + alias);
                   }
                   else {
                       return umbRequestHelper.resourcePromise(
      -                        $http.get(
      -                              umbRequestHelper.getApiUrl(
      -                                    "memberApiBaseUrl",
      -                                    "GetEmpty")),
      -                        'Failed to retrieve data for empty member item type ' + alias);
      +                    $http.get(
      +                        umbRequestHelper.getApiUrl(
      +                            "memberApiBaseUrl",
      +                            "GetEmpty")),
      +                    'Failed to retrieve data for empty member item type ' + alias);
                   }
       
               },
      @@ -240,7 +239,7 @@ function memberResource($q, $http, umbDataFormatter, umbRequestHelper) {
                 * @returns {Promise} resourcePromise object containing the saved media item.
                 *
                 */
      -        save: function (member, isNew, files) {
      +        save: function(member, isNew, files) {
                   return saveMember(member, "save" + (isNew ? "New" : ""), files);
               }
           };
      diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js
      new file mode 100644
      index 0000000000..40baf0f389
      --- /dev/null
      +++ b/src/Umbraco.Web.UI.Client/src/common/resources/tour.resource.js
      @@ -0,0 +1,35 @@
      +/**
      + * @ngdoc service
      + * @name umbraco.resources.usersResource
      + * @function
      + *
      + * @description
      + * Used by the users section to get users and send requests to create, invite, delete, etc. users.
      + */
      +(function () {
      +    'use strict';
      +
      +    function tourResource($http, umbRequestHelper, $q, umbDataFormatter) {
      +
      +        function getTours() {
      +
      +            return umbRequestHelper.resourcePromise(
      +                $http.get(
      +                    umbRequestHelper.getApiUrl(
      +                        "tourApiBaseUrl",
      +                        "GetTours")),
      +                'Failed to get tours');
      +        }
      +        
      +
      +        var resource = {
      +            getTours: getTours
      +        };
      +
      +        return resource;
      +
      +    }
      +
      +    angular.module('umbraco.resources').factory('tourResource', tourResource);
      +
      +})();
      diff --git a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js
      index e51310f584..2369af54b5 100644
      --- a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js
      +++ b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js
      @@ -74,6 +74,15 @@ function appState(eventsService) {
               showMenu: null
           };
       
      +    var drawerState = {
      +        //this view to show
      +        view: null,
      +        // bind custom values to the drawer
      +        model: null,
      +        //Whether the drawer is being shown or not
      +        showDrawer: null
      +    };
      +
           /** function to validate and set the state on a state object */
           function setState(stateObj, key, value, stateObjName) {
               if (!_.has(stateObj, key)) {
      @@ -212,6 +221,35 @@ function appState(eventsService) {
                   setState(menuState, key, value, "menuState");
               },
       
      +        /**
      +         * @ngdoc function
      +         * @name umbraco.services.angularHelper#getDrawerState
      +         * @methodOf umbraco.services.appState
      +         * @function
      +         *
      +         * @description
      +         * Returns the current drawer state value by key - we do not return an object here - we do NOT want this
      +         * to be publicly mutable and allow setting arbitrary values
      +         *
      +         */
      +        getDrawerState: function (key) {
      +            return getState(drawerState, key, "drawerState");
      +        },
      +
      +        /**
      +         * @ngdoc function
      +         * @name umbraco.services.angularHelper#setDrawerState
      +         * @methodOf umbraco.services.appState
      +         * @function
      +         *
      +         * @description
      +         * Sets a drawer state value by key
      +         *
      +         */
      +        setDrawerState: function (key, value) {
      +            setState(drawerState, key, value, "drawerState");
      +        }
      +
           };
       }
       angular.module('umbraco.services').factory('appState', appState);
      diff --git a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js
      index a84be208ba..16330f5493 100644
      --- a/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js
      +++ b/src/Umbraco.Web.UI.Client/src/common/services/assets.service.js
      @@ -2,12 +2,12 @@
        * @ngdoc service
        * @name umbraco.services.assetsService
        *
      - * @requires $q 
      + * @requires $q
        * @requires angularHelper
      - *  
      + *
        * @description
        * Promise-based utillity service to lazy-load client-side dependencies inside angular controllers.
      - * 
      + *
        * ##usage
        * To use, simply inject the assetsService into any controller that needs it, and make
        * sure the umbraco.services module is accesible - which it should be by default.
      @@ -18,7 +18,7 @@
        *                 //this code executes when the dependencies are done loading
        *          });
        *      });
      - * 
      + * * * You can also load individual files, which gives you greater control over what attibutes are passed to the file, as well as timeout * @@ -38,13 +38,14 @@ * //loadcss cannot determine when the css is done loading, so this will trigger instantly * }); * }); - * + * */ angular.module('umbraco.services') .factory('assetsService', function ($q, $log, angularHelper, umbRequestHelper, $rootScope, $http) { var initAssetsLoaded = false; - var appendRnd = function (url) { + + function appendRnd (url) { //if we don't have a global umbraco obj yet, the app is bootstrapping if (!Umbraco.Sys.ServerVariables.application) { return url; @@ -77,7 +78,8 @@ angular.module('umbraco.services') return this.loadedAssets[path]; } }, - /** + + /** Internal method. This is called when the application is loading and the user is already authenticated, or once the user is authenticated. There's a few assets the need to be loaded for the application to function but these assets require authentication to load. */ @@ -108,10 +110,10 @@ angular.module('umbraco.services') * * @description * Injects a file as a stylesheet into the document head - * + * * @param {String} path path to the css file to load * @param {Scope} scope optional scope to pass into the loader - * @param {Object} keyvalue collection of attributes to pass to the stylesheet element + * @param {Object} keyvalue collection of attributes to pass to the stylesheet element * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded */ @@ -149,10 +151,10 @@ angular.module('umbraco.services') * * @description * Injects a file as a javascript into the document - * + * * @param {String} path path to the js file to load * @param {Scope} scope optional scope to pass into the loader - * @param {Object} keyvalue collection of attributes to pass to the script element + * @param {Object} keyvalue collection of attributes to pass to the script element * @param {Number} timeout in milliseconds * @returns {Promise} Promise object which resolves when the file has loaded */ @@ -192,8 +194,8 @@ angular.module('umbraco.services') * @methodOf umbraco.services.assetsService * * @description - * Injects a collection of files, this can be ONLY js files - * + * Injects a collection of css and js files + * * * @param {Array} pathArray string array of paths to the files to load * @param {Scope} scope optional scope to pass into the loader @@ -206,61 +208,72 @@ angular.module('umbraco.services') throw "pathArray must be an array"; } + // Check to see if there's anything to load, resolve promise if not var nonEmpty = _.reject(pathArray, function (item) { return item === undefined || item === ""; - }); + }); - - //don't load anything if there's nothing to load - if (nonEmpty.length > 0) { - var promises = []; - var assets = []; - - //compile a list of promises - //blocking - _.each(nonEmpty, function (path) { - - path = convertVirtualPath(path); - - var asset = service._getAssetPromise(path); - //if not previously loaded, add to list of promises - if (asset.state !== "loaded") { - if (asset.state === "new") { - asset.state = "loading"; - assets.push(asset); - } - - //we need to always push to the promises collection to monitor correct - //execution - promises.push(asset.deferred.promise); - } - }); - - - //gives a central monitoring of all assets to load - promise = $q.all(promises); - - _.each(assets, function (asset) { - LazyLoad.js(appendRnd(asset.path), function () { - asset.state = "loaded"; - if (!scope) { - asset.deferred.resolve(true); - } - else { - angularHelper.safeApply(scope, function () { - asset.deferred.resolve(true); - }); - } - }); - }); - } - else { - //return and resolve + if (nonEmpty.length === 0) { var deferred = $q.defer(); promise = deferred.promise; deferred.resolve(true); + return promise; } + //compile a list of promises + //blocking + var promises = []; + var assets = []; + _.each(nonEmpty, function (path) { + path = convertVirtualPath(path); + var asset = service._getAssetPromise(path); + //if not previously loaded, add to list of promises + if (asset.state !== "loaded") { + if (asset.state === "new") { + asset.state = "loading"; + assets.push(asset); + } + + //we need to always push to the promises collection to monitor correct + //execution + promises.push(asset.deferred.promise); + } + }); + + //gives a central monitoring of all assets to load + promise = $q.all(promises); + + // Split into css and js asset arrays, and use LazyLoad on each array + var cssAssets = _.filter(assets, + function (asset) { + return asset.path.match(/(\.css$|\.css\?)/ig); + }); + var jsAssets = _.filter(assets, + function (asset) { + return asset.path.match(/(\.js$|\.js\?)/ig); + }); + + function assetLoaded(asset) { + asset.state = "loaded"; + if (!scope) { + asset.deferred.resolve(true); + return; + } + angularHelper.safeApply(scope, + function () { + asset.deferred.resolve(true); + }); + } + + if (cssAssets.length > 0) { + var cssPaths = _.map(cssAssets, function (asset) { return appendRnd(asset.path) }); + LazyLoad.css(cssPaths, function() { _.each(cssAssets, assetLoaded); }); + } + + if (jsAssets.length > 0) { + var jsPaths = _.map(jsAssets, function (asset) { return appendRnd(asset.path) }); + LazyLoad.js(jsPaths, function () { _.each(jsAssets, assetLoaded); }); + } return promise; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/backdrop.service.js b/src/Umbraco.Web.UI.Client/src/common/services/backdrop.service.js new file mode 100644 index 0000000000..e463845a1c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/backdrop.service.js @@ -0,0 +1,106 @@ +/** + @ngdoc service + * @name umbraco.services.backdropService + * + * @description + * Added in Umbraco 7.8. Application-wide service for handling backdrops. + * + */ + +(function () { + "use strict"; + + function backdropService(eventsService) { + + var args = { + opacity: null, + element: null, + elementPreventClick: false, + disableEventsOnClick: false, + show: false + }; + + /** + * @ngdoc method + * @name umbraco.services.backdropService#open + * @methodOf umbraco.services.backdropService + * + * @description + * Raises an event to open a backdrop + * @param {Object} options The backdrop options + * @param {Number} options.opacity Sets the opacity on the backdrop (default 0.4) + * @param {DomElement} options.element Highlights a DOM-element (HTML-selector) + * @param {Boolean} options.elementPreventClick Adds blocking element on top of highligted area to prevent all clicks + * @param {Boolean} options.disableEventsOnClick Disables all raised events when the backdrop is clicked + */ + function open(options) { + + if(options && options.element) { + args.element = options.element; + } + + if(options && options.disableEventsOnClick) { + args.disableEventsOnClick = options.disableEventsOnClick; + } + + args.show = true; + + eventsService.emit("appState.backdrop", args); + } + + /** + * @ngdoc method + * @name umbraco.services.backdropService#close + * @methodOf umbraco.services.backdropService + * + * @description + * Raises an event to close the backdrop + * + */ + function close() { + args.element = null; + args.show = false; + eventsService.emit("appState.backdrop", args); + } + + /** + * @ngdoc method + * @name umbraco.services.backdropService#setOpacity + * @methodOf umbraco.services.backdropService + * + * @description + * Raises an event which updates the opacity option on the backdrop + */ + function setOpacity(opacity) { + args.opacity = opacity; + eventsService.emit("appState.backdrop", args); + } + + /** + * @ngdoc method + * @name umbraco.services.backdropService#setHighlight + * @methodOf umbraco.services.backdropService + * + * @description + * Raises an event which updates the element option on the backdrop + */ + function setHighlight(element, preventClick) { + args.element = element; + args.elementPreventClick = preventClick; + eventsService.emit("appState.backdrop", args); + } + + var service = { + open: open, + close: close, + setOpacity: setOpacity, + setHighlight: setHighlight + }; + + return service; + + } + + angular.module("umbraco.services").factory("backdropService", backdropService); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index e439eda1ba..6aab9a655c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -5,7 +5,7 @@ * @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. **/ -function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) { +function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, localizationService, serverValidationManager, dialogService, formHelper, appState) { function isValidIdentifier(id){ //empty id <= 0 @@ -28,7 +28,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica return { - /** Used by the content editor and mini content editor to perform saving operations */ + /** Used by the content editor and mini content editor to perform saving operations */ //TODO: Make this a more helpful/reusable method for other form operations! we can simplify this form most forms contentEditorPerformSave: function (args) { if (!angular.isObject(args)) { @@ -100,7 +100,35 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica return deferred.promise; }, + + /** Used by the content editor and media editor to add an info tab to the tabs array (normally known as the properties tab) */ + addInfoTab: function (tabs) { + var infoTab = { + "alias": "_umb_infoTab", + "id": -1, + "label": "Info", + "properties": [] + }; + + // first check if tab is already added + var foundInfoTab = false; + + angular.forEach(tabs, function (tab) { + if (tab.id === infoTab.id && tab.alias === infoTab.alias) { + foundInfoTab = true; + } + }); + + // add info tab if is is not found + if (!foundInfoTab) { + localizationService.localize("general_info").then(function (value) { + infoTab.label = value; + tabs.push(infoTab); + }); + } + + }, /** Returns the action button definitions based on what permissions the user has. The content.allowedActions parameter contains a list of chars, each represents a button by permission so @@ -134,7 +162,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica labelKey: "buttons_saveAndPublish", handler: args.methods.saveAndPublish, hotKey: "ctrl+p", - hotKeyWhenHidden: true + hotKeyWhenHidden: true, + alias: "saveAndPublish" }; case "H": //send to publish @@ -143,7 +172,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica labelKey: "buttons_saveToPublish", handler: args.methods.sendToPublish, hotKey: "ctrl+p", - hotKeyWhenHidden: true + hotKeyWhenHidden: true, + alias: "sendToPublish" }; case "A": //save @@ -152,7 +182,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica labelKey: "buttons_save", handler: args.methods.save, hotKey: "ctrl+s", - hotKeyWhenHidden: true + hotKeyWhenHidden: true, + alias: "save" }; case "Z": //unpublish @@ -161,7 +192,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica labelKey: "content_unPublish", handler: args.methods.unPublish, hotKey: "ctrl+u", - hotKeyWhenHidden: true + hotKeyWhenHidden: true, + alias: "unpublish" }; default: return null; @@ -176,10 +208,10 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var buttonOrder = ["U", "H", "A"]; //Create the first button (primary button) - //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. - //Another tricky rule is if they only have Create + Browse permissions but not Save but if it's being created then they will - // require the Save button in order to create. - //So this code is going to create the primary button (either Publish, SendToPublish, Save) if we are not in create mode + //We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item. + //Another tricky rule is if they only have Create + Browse permissions but not Save but if it's being created then they will + // require the Save button in order to create. + //So this code is going to create the primary button (either Publish, SendToPublish, Save) if we are not in create mode // or if the user has access to create. if (!args.create || _.contains(args.content.allowedActions, "C")) { for (var b in buttonOrder) { @@ -187,9 +219,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica buttons.defaultButton = createButtonDefinition(buttonOrder[b]); break; } - } - //Here's the special check, if the button still isn't set and we are creating and they have create access - //we need to add the Save button + } + //Here's the special check, if the button still isn't set and we are creating and they have create access + //we need to add the Save button if (!buttons.defaultButton && args.create && _.contains(args.content.allowedActions, "C")) { buttons.defaultButton = createButtonDefinition("A"); } @@ -220,6 +252,40 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } + // If we have a scheduled publish or unpublish date change the default button to + // "save" and update the label to "save and schedule + if(args.content.releaseDate || args.content.removeDate) { + + // if save button is alread the default don't change it just update the label + if (buttons.defaultButton && buttons.defaultButton.letter === "A") { + buttons.defaultButton.labelKey = "buttons_saveAndSchedule"; + return; + } + + if(buttons.defaultButton && buttons.subButtons && buttons.subButtons.length > 0) { + // save a copy of the default so we can push it to the sub buttons later + var defaultButtonCopy = angular.copy(buttons.defaultButton); + var newSubButtons = []; + + // if save button is not the default button - find it and make it the default + angular.forEach(buttons.subButtons, function (subButton) { + + if (subButton.letter === "A") { + buttons.defaultButton = subButton; + buttons.defaultButton.labelKey = "buttons_saveAndSchedule"; + } else { + newSubButtons.push(subButton); + } + + }); + + // push old default button into subbuttons + newSubButtons.push(defaultButtonCopy); + buttons.subButtons = newSubButtons; + } + + } + return buttons; }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js index 174cc8abe2..e28970336f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/events.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/events.service.js @@ -16,52 +16,16 @@ function eventsService($q, $rootScope) { return { - /** raise an event with a given name, returns an array of promises for each listener */ - emit: function (name, args) { + /** raise an event with a given name */ + emit: function (name, args) { //there are no listeners if (!$rootScope.$$listeners[name]) { return; - //return []; } //send the event $rootScope.$emit(name, args); - - - //PP: I've commented out the below, since we currently dont - // expose the eventsService as a documented api - // and think we need to figure out our usecases for this - // since the below modifies the return value of the then on() method - /* - //setup a deferred promise for each listener - var deferred = []; - for (var i = 0; i < $rootScope.$$listeners[name].length; i++) { - deferred.push($q.defer()); - }*/ - - //create a new event args object to pass to the - // $emit containing methods that will allow listeners - // to return data in an async if required - /* - var eventArgs = { - args: args, - reject: function (a) { - deferred.pop().reject(a); - }, - resolve: function (a) { - deferred.pop().resolve(a); - } - };*/ - - - - /* - //return an array of promises - var promises = _.map(deferred, function(p) { - return p.promise; - }); - return promises;*/ }, /** subscribe to a method, or use scope.$on = same thing */ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/help.service.js b/src/Umbraco.Web.UI.Client/src/common/services/help.service.js index 6d7d341a57..77621eee9c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/help.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/help.service.js @@ -1,5 +1,5 @@ angular.module('umbraco.services') - .factory('helpService', function ($http, $q){ + .factory('helpService', function ($http, $q, umbRequestHelper) { var helpTopics = {}; var defaultUrl = "http://our.umbraco.org/rss/help"; @@ -47,8 +47,6 @@ angular.module('umbraco.services') return deferred.promise; } - - var service = { findHelp: function (args) { var url = service.getUrl(defaultUrl, args); @@ -60,6 +58,26 @@ angular.module('umbraco.services') return fetchUrl(url); }, + getContextHelpForPage: function (section, tree, baseurl) { + + var qs = "?section=" + section + "&tree=" + tree; + + if (tree) { + qs += "&tree=" + tree; + } + + if (baseurl) { + qs += "&baseurl=" + encodeURIComponent(baseurl); + } + + var url = umbRequestHelper.getApiUrl( + "helpApiBaseUrl", + "GetContextHelpForPage" + qs); + + return umbRequestHelper.resourcePromise( + $http.get(url), "Failed to get lessons content"); + }, + getUrl: function(url, args){ return url + "?" + $.param(args); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/history.service.js b/src/Umbraco.Web.UI.Client/src/common/services/history.service.js index 75963112e5..ee02efc4a0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/history.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/history.service.js @@ -115,6 +115,27 @@ angular.module('umbraco.services') */ getCurrent: function(){ return nArray; - } + }, + + /** + * @ngdoc method + * @name umbraco.services.historyService#getLastAccessedItemForSection + * @methodOf umbraco.services.historyService + * + * @description + * Method to return the item that was last accessed in the given section + * + * @param {string} sectionAlias Alias of the section to return the last accessed item for. + */ + getLastAccessedItemForSection: function (sectionAlias) { + for (var i = 0, len = nArray.length; i < len; i++) { + var item = nArray[i]; + if (item.link.indexOf(sectionAlias + "/") === 0) { + return item; + } + } + + return null; + } }; }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js index 84bb4333a2..2ffdc3f1dd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/listviewhelper.service.js @@ -269,7 +269,8 @@ var isSelected = false; for (var i = 0; selection.length > i; i++) { var selectedItem = selection[i]; - if (item.id === selectedItem.id || item.key === selectedItem.key) { + // if item.id is 2147483647 (int.MaxValue) use item.key + if ((item.id !== 2147483647 && item.id === selectedItem.id) || item.key === selectedItem.key) { isSelected = true; } } @@ -294,7 +295,8 @@ function deselectItem(item, selection) { for (var i = 0; selection.length > i; i++) { var selectedItem = selection[i]; - if (item.id === selectedItem.id) { + // if item.id is 2147483647 (int.MaxValue) use item.key + if ((item.id !== 2147483647 && item.id === selectedItem.id) || item.key === selectedItem.key) { selection.splice(i, 1); item.selected = false; } @@ -366,7 +368,7 @@ var item = items[i]; if (checkbox.checked) { - selection.push({ id: item.id }); + selection.push({ id: item.id, key: item.key }); } else { clearSelection = true; } @@ -405,7 +407,8 @@ for (var selectedIndex = 0; selection.length > selectedIndex; selectedIndex++) { var selectedItem = selection[selectedIndex]; - if (item.id === selectedItem.id) { + // if item.id is 2147483647 (int.MaxValue) use item.key + if ((item.id !== 2147483647 && item.id === selectedItem.id) || item.key === selectedItem.key) { numberOfSelectedItem++; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js b/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js index c7438a1c47..7200e1f6f8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js @@ -8,9 +8,27 @@ * @description * Defines the methods that are called when menu items declare only an action to execute */ -function umbracoMenuActions($q, treeService, $location, navigationService, appState, localizationService, userResource) { +function umbracoMenuActions($q, treeService, $location, navigationService, appState, localizationService, userResource, umbRequestHelper, notificationsService) { return { + + "ExportMember": function(args) { + var url = umbRequestHelper.getApiUrl( + "memberApiBaseUrl", + "ExportMemberData", + [{ key: args.entity.id }]); + + umbRequestHelper.downloadFile(url).then(function() { + localizationService.localize("speechBubbles_memberExportedSuccess").then(function (value) { + notificationsService.success(value); + }) + }, function(data) { + localizationService.localize("speechBubbles_memberExportedError").then(function (value) { + notificationsService.error(value); + }) + }); + + }, "DisableUser": function(args) { localizationService.localize("defaultdialogs_confirmdisable").then(function (txtConfirmDisable) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index 9687e5c40d..20e73f9e06 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -524,37 +524,6 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo return service.userDialog; }, - /** - * @ngdoc method - * @name umbraco.services.navigationService#showUserDialog - * @methodOf umbraco.services.navigationService - * - * @description - * Opens the user dialog, next to the sections navigation - * template is located in views/common/dialogs/user.html - */ - showHelpDialog: function () { - // hide tray and close user dialog - service.hideTray(); - if (service.userDialog) { - service.userDialog.close(); - } - - if(service.helpDialog){ - service.helpDialog.close(); - service.helpDialog = undefined; - } - - service.helpDialog = dialogService.open( - { - template: "views/common/dialogs/help.html", - modalClass: "umb-modal-left", - show: true - }); - - return service.helpDialog; - }, - /** * @ngdoc method * @name umbraco.services.navigationService#showDialog diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js new file mode 100644 index 0000000000..28ac7f6485 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/tour.service.js @@ -0,0 +1,281 @@ +/** + @ngdoc service + * @name umbraco.services.tourService + * + * @description + * Added in Umbraco 7.8. Application-wide service for handling tours. + */ +(function () { + 'use strict'; + + function tourService(eventsService, currentUserResource, $q, tourResource) { + + var tours = []; + var currentTour = null; + + /** + * Registers all tours from the server and returns a promise + */ + function registerAllTours() { + tours = []; + return tourResource.getTours().then(function(tourFiles) { + angular.forEach(tourFiles, function (tourFile) { + angular.forEach(tourFile.tours, function(newTour) { + validateTour(newTour); + validateTourRegistration(newTour); + tours.push(newTour); + }); + }); + eventsService.emit("appState.tour.updatedTours", tours); + }); + } + + /** + * Method to return all of the tours as a new instance + */ + function getTours() { + return tours; + } + + /** + * @ngdoc method + * @name umbraco.services.tourService#startTour + * @methodOf umbraco.services.tourService + * + * @description + * Raises an event to start a tour + * @param {Object} tour The tour which should be started + */ + function startTour(tour) { + validateTour(tour); + eventsService.emit("appState.tour.start", tour); + currentTour = tour; + } + + /** + * @ngdoc method + * @name umbraco.services.tourService#endTour + * @methodOf umbraco.services.tourService + * + * @description + * Raises an event to end the current tour + */ + function endTour(tour) { + eventsService.emit("appState.tour.end", tour); + currentTour = null; + } + + /** + * Disables a tour for the user, raises an event and returns a promise + * @param {any} tour + */ + function disableTour(tour) { + var deferred = $q.defer(); + tour.disabled = true; + currentUserResource + .saveTourStatus({ alias: tour.alias, disabled: tour.disabled, completed: tour.completed }).then( + function() { + eventsService.emit("appState.tour.end", tour); + currentTour = null; + deferred.resolve(tour); + }); + return deferred.promise; + } + + /** + * @ngdoc method + * @name umbraco.services.tourService#completeTour + * @methodOf umbraco.services.tourService + * + * @description + * Completes a tour for the user, raises an event and returns a promise + * @param {Object} tour The tour which should be completed + */ + function completeTour(tour) { + var deferred = $q.defer(); + tour.completed = true; + currentUserResource + .saveTourStatus({ alias: tour.alias, disabled: tour.disabled, completed: tour.completed }).then( + function() { + eventsService.emit("appState.tour.complete", tour); + currentTour = null; + deferred.resolve(tour); + }); + return deferred.promise; + } + + /** + * @ngdoc method + * @name umbraco.services.tourService#getCurrentTour + * @methodOf umbraco.services.tourService + * + * @description + * Returns the current tour + * @returns {Object} Returns the current tour + */ + function getCurrentTour() { + //TODO: This should be reset if a new user logs in + return currentTour; + } + + /** + * @ngdoc method + * @name umbraco.services.tourService#getGroupedTours + * @methodOf umbraco.services.tourService + * + * @description + * Returns a promise of grouped tours with the current user statuses + * @returns {Array} All registered tours grouped by tour group + */ + function getGroupedTours() { + var deferred = $q.defer(); + var tours = getTours(); + setTourStatuses(tours).then(function() { + var groupedTours = []; + tours.forEach(function (item) { + + var groupExists = false; + var newGroup = { + "group": "", + "tours": [] + }; + + groupedTours.forEach(function(group){ + // extend existing group if it is already added + if(group.group === item.group) { + if(item.groupOrder) { + group.groupOrder = item.groupOrder + } + groupExists = true; + group.tours.push(item) + } + }); + + // push new group to array if it doesn't exist + if(!groupExists) { + newGroup.group = item.group; + if(item.groupOrder) { + newGroup.groupOrder = item.groupOrder + } + newGroup.tours.push(item); + groupedTours.push(newGroup); + } + + }); + + deferred.resolve(groupedTours); + }); + return deferred.promise; + } + + /** + * @ngdoc method + * @name umbraco.services.tourService#getTourByAlias + * @methodOf umbraco.services.tourService + * + * @description + * Returns a promise of the tour found by alias with the current user statuses + * @param {Object} tourAlias The tour alias of the tour which should be returned + * @returns {Object} Tour object + */ + function getTourByAlias(tourAlias) { + var deferred = $q.defer(); + var tours = getTours(); + setTourStatuses(tours).then(function () { + var tour = _.findWhere(tours, { alias: tourAlias }); + deferred.resolve(tour); + }); + return deferred.promise; + } + + /////////// + + /** + * Validates a tour object and makes sure it consists of the correct properties needed to start a tour + * @param {any} tour + */ + function validateTour(tour) { + + if (!tour) { + throw "A tour is not specified"; + } + + if (!tour.alias) { + throw "A tour alias is required"; + } + + if (!tour.steps) { + throw "Tour " + tour.alias + " is missing tour steps"; + } + + if (tour.steps && tour.steps.length === 0) { + throw "Tour " + tour.alias + " is missing tour steps"; + } + + if (tour.requiredSections.length === 0) { + throw "Tour " + tour.alias + " is missing the required sections"; + } + } + + /** + * Validates a tour before it gets registered in the service + * @param {any} tour + */ + function validateTourRegistration(tour) { + // check for existing tours with the same alias + angular.forEach(tours, function (existingTour) { + if (existingTour.alias === tour.alias) { + throw "A tour with the alias " + tour.alias + " is already registered"; + } + }); + } + + /** + * Based on the tours given, this will set each of the tour statuses (disabled/completed) based on what is stored against the current user + * @param {any} tours + */ + function setTourStatuses(tours) { + + var deferred = $q.defer(); + currentUserResource.getTours().then(function (storedTours) { + + angular.forEach(storedTours, function (storedTour) { + if (storedTour.completed === true) { + angular.forEach(tours, function (tour) { + if (storedTour.alias === tour.alias) { + tour.completed = true; + } + }); + } + if (storedTour.disabled === true) { + angular.forEach(tours, function (tour) { + if (storedTour.alias === tour.alias) { + tour.disabled = true; + } + }); + } + }); + + deferred.resolve(tours); + }); + return deferred.promise; + } + + var service = { + registerAllTours: registerAllTours, + startTour: startTour, + endTour: endTour, + disableTour: disableTour, + completeTour: completeTour, + getCurrentTour: getCurrentTour, + getGroupedTours: getGroupedTours, + getTourByAlias: getTourByAlias + }; + + return service; + + } + + angular.module("umbraco.services").factory("tourService", tourService); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 5fc2416927..78dec2f065 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -14,7 +14,7 @@ if (!model) { return null; } - var trimmed = _.omit(model, ["confirm", "generatedPassword"]) + var trimmed = _.omit(model, ["confirm", "generatedPassword"]); //ensure that the pass value is null if all child properties are null var allNull = true; @@ -56,7 +56,7 @@ }); var saveProperties = _.map(realProperties, function (p) { - var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile'); + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData'); return saveProperty; }); @@ -242,8 +242,8 @@ var propGroups = _.find(genericTab.properties, function (item) { return item.alias === "_umb_membergroup"; }); - saveModel.email = propEmail.value; - saveModel.username = propLogin.value; + saveModel.email = propEmail.value.trim(); + saveModel.username = propLogin.value.trim(); saveModel.password = this.formatChangePasswordModel(propPass.value); @@ -267,10 +267,10 @@ // by looking at the key switch (foundAlias[0]) { case "umbracoMemberLockedOut": - saveModel.isLockedOut = prop.value.toString() === "1" ? true : false; + saveModel.isLockedOut = prop.value ? (prop.value.toString() === "1" ? true : false) : false; break; case "umbracoMemberApproved": - saveModel.isApproved = prop.value.toString() === "1" ? true : false; + saveModel.isApproved = prop.value ? (prop.value.toString() === "1" ? true : false) : true; break; case "umbracoMemberComments": saveModel.comments = prop.value; @@ -304,14 +304,14 @@ _.each(tab.properties, function (prop) { //don't include the custom generic tab properties - if (!prop.alias.startsWith("_umb_")) { + //don't include a property that is marked readonly + if (!prop.alias.startsWith("_umb_") && !prop.readonly) { saveModel.properties.push({ id: prop.id, alias: prop.alias, value: prop.value }); } - }); }); @@ -324,22 +324,13 @@ //this is basically the same as for media but we need to explicitly add some extra properties var saveModel = this.formatMediaPostData(displayModel, action); - var genericTab = _.find(displayModel.tabs, function (item) { - return item.id === 0; - }); - - var propExpireDate = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_expiredate"; - }); - var propReleaseDate = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_releasedate"; - }); - var propTemplate = _.find(genericTab.properties, function (item) { - return item.alias === "_umb_template"; - }); - saveModel.expireDate = propExpireDate ? propExpireDate.value : null; - saveModel.releaseDate = propReleaseDate ? propReleaseDate.value : null; - saveModel.templateAlias = propTemplate ? propTemplate.value : null; + var propExpireDate = displayModel.removeDate; + var propReleaseDate = displayModel.releaseDate; + var propTemplate = displayModel.template; + + saveModel.expireDate = propExpireDate ? propExpireDate : null; + saveModel.releaseDate = propReleaseDate ? propReleaseDate : null; + saveModel.templateAlias = propTemplate ? propTemplate : null; return saveModel; } @@ -347,4 +338,4 @@ } angular.module('umbraco.services').factory('umbDataFormatter', umbDataFormatter); -})(); \ No newline at end of file +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 3732e25403..d950d39619 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -230,10 +230,12 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ //success callback //reset the tabs and set the active one - _.each(data.tabs, function (item) { - item.active = false; - }); - data.tabs[activeTabIndex].active = true; + if(data.tabs && data.tabs.length > 0) { + _.each(data.tabs, function (item) { + item.active = false; + }); + data.tabs[activeTabIndex].active = true; + } //the data returned is the up-to-date data so the UI will refresh deferred.resolve(data); @@ -331,6 +333,122 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ failureCallback.apply(this, [data, status, headers, config]); } }); + }, + + /** + * Downloads a file to the client using AJAX/XHR + * Based on an implementation here: web.student.tuwien.ac.at/~e0427417/jsdownload.html + * See https://stackoverflow.com/a/24129082/694494 + */ + downloadFile : function (httpPath) { + + var deferred = $q.defer(); + + // Use an arraybuffer + $http.get(httpPath, { responseType: 'arraybuffer' }) + .success(function (data, status, headers) { + + var octetStreamMime = 'application/octet-stream'; + var success = false; + + // Get the headers + headers = headers(); + + // Get the filename from the x-filename header or default to "download.bin" + var filename = headers['x-filename'] || 'download.bin'; + + // Determine the content type from the header or default to "application/octet-stream" + var contentType = headers['content-type'] || octetStreamMime; + + try { + // Try using msSaveBlob if supported + console.log("Trying saveBlob method ..."); + var blob = new Blob([data], { type: contentType }); + if (navigator.msSaveBlob) + navigator.msSaveBlob(blob, filename); + else { + // Try using other saveBlob implementations, if available + var saveBlob = navigator.webkitSaveBlob || navigator.mozSaveBlob || navigator.saveBlob; + if (saveBlob === undefined) throw "Not supported"; + saveBlob(blob, filename); + } + console.log("saveBlob succeeded"); + success = true; + } catch (ex) { + console.log("saveBlob method failed with the following exception:"); + console.log(ex); + } + + if (!success) { + // Get the blob url creator + var urlCreator = window.URL || window.webkitURL || window.mozURL || window.msURL; + if (urlCreator) { + // Try to use a download link + var link = document.createElement('a'); + if ('download' in link) { + // Try to simulate a click + try { + // Prepare a blob URL + console.log("Trying download link method with simulated click ..."); + var blob = new Blob([data], { type: contentType }); + var url = urlCreator.createObjectURL(blob); + link.setAttribute('href', url); + + // Set the download attribute (Supported in Chrome 14+ / Firefox 20+) + link.setAttribute("download", filename); + + // Simulate clicking the download link + var event = document.createEvent('MouseEvents'); + event.initMouseEvent('click', true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(event); + console.log("Download link method with simulated click succeeded"); + success = true; + + } catch (ex) { + console.log("Download link method with simulated click failed with the following exception:"); + console.log(ex); + } + } + + if (!success) { + // Fallback to window.location method + try { + // Prepare a blob URL + // Use application/octet-stream when using window.location to force download + console.log("Trying download link method with window.location ..."); + var blob = new Blob([data], { type: octetStreamMime }); + var url = urlCreator.createObjectURL(blob); + window.location = url; + console.log("Download link method with window.location succeeded"); + success = true; + } catch (ex) { + console.log("Download link method with window.location failed with the following exception:"); + console.log(ex); + } + } + + } + } + + if (!success) { + // Fallback to window.open method + console.log("No methods worked for saving the arraybuffer, using last resort window.open"); + window.open(httpPath, '_blank', ''); + } + + deferred.resolve(); + }) + .error(function (data, status) { + console.log("Request failed with status: " + status); + + deferred.reject({ + errorMsg: "An error occurred downloading the file", + data: data, + status: status + }); + }); + + return deferred.promise; } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/usershelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/usershelper.service.js index 7d96e17d1f..3d6ad8b265 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/usershelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/usershelper.service.js @@ -11,6 +11,18 @@ { "value": 3, "name": "Invited", "key": "Invited", "color": "warning" } ]; + angular.forEach(userStates, function (userState) { + var key = "user_state" + userState.key; + localizationService.localize(key).then(function (value) { + var reg = /^\[[\S\s]*]$/g; + var result = reg.test(value); + if (result === false) { + // Only translate if key exists + userState.name = value; + } + }); + }); + function getUserStateFromValue(value) { var foundUserState; angular.forEach(userStates, function (userState) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 067ef60492..11444ff65b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -1,420 +1,444 @@ /*Contains multiple services for various helper tasks */ function versionHelper() { - return { + return { - //see: https://gist.github.com/TheDistantSea/8021359 - versionCompare: function (v1, v2, options) { - var lexicographical = options && options.lexicographical, - zeroExtend = options && options.zeroExtend, - v1parts = v1.split('.'), - v2parts = v2.split('.'); + //see: https://gist.github.com/TheDistantSea/8021359 + versionCompare: function (v1, v2, options) { + var lexicographical = options && options.lexicographical, + zeroExtend = options && options.zeroExtend, + v1parts = v1.split('.'), + v2parts = v2.split('.'); - function isValidPart(x) { - return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); - } + function isValidPart(x) { + return (lexicographical ? /^\d+[A-Za-z]*$/ : /^\d+$/).test(x); + } - if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { - return NaN; - } + if (!v1parts.every(isValidPart) || !v2parts.every(isValidPart)) { + return NaN; + } - if (zeroExtend) { - while (v1parts.length < v2parts.length) { - v1parts.push("0"); + if (zeroExtend) { + while (v1parts.length < v2parts.length) { + v1parts.push("0"); + } + while (v2parts.length < v1parts.length) { + v2parts.push("0"); + } + } + + if (!lexicographical) { + v1parts = v1parts.map(Number); + v2parts = v2parts.map(Number); + } + + for (var i = 0; i < v1parts.length; ++i) { + if (v2parts.length === i) { + return 1; + } + + if (v1parts[i] === v2parts[i]) { + continue; + } + else if (v1parts[i] > v2parts[i]) { + return 1; + } + else { + return -1; + } + } + + if (v1parts.length !== v2parts.length) { + return -1; + } + + return 0; } - while (v2parts.length < v1parts.length) { - v2parts.push("0"); - } - } - - if (!lexicographical) { - v1parts = v1parts.map(Number); - v2parts = v2parts.map(Number); - } - - for (var i = 0; i < v1parts.length; ++i) { - if (v2parts.length === i) { - return 1; - } - - if (v1parts[i] === v2parts[i]) { - continue; - } - else if (v1parts[i] > v2parts[i]) { - return 1; - } - else { - return -1; - } - } - - if (v1parts.length !== v2parts.length) { - return -1; - } - - return 0; - } - }; + }; } angular.module('umbraco.services').factory('versionHelper', versionHelper); function dateHelper() { - return { + return { - convertToServerStringTime: function (momentLocal, serverOffsetMinutes, format) { + convertToServerStringTime: function (momentLocal, serverOffsetMinutes, format) { - //get the formatted offset time in HH:mm (server time offset is in minutes) - var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + - moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); + //get the formatted offset time in HH:mm (server time offset is in minutes) + var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + + moment() + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); - var server = moment.utc(momentLocal).utcOffset(formattedOffset); - return server.format(format ? format : "YYYY-MM-DD HH:mm:ss"); - }, + var server = moment.utc(momentLocal).utcOffset(formattedOffset); + return server.format(format ? format : "YYYY-MM-DD HH:mm:ss"); + }, - convertToLocalMomentTime: function (strVal, serverOffsetMinutes) { + convertToLocalMomentTime: function (strVal, serverOffsetMinutes) { - //get the formatted offset time in HH:mm (server time offset is in minutes) - var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + - moment() - .startOf('day') - .minutes(Math.abs(serverOffsetMinutes)) - .format('HH:mm'); + //get the formatted offset time in HH:mm (server time offset is in minutes) + var formattedOffset = (serverOffsetMinutes > 0 ? "+" : "-") + + moment() + .startOf('day') + .minutes(Math.abs(serverOffsetMinutes)) + .format('HH:mm'); - //convert to the iso string format - var isoFormat = moment(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset; + //if the string format already denotes that it's in "Roundtrip UTC" format (i.e. "2018-02-07T00:20:38.173Z") + //otherwise known as https://en.wikipedia.org/wiki/ISO_8601. This is the default format returned from the server + //since that is the default formatter for newtonsoft.json. When it is in this format, we need to tell moment + //to load the date as UTC so it's not changed, otherwise load it normally + var isoFormat; + if (strVal.indexOf("T") > -1 && strVal.endsWith("Z")) { + isoFormat = moment.utc(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset; + } + else { + isoFormat = moment(strVal).format("YYYY-MM-DDTHH:mm:ss") + formattedOffset; + } - //create a moment with the iso format which will include the offset with the correct time - // then convert it to local time - return moment.parseZone(isoFormat).local(); - } + //create a moment with the iso format which will include the offset with the correct time + // then convert it to local time + return moment.parseZone(isoFormat).local(); + }, - }; + getLocalDate: function (date, culture, format) { + if (date) { + var dateVal; + var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; + var localOffset = new Date().getTimezoneOffset(); + var serverTimeNeedsOffsetting = -serverOffset !== localOffset; + if (serverTimeNeedsOffsetting) { + dateVal = this.convertToLocalMomentTime(date, serverOffset); + } else { + dateVal = moment(date, 'YYYY-MM-DD HH:mm:ss'); + } + return dateVal.locale(culture).format(format); + } + } + + }; } angular.module('umbraco.services').factory('dateHelper', dateHelper); function packageHelper(assetsService, treeService, eventsService, $templateCache) { - return { + return { - /** Called when a package is installed, this resets a bunch of data and ensures the new package assets are loaded in */ - packageInstalled: function () { + /** Called when a package is installed, this resets a bunch of data and ensures the new package assets are loaded in */ + packageInstalled: function () { - //clears the tree - treeService.clearCache(); + //clears the tree + treeService.clearCache(); - //clears the template cache - $templateCache.removeAll(); + //clears the template cache + $templateCache.removeAll(); - //emit event to notify anything else - eventsService.emit("app.reInitialize"); - } + //emit event to notify anything else + eventsService.emit("app.reInitialize"); + } - }; + }; } angular.module('umbraco.services').factory('packageHelper', packageHelper); //TODO: I believe this is obsolete function umbPhotoFolderHelper($compile, $log, $timeout, $filter, imageHelper, mediaHelper, umbRequestHelper) { - return { - /** sets the image's url, thumbnail and if its a folder */ - setImageData: function (img) { + return { + /** sets the image's url, thumbnail and if its a folder */ + setImageData: function (img) { - img.isFolder = !mediaHelper.hasFilePropertyType(img); + img.isFolder = !mediaHelper.hasFilePropertyType(img); - if (!img.isFolder) { - img.thumbnail = mediaHelper.resolveFile(img, true); - img.image = mediaHelper.resolveFile(img, false); - } - }, + if (!img.isFolder) { + img.thumbnail = mediaHelper.resolveFile(img, true); + img.image = mediaHelper.resolveFile(img, false); + } + }, - /** sets the images original size properties - will check if it is a folder and if so will just make it square */ - setOriginalSize: function (img, maxHeight) { - //set to a square by default - img.originalWidth = maxHeight; - img.originalHeight = maxHeight; + /** sets the images original size properties - will check if it is a folder and if so will just make it square */ + setOriginalSize: function (img, maxHeight) { + //set to a square by default + img.originalWidth = maxHeight; + img.originalHeight = maxHeight; - var widthProp = _.find(img.properties, function (v) { return (v.alias === "umbracoWidth"); }); - if (widthProp && widthProp.value) { - img.originalWidth = parseInt(widthProp.value, 10); - if (isNaN(img.originalWidth)) { - img.originalWidth = maxHeight; + var widthProp = _.find(img.properties, function (v) { return (v.alias === "umbracoWidth"); }); + if (widthProp && widthProp.value) { + img.originalWidth = parseInt(widthProp.value, 10); + if (isNaN(img.originalWidth)) { + img.originalWidth = maxHeight; + } + } + var heightProp = _.find(img.properties, function (v) { return (v.alias === "umbracoHeight"); }); + if (heightProp && heightProp.value) { + img.originalHeight = parseInt(heightProp.value, 10); + if (isNaN(img.originalHeight)) { + img.originalHeight = maxHeight; + } + } + }, + + /** sets the image style which get's used in the angular markup */ + setImageStyle: function (img, width, height, rightMargin, bottomMargin) { + img.style = { width: width + "px", height: height + "px", "margin-right": rightMargin + "px", "margin-bottom": bottomMargin + "px" }; + img.thumbStyle = { + "background-image": "url('" + img.thumbnail + "')", + "background-repeat": "no-repeat", + "background-position": "center", + "background-size": Math.min(width, img.originalWidth) + "px " + Math.min(height, img.originalHeight) + "px" + }; + }, + + /** gets the image's scaled wdith based on the max row height */ + getScaledWidth: function (img, maxHeight) { + var scaled = img.originalWidth * maxHeight / img.originalHeight; + return scaled; + //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row + //return Math.floor(scaled); + }, + + /** returns the target row width taking into account how many images will be in the row and removing what the margin is */ + getTargetWidth: function (imgsPerRow, maxRowWidth, margin) { + //take into account the margin, we will have 1 less margin item than we have total images + return (maxRowWidth - ((imgsPerRow - 1) * margin)); + }, + + /** + This will determine the row/image height for the next collection of images which takes into account the + ideal image count per row. It will check if a row can be filled with this ideal count and if not - if there + are additional images available to fill the row it will keep calculating until they fit. + + It will return the calculated height and the number of images for the row. + + targetHeight = optional; + */ + getRowHeightForImages: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { + + var idealImages = imgs.slice(0, idealImgPerRow); + //get the target row width without margin + var targetRowWidth = this.getTargetWidth(idealImages.length, maxRowWidth, margin); + //this gets the image with the smallest height which equals the maximum we can scale up for this image block + var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); + //if the max scale height is smaller than the min display height, we'll use the min display height + targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); + + var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); + + if (attemptedRowHeight != null) { + + //if this is smaller than the min display then we need to use the min display, + // which means we'll need to remove one from the row so we can scale up to fill the row + if (attemptedRowHeight < minDisplayHeight) { + + if (idealImages.length > 1) { + + //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight + targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); + return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); + } + else { + //this will occur when we only have one image remaining in the row but it's still going to be too wide even when + // using the minimum display height specified. In this case we're going to have to just crop the image in it's center + // using the minimum display height and the full row width + return { height: minDisplayHeight, imgCount: 1 }; + } + } + else { + //success! + return { height: attemptedRowHeight, imgCount: idealImages.length }; + } + } + + //we know the width will fit in a row, but we now need to figure out if we can fill + // the entire row in the case that we have more images remaining than the idealImgPerRow. + + if (idealImages.length === imgs.length) { + //we have no more remaining images to fill the space, so we'll just use the calc height + return { height: targetHeight, imgCount: idealImages.length }; + } + else if (idealImages.length === 1) { + //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally + // in the row. + return { height: minDisplayHeight, imgCount: 1 }; + } + else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) { + + //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so + // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current + // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit) + + while (targetHeight < maxRowHeight && (maxRowHeight - targetHeight) > 5) { + targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); + attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); + if (attemptedRowHeight != null) { + //success! + return { height: attemptedRowHeight, imgCount: idealImages.length }; + } + } + + //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count. + return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin); + } + else if (targetHeight === maxRowHeight) { + + //This is going to happen when: + // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight) + // * Then we'll try to remove an image, but when we try to scale to fit, the width comes up too narrow but the images are already at their + // maximum height (maxRowHeight) + // * So we're stuck, we cannot precicely fit the current list of images, so we'll render a row that will be max height but won't be wide enough + // which is better than rendering a row that is shorter than the minimum since that could be quite small. + + return { height: targetHeight, imgCount: idealImages.length }; + } + else { + + //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits + return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin); + } + + }, + + performGetRowHeight: function (idealImages, targetRowWidth, minDisplayHeight, targetHeight) { + + var currRowWidth = 0; + + for (var i = 0; i < idealImages.length; i++) { + var scaledW = this.getScaledWidth(idealImages[i], targetHeight); + currRowWidth += scaledW; + } + + if (currRowWidth > targetRowWidth) { + //get the new scaled height to fit + var newHeight = targetRowWidth * targetHeight / currRowWidth; + + return newHeight; + } + else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) { + //if there is only one image, then return the target height + return targetHeight; + } + else if (currRowWidth / targetRowWidth > 0.90) { + //it's close enough, it's at least 90% of the width so we'll accept it with the target height + return targetHeight; + } + else { + //if it's not successful, return null + return null; + } + }, + + /** builds an image grid row */ + buildRow: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { + var currRowWidth = 0; + var row = { images: [] }; + + var imageRowHeight = this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); + var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin); + + var sizes = []; + //loop through the images we know fit into the height + for (var i = 0; i < imageRowHeight.imgCount; i++) { + //get the lower width to ensure it always fits + var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); + + if (currRowWidth + scaledWidth <= targetWidth) { + currRowWidth += scaledWidth; + sizes.push({ + width: scaledWidth, + //ensure that the height is rounded + height: Math.round(imageRowHeight.height) + }); + row.images.push(imgs[i]); + } + else if (imageRowHeight.imgCount === 1 && row.images.length === 0) { + //the image is simply too wide, we'll crop/center it + sizes.push({ + width: maxRowWidth, + //ensure that the height is rounded + height: Math.round(imageRowHeight.height) + }); + row.images.push(imgs[i]); + } + else { + //the max width has been reached + break; + } + } + + //loop through the images for the row and apply the styles + for (var j = 0; j < row.images.length; j++) { + var bottomMargin = margin; + //make the margin 0 for the last one + if (j === (row.images.length - 1)) { + margin = 0; + } + this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin); + } + + if (row.images.length === 1 && totalRemaining > 1) { + //if there's only one image on the row and there are more images remaining, set the container to max width + row.images[0].style.width = maxRowWidth + "px"; + } + + + return row; + }, + + /** Returns the maximum image scaling height for the current image collection */ + getMaxScaleableHeight: function (imgs, maxRowHeight) { + + var smallestHeight = _.min(imgs, function (item) { return item.originalHeight; }).originalHeight; + + //adjust the smallestHeight if it is larger than the static max row height + if (smallestHeight > maxRowHeight) { + smallestHeight = maxRowHeight; + } + return smallestHeight; + }, + + /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */ + buildGrid: function (images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin, imagesOnly) { + + var rows = []; + var imagesProcessed = 0; + + //first fill in all of the original image sizes and URLs + for (var i = startingIndex; i < images.length; i++) { + var item = images[i]; + + this.setImageData(item); + this.setOriginalSize(item, maxRowHeight); + + if (imagesOnly && !item.isFolder && !item.thumbnail) { + images.splice(i, 1); + i--; + } + } + + while ((imagesProcessed + startingIndex) < images.length) { + //get the maxHeight for the current un-processed images + var currImgs = images.slice(imagesProcessed); + + //build the row + var remaining = images.length - imagesProcessed; + var row = this.buildRow(currImgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, remaining); + if (row.images.length > 0) { + rows.push(row); + imagesProcessed += row.images.length; + } + else { + + if (currImgs.length > 0) { + throw "Could not fill grid with all images, images remaining: " + currImgs.length; + } + + //if there was nothing processed, exit + break; + } + } + + return rows; } - } - var heightProp = _.find(img.properties, function (v) { return (v.alias === "umbracoHeight"); }); - if (heightProp && heightProp.value) { - img.originalHeight = parseInt(heightProp.value, 10); - if (isNaN(img.originalHeight)) { - img.originalHeight = maxHeight; - } - } - }, - - /** sets the image style which get's used in the angular markup */ - setImageStyle: function (img, width, height, rightMargin, bottomMargin) { - img.style = { width: width + "px", height: height + "px", "margin-right": rightMargin + "px", "margin-bottom": bottomMargin + "px" }; - img.thumbStyle = { - "background-image": "url('" + img.thumbnail + "')", - "background-repeat": "no-repeat", - "background-position": "center", - "background-size": Math.min(width, img.originalWidth) + "px " + Math.min(height, img.originalHeight) + "px" - }; - }, - - /** gets the image's scaled wdith based on the max row height */ - getScaledWidth: function (img, maxHeight) { - var scaled = img.originalWidth * maxHeight / img.originalHeight; - return scaled; - //round down, we don't want it too big even by half a pixel otherwise it'll drop to the next row - //return Math.floor(scaled); - }, - - /** returns the target row width taking into account how many images will be in the row and removing what the margin is */ - getTargetWidth: function (imgsPerRow, maxRowWidth, margin) { - //take into account the margin, we will have 1 less margin item than we have total images - return (maxRowWidth - ((imgsPerRow - 1) * margin)); - }, - - /** - This will determine the row/image height for the next collection of images which takes into account the - ideal image count per row. It will check if a row can be filled with this ideal count and if not - if there - are additional images available to fill the row it will keep calculating until they fit. - - It will return the calculated height and the number of images for the row. - - targetHeight = optional; - */ - getRowHeightForImages: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, targetHeight) { - - var idealImages = imgs.slice(0, idealImgPerRow); - //get the target row width without margin - var targetRowWidth = this.getTargetWidth(idealImages.length, maxRowWidth, margin); - //this gets the image with the smallest height which equals the maximum we can scale up for this image block - var maxScaleableHeight = this.getMaxScaleableHeight(idealImages, maxRowHeight); - //if the max scale height is smaller than the min display height, we'll use the min display height - targetHeight = targetHeight !== undefined ? targetHeight : Math.max(maxScaleableHeight, minDisplayHeight); - - var attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); - - if (attemptedRowHeight != null) { - - //if this is smaller than the min display then we need to use the min display, - // which means we'll need to remove one from the row so we can scale up to fill the row - if (attemptedRowHeight < minDisplayHeight) { - - if (idealImages.length > 1) { - - //we'll generate a new targetHeight that is halfway between the max and the current and recurse, passing in a new targetHeight - targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin, targetHeight); - } - else { - //this will occur when we only have one image remaining in the row but it's still going to be too wide even when - // using the minimum display height specified. In this case we're going to have to just crop the image in it's center - // using the minimum display height and the full row width - return { height: minDisplayHeight, imgCount: 1 }; - } - } - else { - //success! - return { height: attemptedRowHeight, imgCount: idealImages.length }; - } - } - - //we know the width will fit in a row, but we now need to figure out if we can fill - // the entire row in the case that we have more images remaining than the idealImgPerRow. - - if (idealImages.length === imgs.length) { - //we have no more remaining images to fill the space, so we'll just use the calc height - return { height: targetHeight, imgCount: idealImages.length }; - } - else if (idealImages.length === 1) { - //this will occur when we only have one image remaining in the row to process but it's not really going to fit ideally - // in the row. - return { height: minDisplayHeight, imgCount: 1 }; - } - else if (idealImages.length === idealImgPerRow && targetHeight < maxRowHeight) { - - //if we're already dealing with the ideal images per row and it's not quite wide enough, we can scale up a little bit so - // long as the targetHeight is currently less than the maxRowHeight. The scale up will be half-way between our current - // target height and the maxRowHeight (we won't loop forever though - if there's a difference of 5 px we'll just quit) - - while (targetHeight < maxRowHeight && (maxRowHeight - targetHeight) > 5) { - targetHeight += Math.floor((maxRowHeight - targetHeight) / 2); - attemptedRowHeight = this.performGetRowHeight(idealImages, targetRowWidth, minDisplayHeight, targetHeight); - if (attemptedRowHeight != null) { - //success! - return { height: attemptedRowHeight, imgCount: idealImages.length }; - } - } - - //Ok, we couldn't actually scale it up with the ideal row count we'll just recurse with a lesser image count. - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow - 1, margin); - } - else if (targetHeight === maxRowHeight) { - - //This is going to happen when: - // * We can fit a list of images in a row, but they come up too short (based on minDisplayHeight) - // * Then we'll try to remove an image, but when we try to scale to fit, the width comes up too narrow but the images are already at their - // maximum height (maxRowHeight) - // * So we're stuck, we cannot precicely fit the current list of images, so we'll render a row that will be max height but won't be wide enough - // which is better than rendering a row that is shorter than the minimum since that could be quite small. - - return { height: targetHeight, imgCount: idealImages.length }; - } - else { - - //we have additional images so we'll recurse and add 1 to the idealImgPerRow until it fits - return this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow + 1, margin); - } - - }, - - performGetRowHeight: function (idealImages, targetRowWidth, minDisplayHeight, targetHeight) { - - var currRowWidth = 0; - - for (var i = 0; i < idealImages.length; i++) { - var scaledW = this.getScaledWidth(idealImages[i], targetHeight); - currRowWidth += scaledW; - } - - if (currRowWidth > targetRowWidth) { - //get the new scaled height to fit - var newHeight = targetRowWidth * targetHeight / currRowWidth; - - return newHeight; - } - else if (idealImages.length === 1 && (currRowWidth <= targetRowWidth) && !idealImages[0].isFolder) { - //if there is only one image, then return the target height - return targetHeight; - } - else if (currRowWidth / targetRowWidth > 0.90) { - //it's close enough, it's at least 90% of the width so we'll accept it with the target height - return targetHeight; - } - else { - //if it's not successful, return null - return null; - } - }, - - /** builds an image grid row */ - buildRow: function (imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, totalRemaining) { - var currRowWidth = 0; - var row = { images: [] }; - - var imageRowHeight = this.getRowHeightForImages(imgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin); - var targetWidth = this.getTargetWidth(imageRowHeight.imgCount, maxRowWidth, margin); - - var sizes = []; - //loop through the images we know fit into the height - for (var i = 0; i < imageRowHeight.imgCount; i++) { - //get the lower width to ensure it always fits - var scaledWidth = Math.floor(this.getScaledWidth(imgs[i], imageRowHeight.height)); - - if (currRowWidth + scaledWidth <= targetWidth) { - currRowWidth += scaledWidth; - sizes.push({ - width: scaledWidth, - //ensure that the height is rounded - height: Math.round(imageRowHeight.height) - }); - row.images.push(imgs[i]); - } - else if (imageRowHeight.imgCount === 1 && row.images.length === 0) { - //the image is simply too wide, we'll crop/center it - sizes.push({ - width: maxRowWidth, - //ensure that the height is rounded - height: Math.round(imageRowHeight.height) - }); - row.images.push(imgs[i]); - } - else { - //the max width has been reached - break; - } - } - - //loop through the images for the row and apply the styles - for (var j = 0; j < row.images.length; j++) { - var bottomMargin = margin; - //make the margin 0 for the last one - if (j === (row.images.length - 1)) { - margin = 0; - } - this.setImageStyle(row.images[j], sizes[j].width, sizes[j].height, margin, bottomMargin); - } - - if (row.images.length === 1 && totalRemaining > 1) { - //if there's only one image on the row and there are more images remaining, set the container to max width - row.images[0].style.width = maxRowWidth + "px"; - } - - - return row; - }, - - /** Returns the maximum image scaling height for the current image collection */ - getMaxScaleableHeight: function (imgs, maxRowHeight) { - - var smallestHeight = _.min(imgs, function (item) { return item.originalHeight; }).originalHeight; - - //adjust the smallestHeight if it is larger than the static max row height - if (smallestHeight > maxRowHeight) { - smallestHeight = maxRowHeight; - } - return smallestHeight; - }, - - /** Creates the image grid with calculated widths/heights for images to fill the grid nicely */ - buildGrid: function (images, maxRowWidth, maxRowHeight, startingIndex, minDisplayHeight, idealImgPerRow, margin, imagesOnly) { - - var rows = []; - var imagesProcessed = 0; - - //first fill in all of the original image sizes and URLs - for (var i = startingIndex; i < images.length; i++) { - var item = images[i]; - - this.setImageData(item); - this.setOriginalSize(item, maxRowHeight); - - if (imagesOnly && !item.isFolder && !item.thumbnail) { - images.splice(i, 1); - i--; - } - } - - while ((imagesProcessed + startingIndex) < images.length) { - //get the maxHeight for the current un-processed images - var currImgs = images.slice(imagesProcessed); - - //build the row - var remaining = images.length - imagesProcessed; - var row = this.buildRow(currImgs, maxRowHeight, minDisplayHeight, maxRowWidth, idealImgPerRow, margin, remaining); - if (row.images.length > 0) { - rows.push(row); - imagesProcessed += row.images.length; - } - else { - - if (currImgs.length > 0) { - throw "Could not fill grid with all images, images remaining: " + currImgs.length; - } - - //if there was nothing processed, exit - break; - } - } - - return rows; - } - }; + }; } angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolderHelper); @@ -428,40 +452,40 @@ angular.module("umbraco.services").factory("umbPhotoFolderHelper", umbPhotoFolde */ function umbModelMapper() { - return { + return { - /** - * @ngdoc function - * @name umbraco.services.umbModelMapper#convertToEntityBasic - * @methodOf umbraco.services.umbModelMapper - * @function - * - * @description - * Converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model. - * @param {Object} source The source model - * @param {Number} source.id The node id of the model - * @param {String} source.name The node name - * @param {String} source.icon The models icon as a css class (.icon-doc) - * @param {Number} source.parentId The parentID, if no parent, set to -1 - * @param {path} source.path comma-separated string of ancestor IDs (-1,1234,1782,1234) - */ + /** + * @ngdoc function + * @name umbraco.services.umbModelMapper#convertToEntityBasic + * @methodOf umbraco.services.umbModelMapper + * @function + * + * @description + * Converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model. + * @param {Object} source The source model + * @param {Number} source.id The node id of the model + * @param {String} source.name The node name + * @param {String} source.icon The models icon as a css class (.icon-doc) + * @param {Number} source.parentId The parentID, if no parent, set to -1 + * @param {path} source.path comma-separated string of ancestor IDs (-1,1234,1782,1234) + */ - /** This converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model */ - convertToEntityBasic: function (source) { - var required = ["id", "name", "icon", "parentId", "path"]; - _.each(required, function (k) { - if (!_.has(source, k)) { - throw "The source object does not contain the property " + k; + /** This converts the source model to a basic entity model, it will throw an exception if there isn't enough data to create the model */ + convertToEntityBasic: function (source) { + var required = ["id", "name", "icon", "parentId", "path"]; + _.each(required, function (k) { + if (!_.has(source, k)) { + throw "The source object does not contain the property " + k; + } + }); + var optional = ["metaData", "key", "alias"]; + //now get the basic object + var result = _.pick(source, required.concat(optional)); + return result; } - }); - var optional = ["metaData", "key", "alias"]; - //now get the basic object - var result = _.pick(source, required.concat(optional)); - return result; - } - }; + }; } angular.module('umbraco.services').factory('umbModelMapper', umbModelMapper); @@ -476,21 +500,21 @@ angular.module('umbraco.services').factory('umbModelMapper', umbModelMapper); */ function umbSessionStorage($window) { - //gets the sessionStorage object if available, otherwise just uses a normal object - // - required for unit tests. - var storage = $window['sessionStorage'] ? $window['sessionStorage'] : {}; + //gets the sessionStorage object if available, otherwise just uses a normal object + // - required for unit tests. + var storage = $window['sessionStorage'] ? $window['sessionStorage'] : {}; - return { + return { - get: function (key) { - return angular.fromJson(storage["umb_" + key]); - }, + get: function (key) { + return angular.fromJson(storage["umb_" + key]); + }, - set: function (key, value) { - storage["umb_" + key] = angular.toJson(value); - } + set: function (key, value) { + storage["umb_" + key] = angular.toJson(value); + } - }; + }; } angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorage); @@ -503,28 +527,28 @@ angular.module('umbraco.services').factory('umbSessionStorage', umbSessionStorag * used to check for updates and display a notifcation */ function updateChecker($http, umbRequestHelper) { - return { + return { - /** - * @ngdoc function - * @name umbraco.services.updateChecker#check - * @methodOf umbraco.services.updateChecker - * @function - * - * @description - * Called to load in the legacy tree js which is required on startup if a user is logged in or - * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. - */ - check: function () { + /** + * @ngdoc function + * @name umbraco.services.updateChecker#check + * @methodOf umbraco.services.updateChecker + * @function + * + * @description + * Called to load in the legacy tree js which is required on startup if a user is logged in or + * after login, but cannot be called until they are authenticated which is why it needs to be lazy loaded. + */ + check: function () { - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "updateCheckApiBaseUrl", - "GetCheck")), - 'Failed to retrieve update status'); - } - }; + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "updateCheckApiBaseUrl", + "GetCheck")), + 'Failed to retrieve update status'); + } + }; } angular.module('umbraco.services').factory('updateChecker', updateChecker); @@ -534,43 +558,43 @@ angular.module('umbraco.services').factory('updateChecker', updateChecker); * @description A helper object used for property editors **/ function umbPropEditorHelper() { - return { - /** - * @ngdoc function - * @name getImagePropertyValue - * @methodOf umbraco.services.umbPropertyEditorHelper - * @function - * - * @description - * Returns the correct view path for a property editor, it will detect if it is a full virtual path but if not then default to the internal umbraco one - * - * @param {string} input the view path currently stored for the property editor - */ - getViewPath: function (input, isPreValue) { - var path = String(input); + return { + /** + * @ngdoc function + * @name getImagePropertyValue + * @methodOf umbraco.services.umbPropertyEditorHelper + * @function + * + * @description + * Returns the correct view path for a property editor, it will detect if it is a full virtual path but if not then default to the internal umbraco one + * + * @param {string} input the view path currently stored for the property editor + */ + getViewPath: function (input, isPreValue) { + var path = String(input); - if (path.startsWith('/')) { + if (path.startsWith('/')) { - //This is an absolute path, so just leave it - return path; - } else { + //This is an absolute path, so just leave it + return path; + } else { - if (path.indexOf("/") >= 0) { - //This is a relative path, so just leave it - return path; - } else { - if (!isPreValue) { - //i.e. views/propertyeditors/fileupload/fileupload.html - return "views/propertyeditors/" + path + "/" + path + ".html"; - } else { - //i.e. views/prevalueeditors/requiredfield.html - return "views/prevalueeditors/" + path + ".html"; - } + if (path.indexOf("/") >= 0) { + //This is a relative path, so just leave it + return path; + } else { + if (!isPreValue) { + //i.e. views/propertyeditors/fileupload/fileupload.html + return "views/propertyeditors/" + path + "/" + path + ".html"; + } else { + //i.e. views/prevalueeditors/requiredfield.html + return "views/prevalueeditors/" + path + ".html"; + } + } + + } } - - } - } - }; + }; } angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorHelper); @@ -581,24 +605,24 @@ angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorH **/ function queryStrings($window) { - var pl = /\+/g; // Regex for replacing addition symbol with a space - var search = /([^&=]+)=?([^&]*)/g; - var decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }; + var pl = /\+/g; // Regex for replacing addition symbol with a space + var search = /([^&=]+)=?([^&]*)/g; + var decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }; - return { - - getParams: function () { - var match; - var query = $window.location.search.substring(1); + return { - var urlParams = {}; - while (match = search.exec(query)) { - urlParams[decode(match[1])] = decode(match[2]); - } + getParams: function () { + var match; + var query = $window.location.search.substring(1); - return urlParams; - } - }; + var urlParams = {}; + while (match = search.exec(query)) { + urlParams[decode(match[1])] = decode(match[2]); + } + + return urlParams; + } + }; } angular.module('umbraco.services').factory('queryStrings', queryStrings); diff --git a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js index dc39678a2f..b4536f0e35 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/main.controller.js @@ -8,7 +8,7 @@ * The main application controller * */ -function MainController($scope, $rootScope, $location, $routeParams, $timeout, $http, $log, appState, treeService, notificationsService, userService, navigationService, historyService, updateChecker, assetsService, eventsService, umbRequestHelper, tmhDynamicLocale, localStorageService) { +function MainController($scope, $rootScope, $location, $routeParams, $timeout, $http, $log, appState, treeService, notificationsService, userService, navigationService, historyService, updateChecker, assetsService, eventsService, umbRequestHelper, tmhDynamicLocale, localStorageService, tourService) { //the null is important because we do an explicit bool check on this in the view //the avatar is by default the umbraco logo @@ -136,6 +136,40 @@ function MainController($scope, $rootScope, $location, $routeParams, $timeout, $ }; })); + // manage the help dialog by subscribing to the showHelp appState + $scope.drawer = {}; + evts.push(eventsService.on("appState.drawerState.changed", function (e, args) { + // set view + if (args.key === "view") { + $scope.drawer.view = args.value; + } + // set custom model + if (args.key === "model") { + $scope.drawer.model = args.value; + } + // show / hide drawer + if (args.key === "showDrawer") { + $scope.drawer.show = args.value; + } + })); + + evts.push(eventsService.on("appState.tour.start", function (name, args) { + $scope.tour = args; + $scope.tour.show = true; + })); + + evts.push(eventsService.on("appState.tour.end", function () { + $scope.tour = null; + })); + + evts.push(eventsService.on("appState.tour.complete", function () { + $scope.tour = null; + })); + + evts.push(eventsService.on("appState.backdrop", function (name, args) { + $scope.backdrop = args; + })); + //ensure to unregister from all events! $scope.$on('$destroy', function () { for (var e in evts) { diff --git a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js index 57ddb6babd..fe1148d6a8 100644 --- a/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/controllers/search.controller.js @@ -14,7 +14,6 @@ function SearchController($scope, searchService, $log, $location, navigationServ $scope.isSearching = false; $scope.selectedResult = -1; - $scope.navigateResults = function (ev) { //38: up 40: down, 13: enter @@ -34,24 +33,37 @@ function SearchController($scope, searchService, $log, $location, navigationServ } }; - var group = undefined; + var groupNames = []; var groupIndex = -1; var itemIndex = -1; $scope.selectedItem = undefined; - + $scope.clearSearch = function () { + $scope.searchTerm = null; + }; function iterateResults(up) { //default group if (!group) { - group = $scope.groups[0]; + + for (var g in $scope.groups) { + if ($scope.groups.hasOwnProperty(g)) { + groupNames.push(g); + + } + } + + //Sorting to match the groups order + groupNames.sort(); + + group = $scope.groups[groupNames[0]]; groupIndex = 0; } if (up) { if (itemIndex === 0) { if (groupIndex === 0) { - gotoGroup($scope.groups.length - 1, true); + gotoGroup(Object.keys($scope.groups).length - 1, true); } else { gotoGroup(groupIndex - 1, true); } @@ -62,7 +74,7 @@ function SearchController($scope, searchService, $log, $location, navigationServ if (itemIndex < group.results.length - 1) { gotoItem(itemIndex + 1); } else { - if (groupIndex === $scope.groups.length - 1) { + if (groupIndex === Object.keys($scope.groups).length - 1) { gotoGroup(0); } else { gotoGroup(groupIndex + 1); @@ -73,7 +85,7 @@ function SearchController($scope, searchService, $log, $location, navigationServ function gotoGroup(index, up) { groupIndex = index; - group = $scope.groups[groupIndex]; + group = $scope.groups[groupNames[groupIndex]]; if (up) { gotoItem(group.results.length - 1); @@ -95,6 +107,13 @@ function SearchController($scope, searchService, $log, $location, navigationServ $scope.hasResults = false; if ($scope.searchTerm) { if (newVal !== null && newVal !== undefined && newVal !== oldVal) { + + //Resetting for brand new search + group = undefined; + groupNames = []; + groupIndex = -1; + itemIndex = -1; + $scope.isSearching = true; navigationService.showSearch(); $scope.selectedItem = undefined; @@ -114,7 +133,7 @@ function SearchController($scope, searchService, $log, $location, navigationServ var filtered = {}; _.each(result, function (value, key) { if (value.results.length > 0) { - filtered[key] = value; + filtered[key] = value; } }); $scope.groups = filtered; diff --git a/src/Umbraco.Web.UI.Client/src/init.js b/src/Umbraco.Web.UI.Client/src/init.js index 631e38657b..e86fa25c42 100644 --- a/src/Umbraco.Web.UI.Client/src/init.js +++ b/src/Umbraco.Web.UI.Client/src/init.js @@ -1,7 +1,7 @@ /** Executed when the application starts, binds to events and set global state */ -app.run(['userService', '$log', '$rootScope', '$location', 'queryStrings', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', - function (userService, $log, $rootScope, $location, queryStrings, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService) { - +app.run(['userService', '$log', '$rootScope', '$location', 'queryStrings', 'navigationService', 'appState', 'editorState', 'fileManager', 'assetsService', 'eventsService', '$cookies', '$templateCache', 'localStorageService', 'tourService', 'dashboardResource', + function (userService, $log, $rootScope, $location, queryStrings, navigationService, appState, editorState, fileManager, assetsService, eventsService, $cookies, $templateCache, localStorageService, tourService, dashboardResource) { + //This sets the default jquery ajax headers to include our csrf token, we // need to user the beforeSend method because our token changes per user/login so // it cannot be static @@ -18,14 +18,34 @@ app.run(['userService', '$log', '$rootScope', '$location', 'queryStrings', 'navi eventsService.on("app.authenticated", function(evt, data) { assetsService._loadInitAssets().then(function() { - appState.setGlobalState("isReady", true); + + //Register all of the tours on the server + tourService.registerAllTours().then(function () { + appReady(data); + + // Auto start intro tour + tourService.getTourByAlias("umbIntroIntroduction").then(function (introTour) { + // start intro tour if it hasn't been completed or disabled + if (introTour && introTour.disabled !== true && introTour.completed !== true) { + tourService.startTour(introTour); + } + }); + + }, function(){ + appReady(data); + }); - //send the ready event with the included returnToPath,returnToSearch data - eventsService.emit("app.ready", data); - returnToPath = null, returnToSearch = null; }); + }); + function appReady(data) { + appState.setGlobalState("isReady", true); + //send the ready event with the included returnToPath,returnToSearch data + eventsService.emit("app.ready", data); + returnToPath = null, returnToSearch = null; + } + /** execute code on each successful route */ $rootScope.$on('$routeChangeSuccess', function(event, current, previous) { @@ -96,4 +116,5 @@ app.run(['userService', '$log', '$rootScope', '$location', 'queryStrings', 'navi //var touchDevice = ("ontouchstart" in window || window.touch || window.navigator.msMaxTouchPoints === 5 || window.DocumentTouch && document instanceof DocumentTouch); var touchDevice = /android|webos|iphone|ipad|ipod|blackberry|iemobile|touch/i.test(navigator.userAgent.toLowerCase()); appState.setGlobalState("touchDevice", touchDevice); + }]); diff --git a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js index 54b4733529..7d586cae82 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/installer.service.js +++ b/src/Umbraco.Web.UI.Client/src/installer/installer.service.js @@ -17,7 +17,7 @@ angular.module("umbraco.install").factory('installerService', function($rootScop //add to umbraco installer facts here var facts = ['Umbraco helped millions of people watch a man jump from the edge of space', - 'Over 420 000 websites are currently powered by Umbraco', + 'Over 440 000 websites are currently powered by Umbraco', "At least 2 people have named their cat 'Umbraco'", 'On an average day, more than 1000 people download Umbraco', 'umbraco.tv is the premier source of Umbraco video tutorials to get you started', @@ -31,10 +31,10 @@ angular.module("umbraco.install").factory('installerService', function($rootScop "At least 4 people have the Umbraco logo tattooed on them", "'Umbraco' is the danish name for an allen key", "Umbraco has been around since 2005, that's a looong time in IT", - "More than 550 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", + "More than 600 people from all over the world meet each year in Denmark in June for our annual conference CodeGarden", "While you are installing Umbraco someone else on the other side of the planet is probably doing it too", "You can extend Umbraco without modifying the source code using either JavaScript or C#", - "Umbraco was installed in more than 165 countries in 2015" + "Umbraco has been installed in more than 198 countries" ]; /** diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js index 297db6ac4a..10c0d596eb 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.controller.js @@ -1,7 +1,7 @@ angular.module("umbraco.install").controller("Umbraco.Install.UserController", function($scope, installerService) { $scope.passwordPattern = /.*/; - $scope.installer.current.model.subscribeToNewsLetter = true; + $scope.installer.current.model.subscribeToNewsLetter = false; if ($scope.installer.current.model.minNonAlphaNumericLength > 0) { var exp = ""; diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html index 8c0989457d..8fbe4263e8 100644 --- a/src/Umbraco.Web.UI.Client/src/installer/steps/user.html +++ b/src/Umbraco.Web.UI.Client/src/installer/steps/user.html @@ -18,7 +18,7 @@
      - + Your email will be used as your login
      diff --git a/src/Umbraco.Web.UI.Client/src/less/application/grid.less b/src/Umbraco.Web.UI.Client/src/less/application/grid.less index f6879bb679..2671adba32 100644 --- a/src/Umbraco.Web.UI.Client/src/less/application/grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/application/grid.less @@ -40,11 +40,15 @@ body { } #mainwrapper { - height: 100%; - width: 100%; + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; margin: 0; } +body.umb-drawer-is-visible #mainwrapper{ + left: @drawerWidth; +} + #contentwrapper, #contentcolumn { position: absolute; top: 0px; bottom: 0px; right: 0px; left: 80px; @@ -83,7 +87,8 @@ body { top: 0; bottom: 0; position: absolute; - text-align: center + text-align: center; + box-shadow: -10px 0px 25px rgba(0, 0, 0, 0.3) } #applications-tray { diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 44365b95e7..d2a80d93aa 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -81,6 +81,10 @@ @import "forms/umb-validation-label.less"; // Umbraco Components +@import "components/application/umb-tour.less"; +@import "components/application/umb-backdrop.less"; +@import "components/application/umb-drawer.less"; + @import "components/editor.less"; @import "components/overlays.less"; @import "components/card.less"; @@ -125,6 +129,8 @@ @import "components/umb-checkmark.less"; @import "components/umb-list.less"; @import "components/umb-box.less"; +@import "components/umb-number-badge.less"; +@import "components/umb-progress-circle.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; @@ -167,3 +173,6 @@ @import "hacks.less"; @import "healthcheck.less"; + +// cleanup properties.less when it is done +@import "properties.less"; \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/buttons.less b/src/Umbraco.Web.UI.Client/src/less/buttons.less index e76ffc682c..1eaf285119 100644 --- a/src/Umbraco.Web.UI.Client/src/less/buttons.less +++ b/src/Umbraco.Web.UI.Client/src/less/buttons.less @@ -261,6 +261,9 @@ input[type="submit"].btn { *padding-top: 1px; *padding-bottom: 1px; } + + // Safari defaults to 1px for input. Ref U4-7721. + margin: 0px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-backdrop.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-backdrop.less new file mode 100644 index 0000000000..45e73df6da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-backdrop.less @@ -0,0 +1,30 @@ +.umb-backdrop { + height: 100%; + width: 100%; + position: fixed; + z-index: 9999; + top: 0; + left: 0; + pointer-events: none; +} + +.umb-backdrop__backdrop { + height: 100%; + width: 100%; +} + +.umb-backdrop__rect { + position: absolute; + pointer-events: all; + margin: 0; + width: 100%; + height: 100%; + background: @black; + opacity: 0.4; + transition: 200ms opacity ease-in-out; +} + +.umb-backdrop__highlight-prevent-click { + position: absolute; + pointer-events: all; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less new file mode 100644 index 0000000000..0fae291e72 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-drawer.less @@ -0,0 +1,194 @@ +.umb-drawer { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 10; + width: @drawerWidth; + box-shadow: 0 0 20px rgba(0,0,0,0.19), 0 0 6px rgba(0,0,0,0.23); + background: @gray-9; +} + +.umb-drawer-view { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* Header */ + +.umb-drawer-header { + flex: 0 0 100px; + padding: 20px 30px; + box-sizing: border-box; +} + +.umb-drawer-header__title { + font-size: @fontSizeLarge; + font-weight: bold; + margin-top: 7px; + margin-bottom: 7px; +} + +.umb-drawer-header__subtitle { + font-size: @fontSizeSmall; +} + +/* Content */ + +.umb-drawer-content { + flex: 1 1 auto; + overflow-y: auto; + overflow-x: hidden; + padding: 0 30px 20px 30px; +} + +/* Footer */ +.umb-drawer-footer { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 31px; + padding: 15px 30px; +} + +/* Our badge - should be moved */ + +.umb-help-badge { + padding: 10px 20px 10px 35px; + background: @white; + position: relative; + overflow: hidden; + border-radius: 3px; + display: block; +} + +.umb-help-badge:hover, +.umb-help-badge:active, +.umb-help-badge:focus { + text-decoration: none; + + .umb-help-badge__title { + text-decoration: underline !important; + } +} + +.umb-help-badge__icon { + font-size: 40px; + transform: translate(0,-50%); + position: absolute; + left: -15px; + top: 50%; + color: @red-l3; +} + +.umb-help-badge__title { + font-size: 15px; + font-weight: bold; + color: @black; +} + +/* Help article */ + +.umb-help-article { + background: @white; + padding: 20px; + line-height: 1.4em; +} + +/* Make sure typography looks good */ +.umb-help-article h1, +.umb-help-article h2, +.umb-help-article h3, +.umb-help-article h4 { + line-height: 1.3em; + font-weight: bold; +} + +.umb-help-article h1 { font-size: 20px; } +.umb-help-article h2 { font-size: 16px; margin-top: 20px; } +.umb-help-article h3 { font-size: 15px; } +.umb-help-article h4 { font-size: 14px; } + +.umb-help-article ol li, +.umb-help-article ul li { + line-height: 1.4em; + margin-bottom: 8px; +} + +.umb-help-article code { + white-space: pre-wrap; + word-break: break-word; +} + +.umb-help-article-navigation { + margin-top: 25px; + display: flex; + justify-content: space-between; + align-items: center; +} + + +/* Help list */ + +.umb-help-list { + list-style: none; + margin-left: 0; + margin-bottom: 0; + background: @white; + border-radius: 3px; +} + +.umb-help-list:last-child { + border-bottom: none; +} + +.umb-help-list-item { + margin-bottom: 1px; + border-radius: 0; + border-bottom: 1px solid @gray-9; +} + +.umb-help-list-item > a, +.umb-help-list-item__content { + display: flex; + align-items: center; + padding: 10px 20px; +} + +.umb-help-list-item > a:hover, +.umb-help-list-item > a:focus, +.umb-help-list-item > a:active { + text-decoration: none; + + .umb-help-list-item__title { + text-decoration: underline !important; + } +} + +.umb-help-list-item__title { + font-size: 14px; + display: block; +} + +.umb-help-list-item__description { + margin-top: 5px; + display: block; + font-size: 14px; +} + +.umb-help-list-item__icon { + margin-right: 8px; + color: @gray-4; + font-size: 18px; +} + +.umb-help-list-item__open-icon { + font-size: 14px; + color: @gray-6; + margin-left: auto; +} + +.umb-help-list-item:hover .umb-help-list-item__group-title { + text-decoration: underline; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less new file mode 100644 index 0000000000..a97c700935 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/application/umb-tour.less @@ -0,0 +1,102 @@ +.umb-tour__loader { + background: @white; + z-index: 10000; + position: fixed; + height: 5px; +} + +.umb-tour__pulse { + position: fixed; + z-index: 10000; + display: none; + background: transparent; + box-shadow: 0 0 0 @green inset; + animation: pulse 2s infinite; + pointer-events: none; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 @green inset; + } + 70% { + box-shadow: 0 0 0 5px fade(@green, 80%) inset; + } + 100% { + box-shadow: 0 0 0 0 @green inset; + } +} + +.umb-tour__popover { + position: fixed; + background: @white; + border-radius: @baseBorderRadius; + z-index: 10000; + width: 320px; + max-width: 100%; + box-sizing: border-box; + padding: 15px; + + h1, h2, h3, h4, h5 { + font-weight: bold; + color: @black; + } +} + +.umb-tour__popover--l { + padding: 30px; + width: 500px; + + .umb-tour-step__header { + margin-bottom: 30px; + margin-top: 10px; + } + + .umb-tour-step__title { + font-size: 20px; + } + + .umb-tour-step__content { + margin-bottom: 25px; + font-size: 15px; + } +} + +.umb-tour-step__counter { + font-size: 13px; + color: @gray-5; +} + +.umb-tour-step__close { + position: absolute; + top: 15px; + right: 15px; + font-size: 19px; + color: @gray-7; + cursor: pointer; +} + +.umb-tour-step__close:hover, +.umb-tour-step__close:active { + color: @gray-4; + text-decoration: none; +} + +.umb-tour-step__header { + margin-bottom: 10px; + margin-top: 10px; +} + +.umb-tour-step__title { + font-weight: bold; + color: @black; + font-size: 15px; + line-height: 1.3em; + width: calc(~"100% - 35px"); +} + +.umb-tour-step__content { + margin-bottom: 15px; + font-size: 14px; + line-height: 1.6em; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less index 1bd896022c..9762eaf058 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button-group.less @@ -1,6 +1,7 @@ .umb-button-group__toggle { padding-left: 8px; padding-right: 8px; + float: none; } .umb-button-group__sub-buttons.-align-right { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less index 82e3afbb83..ab8b6b0671 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/buttons/umb-button.less @@ -1,7 +1,6 @@ .umb-button { position: relative; - overflow: hidden; - display: inline; + display: inline-block; } .umb-button__button:focus { @@ -100,6 +99,11 @@ } /* Sizes */ +.umb-button--xxs { + padding: 2px 10px; + font-size: 13px; +} + .umb-button--xs { padding: 5px 16px; font-size: 14px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index 68d3b20cab..0906b513a6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -2,7 +2,7 @@ position: fixed; overflow: hidden; background: @white; - z-index: 7500; + z-index: @zindexUmbOverlay; animation: fadeIn 0.2s; box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23); } @@ -136,6 +136,11 @@ margin-left: 81px; } +// push left overlay when drawer is open +.umb-drawer-is-visible .umb-overlay.umb-overlay-left { + left: @drawerWidth; +} + .umb-overlay.umb-overlay-left .umb-overlay-header { flex-basis: 100px; padding: 20px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-overlay-backdrop.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-overlay-backdrop.less index a05e9c8f95..65b3f34c2d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-overlay-backdrop.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays/umb-overlay-backdrop.less @@ -3,8 +3,12 @@ width: 100%; height: 100%; background-color: rgba(255, 255, 255, 0.50); - z-index: 2000; + z-index: @zindexOverlayBackdrop; top: 0; left: 0; animation: fadeIn 0.2s; } + +.umb-drawer-is-visible .umb-overlay-backdrop { + left: @drawerWidth; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less index 2807c2d427..de746090b9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-avatar.less @@ -6,6 +6,7 @@ display: flex; align-items: center; justify-content: center; + margin: 0 auto; color: @black; font-weight: bold; font-size: 16px; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less index 522b7564c1..015aa607c9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-badge.less @@ -17,11 +17,16 @@ border-color: @turquoise; } -.umb-badge--seconday { +.umb-badge--secondary { background-color: @purple-washed; border-color: @purple; } +.umb-badge--gray { + background-color: @gray-10; + border-color: @gray-8; +} + .umb-badge--danger { background-color: @red-washed; border-color: @red; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less index d25744a108..47ed97fde2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-breadcrumbs.less @@ -35,3 +35,10 @@ margin-right: 5px; color: @gray-7; } + +input.umb-breadcrumbs__add-ancestor { + height: 25px; + margin-top: -2px; + margin-left: 3px; + width: 100px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less index b9f2731e51..9c7eb7d7b2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation.less @@ -6,9 +6,10 @@ .umb-sub-views-nav-item { text-align: center; - margin-left: 20px; + //margin-left: 20px; cursor: pointer; display: block; + padding: 5px 10px; } .umb-sub-views-nav-item:focus { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-empty-state.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-empty-state.less index a9ded358fa..9173344112 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-empty-state.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-empty-state.less @@ -6,7 +6,7 @@ } .umb-empty-state.-small { - font-size: @fontSizeSmall; + font-size: 14px; } .umb-empty-state.-large { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 3551d9d31a..5cc4817142 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -280,7 +280,6 @@ .umb-grid .umb-control { position: relative; display: block; - overflow: hidden; margin-left: 10px; margin-right: 10px; margin-bottom: 10px; @@ -535,7 +534,7 @@ color: @gray-3; } -.umb-grid .umb-cell-rte textarea { +.umb-grid .umb-cell-rte textarea.mceNoEditor { display: none !important; } @@ -636,9 +635,19 @@ clear: both; } -.umb-grid .mce-btn button { - padding: 8px 6px; - line-height: inherit; +.umb-grid .mce-btn { + button { + padding-top: 8px; + padding-bottom: 8px; + padding-left: 6px; + line-height: inherit; + } + + &:not(.mce-menubtn) { + button { + padding-right: 6px; + } + } } .umb-grid .mce-toolbar { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index dfd2761643..eecbf51ec5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -11,7 +11,6 @@ margin: 50px 0 0 0; border: 2px solid @gray-7; border-radius: 0 10px 10px 10px; - position: relative; padding: 10px 10px 5px 10px; box-sizing: border-box; } @@ -34,6 +33,7 @@ border: 1px dashed @gray-8; color: @turquoise-d1; font-weight: bold; + position: relative; } .umb-group-builder__group.-sortable { @@ -67,16 +67,18 @@ } .umb-group-builder__group-title-wrapper { - position: absolute; - left: -2px; - top: -45px; display: flex; align-items: center; + margin-left: -12px; + margin-top: -55px; } .umb-group-builder__group-title-wrapper.-placeholder { + position: absolute; left: -1px; top: -44px; + margin-left: 0; + margin-top: 0; } .umb-group-builder__group-title { @@ -424,103 +426,114 @@ input.umb-group-builder__group-title-input { .content-type-editor-dialog.edit-property-settings { - .validation-wrapper { - position: relative; - } - - .validation-label { - position: absolute; - top: 50%; - right: 0; - font-size: 12px; - color: @red; - transform: translate(0, -50%); - } - - textarea.editor-label { - border-color:transparent; - box-shadow: none; - width: 100%; - box-sizing: border-box; - margin-bottom: 0; - font-size: 16px; - font-weight: bold; - resize: none; - line-height: 1.5em; - padding-left: 0; - border: none; - &:focus { - outline: none; - box-shadow: none !important; + .validation-wrapper { + position: relative; } - } - .editor-placeholder { - border: 1px dashed @gray-8; - width: 100%; - height: 80px; - line-height: 80px; - text-align: center; - display: block; - border-radius: 5px; - color: @gray-3; - font-weight: bold; - font-size: 14px; - color: @turquoise-d1; - &:hover { - text-decoration: none; - } - } - - .editor { - margin-bottom: 10px; - .editor-icon-wrapper { - border: 1px solid @gray-8; - width: 60px; - height: 60px; - text-align: center; - line-height: 60px; - border-radius: 5px; - float: left; - margin-right: 20px; - .icon { - font-size: 26px; - } - } - .editor-details { - float: left; - margin-top: 10px; - .editor-name { - display: block; - font-weight: bold; - } - .editor-editor { - display: block; + .validation-label { + position: absolute; + top: 50%; + right: 0; font-size: 12px; - } + color: @red; + transform: translate(0, -50%); } - .editor-settings-icon { - font-size: 18px; - margin-top: 8px; - } - } - .checkbox { - margin-bottom: 20px; - } + textarea.editor-label { + border-color: transparent; + box-shadow: none; + width: 100%; + box-sizing: border-box; + margin-bottom: 0; + font-size: 16px; + font-weight: bold; + resize: none; + line-height: 1.5em; + padding-left: 0; + border: none; - .editor-description, - .editor-validation-pattern { - min-width: 100%; - min-height: 25px; - resize: none; - box-sizing: border-box; - border: none; - overflow: hidden; - } + &:focus { + outline: none; + box-shadow: none !important; + } + } - .umb-dropdown { - width: 100%; - } + .editor-placeholder { + border: 1px dashed @gray-8; + width: 100%; + height: 80px; + line-height: 80px; + text-align: center; + display: block; + border-radius: 5px; + color: @gray-3; + font-weight: bold; + font-size: 14px; + color: @turquoise-d1; + &:hover { + text-decoration: none; + } + } + + .editor { + margin-bottom: 10px; + + .editor-icon-wrapper { + border: 1px solid @gray-8; + width: 60px; + height: 60px; + text-align: center; + line-height: 60px; + border-radius: 5px; + float: left; + margin-right: 20px; + + .icon { + font-size: 26px; + } + } + + .editor-details { + float: left; + margin-top: 10px; + + .editor-name { + display: block; + font-weight: bold; + } + + .editor-editor { + display: block; + font-size: 12px; + } + } + + .editor-settings-icon { + font-size: 18px; + margin-top: 8px; + } + } + + .checkbox { + margin-bottom: 20px; + } + + .editor-description, + .editor-validation-pattern { + min-width: 100%; + min-height: 25px; + resize: none; + box-sizing: border-box; + border: none; + overflow: hidden; + } + + .umb-dropdown { + width: 100%; + } + + label.checkbox.no-indent { + width: 100%; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less index 6fe68ff490..dca263ea0c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less @@ -13,6 +13,11 @@ flex-direction: column; } +.umb-drawer-is-visible .umb-lightbox { + width: calc(~'100%' - ~'@{drawerWidth}'); + left: @drawerWidth; +} + .umb-lightbox__backdrop { position: absolute; top: 0; @@ -40,16 +45,13 @@ .umb-lightbox__images { position: relative; z-index: 1000; + max-width: calc(~'100%' - 200px); // subtract the width of the two arrow buttons } .umb-lightbox__image { background: @white; border-radius: 3px; padding: 10px; - img { - max-width: 80vw; - max-height: 80vh; - } } .umb-lightbox__control { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index 65d34ba693..9e038bd571 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -1,5 +1,5 @@ .umb-node-preview { - padding: 7px 0; + padding: 5px 0; display: flex; max-width: 66.6%; box-sizing: border-box; @@ -8,7 +8,6 @@ .umb-node-preview:last-of-type { border-bottom: none; - margin-bottom: 7px; } .umb-node-preview--sortable { @@ -37,6 +36,7 @@ .umb-node-preview__content { flex: 1 1 auto; margin-right: 25px; + overflow: hidden; } .umb-node-preview__name { @@ -50,6 +50,13 @@ color: @gray-3; } +.umb-node-preview__name, +.umb-node-preview__description { + /*text-overflow: ellipsis; + overflow: hidden;*/ + word-wrap: break-word; +} + .umb-node-preview__actions { flex: 0 0 auto; display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-number-badge.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-number-badge.less new file mode 100644 index 0000000000..3c2dbbdf20 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-number-badge.less @@ -0,0 +1,39 @@ +.umb-number-badge { + border: 1px solid @gray-6; + width: 25px; + height: 25px; + border-radius: 50%; + box-sizing: border-box; + display: flex; + justify-content: center; + color: @black; + font-size: 15px; +} + +.umb-number-badge--xs { + width: 20px; + height: 20px; + font-size: 13px; +} + +.umb-number-badge--s { + width: 25px; + height: 25px; +} + +.umb-number-badge--m { + width: 30px; + height: 30px; +} + +.umb-number-badge--l { + width: 40px; + height: 40px; + font-size: 18px; +} + +.umb-number-badge--xl { + width: 50px; + height: 50px; + font-size: 20px; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less new file mode 100644 index 0000000000..348d7bb5db --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-progress-circle.less @@ -0,0 +1,49 @@ +.umb-progress-circle { + position: relative; +} + +.umb-progress-circle__view-box { + position: absolute; + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + transform: rotate(-90deg); +} + +// circle highlight on progressbar +.umb-progress-circle__highlight { + stroke: @green; +} + +.umb-progress-circle__highlight--primary { + stroke: @turquoise; +} + +.umb-progress-circle__highlight--secondary { + stroke: @purple; +} + +.umb-progress-circle__highlight--success { + stroke: @green; +} + +.umb-progress-circle__highlight--warning { + stroke: @yellow; +} + +.umb-progress-circle__highlight--danger { + stroke: @red; +} + +// circle progressbar bg +.umb-progress-circle__bg { + stroke: @gray-8; +} + +// the text in the center +.umb-progress-circle__percentage { + font-size: 16px; + font-weight: bold; + text-align: center; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less index 40a85bec71..e79e474935 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/users/umb-user-cards.less @@ -11,6 +11,8 @@ flex: 0 0 100%; max-width: 100%; display: flex; + flex-wrap: wrap; + flex-direction: column; } .umb-user-card:hover, diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards.less b/src/Umbraco.Web.UI.Client/src/less/dashboards.less index 9d1f9cf39d..5fd0e25be1 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards.less @@ -5,21 +5,20 @@ top: -30px; padding-top: 30px; box-shadow: inset 0px -40px 30px 25px rgba(255,255,255,1); - border-radius: 0px 0px 200px 200px; - -moz-border-radius: 0px 0px 200px 200px; + -moz-border-radius: 0px 0px 200px 200px; -webkit-border-radius: 0px 0px 200px 200px; + border-radius: 0px 0px 200px 200px; small { font-size: 14px; opacity: .5; } - .umb-loader{ + .umb-loader { width: 640px; height: 4px; } - .video_player{ video { width: 100%; @@ -27,8 +26,8 @@ border: 1px solid @gray-9; border-left: none; border-bottom: none; + -moz-box-sizing:border-box; box-sizing:border-box; - -moz-box-sizing:border-box; } input[type="range"] { @@ -67,8 +66,8 @@ .progress-bar { display: block; - box-sizing: border-box; - -moz-box-sizing:border-box; + -moz-box-sizing:border-box; + box-sizing: border-box; max-width: 100%; width: 200px; height: 100%; @@ -112,26 +111,25 @@ line-height: 80px; text-align: center; position: relative; + display: flex; + justify-content: center; - i, h3 { + .icon, h3 { display: inline-block; } - i { - position: absolute; - top: 50%; - margin-top: -40px; + .icon { + font-size: 80px; } h3 { - margin: 0; + margin: 0 0 0 20px; line-height: 80px; font-weight: 700; - margin-left: 100px; font-size: 36px; letter-spacing: -1px; } } -} +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index b0557954e6..1a1f3bb93e 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -775,6 +775,8 @@ legend + .control-group { *padding-left: @horizontalComponentOffset; } } + + // Remove bottom margin on block level help text since that's accounted for on .control-group .help-block { margin-bottom: 0; @@ -795,6 +797,14 @@ legend + .control-group { padding-left: @horizontalComponentOffset; } } + +// adjustments for properties tab +.form-horizontal .block-form .control-label { + display: block; + float: none; + width: 100%; +} + //make sure buttons are always on top .umb-panel-buttons .umb-btn-toolbar .btn { position: relative; diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index ddba4387a3..29035213a2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -193,8 +193,8 @@ pre { display: block; padding: (@baseLineHeight - 1) / 2; margin: 0 0 @baseLineHeight / 2; - #font > #family > .monospace; - font-size: @baseFontSize - 1; // 14px to 13px + font-family: @sansFontFamily; + //font-size: @baseFontSize - 1; // 14px to 13px color: @gray-2; line-height: @baseLineHeight; white-space: pre-line; // 1 diff --git a/src/Umbraco.Web.UI.Client/src/less/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less index 24660396d6..9a8c55d08b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/healthcheck.less +++ b/src/Umbraco.Web.UI.Client/src/less/healthcheck.less @@ -29,12 +29,12 @@ padding: 15px 10px; box-sizing: border-box; text-align: center; - border: 2px solid transparent; + border: 1px solid @gray-8; height: 100%; } .umb-healthcheck-group:hover { - border: 2px solid @turquoise; + border: 1px solid @turquoise; cursor: pointer; } diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 699deb9b18..210af19355 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -131,7 +131,6 @@ h5.-black { /* BLOCK MODE */ .block-form .umb-control-group { border-bottom: none; - margin-bottom: 10px !important; padding-bottom: 0; } diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 702f80a67f..d4552c3ac5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -181,7 +181,7 @@ width: 640px !important; } .umb-modal i { - font-size: 14px; + font-size: 20px; } .umb-modal .breadcrumb { background: none; diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index da0dfc0110..c6fd3dde01 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -249,6 +249,11 @@ border-radius: 0; } +// fix dropdown with checkbox + long text in label +.dropdown-menu > li > .flex > label { + flex: 1 1 0; +} + .dropdown-menu > li > a { padding: 8px 20px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/pages/login.less b/src/Umbraco.Web.UI.Client/src/less/pages/login.less index 924f9b20bb..5443134dd2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/pages/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/pages/login.less @@ -66,7 +66,7 @@ margin-bottom: auto; } -.login-overlay .form input[type="text"], +.login-overlay .form input[type="text"], .login-overlay .form input[type="password"], .login-overlay .form input[type="email"] { height: 36px; @@ -74,10 +74,6 @@ padding-right: 10px; } -.login-overlay .btn-success { - padding: 12px 24px; -} - .login-overlay .form label { font-weight: bold; } @@ -114,8 +110,44 @@ line-height: 36px; } -.login-overlay .text-error, -.login-overlay .text-info +.login-overlay .text-error, +.login-overlay .text-info { font-weight:bold; -} +} + +.password-toggle { + position: relative; + display: block; + user-select: none; + + input::-ms-clear, input::-ms-reveal { + display: none; + } + + a { + opacity: .5; + cursor: pointer; + display: inline-block; + position: absolute; + height: 1px; + width: 45px; + height: 75%; + font-size: 0; + background-repeat: no-repeat; + background-size: 50%; + background-position: center; + top: 0; + margin-left: -45px; + z-index: 1; + -webkit-tap-highlight-color: transparent; + } + + [type="text"] + a { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M29.6.4C29 0 28 0 27.4.4L21 6.8c-1.4-.5-3-.8-5-.8C9 6 3 10 0 16c1.3 2.6 3 4.8 5.4 6.5l-5 5c-.5.5-.5 1.5 0 2 .3.4.7.5 1 .5s1 0 1.2-.4l27-27C30 2 30 1 29.6.4zM13 10c1.3 0 2.4 1 2.8 2L12 15.8c-1-.4-2-1.5-2-2.8 0-1.7 1.3-3 3-3zm-9.6 6c1.2-2 2.8-3.5 4.7-4.7l.7-.2c-.4 1-.6 2-.6 3 0 1.8.6 3.4 1.6 4.7l-2 2c-1.6-1.2-3-2.7-4-4.4zM24 13.8c0-.8 0-1.7-.4-2.4l-10 10c.7.3 1.6.4 2.4.4 4.4 0 8-3.6 8-8z'/%3E%3Cpath fill='%23444' d='M26 9l-2.2 2.2c2 1.3 3.6 3 4.8 4.8-1.2 2-2.8 3.5-4.7 4.7-2.7 1.5-5.4 2.3-8 2.3-1.4 0-2.6 0-3.8-.4L10 25c2 .6 4 1 6 1 7 0 13-4 16-10-1.4-2.8-3.5-5.2-6-7z'/%3E%3C/svg%3E"); + } + + [type="password"] + a { + background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath fill='%23444' d='M16 6C9 6 3 10 0 16c3 6 9 10 16 10s13-4 16-10c-3-6-9-10-16-10zm8 5.3c1.8 1.2 3.4 2.8 4.6 4.7-1.2 2-2.8 3.5-4.7 4.7-3 1.5-6 2.3-8 2.3s-6-.8-8-2.3C6 19.5 4 18 3 16c1.5-2 3-3.5 5-4.7l.6-.2C8 12 8 13 8 14c0 4.5 3.5 8 8 8s8-3.5 8-8c0-1-.3-2-.6-2.6l.4.3zM16 13c0 1.7-1.3 3-3 3s-3-1.3-3-3 1.3-3 3-3 3 1.3 3 3z'/%3E%3C/svg%3E"); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less new file mode 100644 index 0000000000..57edbd7266 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -0,0 +1,114 @@ +//----- SCHEDULED PUBLISH ------ + +.place-holder { + height: 60px; + width: 60px; + margin: 15px auto; + background-color: @gray-8; +} + +.date-wrapper { + display: flex; + justify-content: space-around; + flex-direction: row; +} + +.date-container { + text-align: center; +} + +.date-wrapper__number{ + font-size: 40px; + line-height: 50px; + color: @gray-2; + font-weight: 900; +} + +.date-container__title { + font-size: 16px; + font-weight: bold; + color: @gray-3; + margin-bottom: 5px; +} + +.date-container__date { + padding: 0 10px; +} +.date-container__date:hover { + background-color: @gray-10; + cursor: pointer; +} + +.date-wrapper__date{ + font-size: 13px; + color: @gray-6; + margin: 0; +} + +.data-wrapper__add{ + font-size: 18px; + line-height: 10px; + color: @gray-8; + font-weight: 900; + margin: 0; +} + +.date-separate { + width: 1px; + background-color: @gray-8; +} + +//------------------- HISTORY ------------------ + +.history { + position: relative; +} + +.history-line { + width: 2px; + height: 100%; + margin: 0 0 0 14px; + background-color: @gray-8; + position: absolute; + z-index: 0; +} + +.history-item { + display: flex; + align-items: center; + margin-bottom: 24px; + position: relative; + z-index: 1; +} + +.history-item__avatar { + margin-right: 7px; +} + +.history-item__date { + font-size: 13px; + color: @gray-5; +} + +.history-item__break { + display: flex; + align-items: center; + min-width: 230px; + font-size: 14px; +} + +/* RESPONSIVE */ +@media (min-width: 1101px) and (max-width: 1365px), (max-width: 979px) { + + .history-item { + display: block; + } + + .history-item__break { + padding: 7px 0; + } + + .history-line { + display: none; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index 2388aae2b2..2d317fa4a0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -40,6 +40,16 @@ padding: 10px; } +.umb-contentpicker__min-max-help { + font-size: 13px; + margin-top: 5px; + color: @gray-4; +} + +.show-validation .umb-contentpicker__min-max-help { + display: none; +} + .umb-contentpicker small { &:not(:last-child) { @@ -109,16 +119,48 @@ ul.color-picker li a { } /* pre-value editor */ +/*.control-group.color-picker-preval:before { + content: ""; + display: inline-block; + vertical-align: middle; + height: 100%; +}*/ + +/*.control-group.color-picker-preval div.thumbnail { + display: inline-block; + vertical-align: middle; +}*/ +.control-group.color-picker-preval div.color-picker-prediv { + display: inline-block; + width: 60%; +} .control-group.color-picker-preval pre { display: inline; margin-right: 20px; margin-left: 10px; + width: 50%; + white-space: nowrap; + overflow: hidden; + margin-bottom: 0; + vertical-align: middle; +} + +.control-group.color-picker-preval btn { + //vertical-align: middle; +} + +.control-group.color-picker-preval input[type="text"] { + min-width: 40%; + width: 40%; + display: inline-block; + margin-right: 20px; + margin-top: 1px; } .control-group.color-picker-preval label { - border:solid @white 1px; - padding:6px; + border: solid @white 1px; + padding: 6px; } @@ -126,21 +168,21 @@ ul.color-picker li a { // Media picker // -------------------------------------------------- .umb-mediapicker .add-link { - display: inline-block; - height: 120px; - width: 120px; - text-align: center; - color: @gray-8; - border: 2px @gray-8 dashed; - line-height: 120px; - text-decoration: none; + display: flex; + justify-content:center; + align-items:center; + width: 120px; + text-align: center; + color: @gray-8; + border: 2px @gray-8 dashed; + text-decoration: none; - transition: all 150ms ease-in-out; + transition: all 150ms ease-in-out; - &:hover { - color: @turquoise-d1; - border-color: @turquoise; - } + &:hover { + color: @turquoise-d1; + border-color: @turquoise; + } } .umb-mediapicker .picked-image { @@ -165,13 +207,29 @@ ul.color-picker li a { text-decoration: none; } - - -.umb-thumbnails{ - position: relative; +.umb-mediapicker .add-link-square { + height: 120px; } + +.umb-thumbnails { + position: relative; + display: flex; + -ms-flex-direction: row; + -webkit-flex-direction: row; + flex-direction: row; + -ms-flex-wrap: wrap; + -webkit-flex-wrap: wrap; + flex-wrap: wrap; + justify-content: flex-start; +} + +.umb-thumbnails > li.icon { + width: 14%; + text-align: center; +} + .umb-thumbnails i{margin: auto;} .umb-thumbnails a{ outline: none; @@ -207,11 +265,10 @@ ul.color-picker li a { .umb-mediapicker .umb-sortable-thumbnails li { flex-direction: column; - margin: 0; + margin: 0 5px 5px 0; padding: 5px; } - .umb-sortable-thumbnails li:hover a { display: flex; justify-content: center; @@ -219,16 +276,20 @@ ul.color-picker li a { } .umb-sortable-thumbnails li img { - max-width:100%; - max-height:100%; - margin:auto; - display:block; - background-image: url(../img/checkered-background.png); + max-width:100%; + max-height:100%; + margin:auto; + display:block; + background-image: url(../img/checkered-background.png); } -.umb-sortable-thumbnails li img.noScale{ - max-width: none !important; - max-height: none !important; +.umb-sortable-thumbnails li img.trashed { + opacity:0.3; +} + +.umb-sortable-thumbnails li img.noScale { + max-width: none !important; + max-height: none !important; } .umb-sortable-thumbnails .umb-icon-holder { @@ -242,6 +303,12 @@ ul.color-picker li a { display: block; } +.umb-sortable-thumbnails .umb-sortable-thumbnails__wrapper { + width: 124px; + height: 124px; + overflow: hidden; +} + .umb-sortable-thumbnails .umb-sortable-thumbnails__actions { position: absolute; bottom: 10px; @@ -253,9 +320,15 @@ ul.color-picker li a { visibility: hidden; } +.umb-sortable-thumbnails.ui-sortable:not(.ui-sortable-disabled) { + > li:not(.unsortable) { + cursor: move; + } +} + .umb-sortable-thumbnails li:hover .umb-sortable-thumbnails__actions { - opacity: 1; - visibility: visible; + opacity: 1; + visibility: visible; } .umb-sortable-thumbnails .umb-sortable-thumbnails__action { @@ -269,6 +342,7 @@ ul.color-picker li a { justify-content: center; align-items: center; margin-left: 5px; + text-decoration: none; } .umb-sortable-thumbnails .umb-sortable-thumbnails__action.-red { @@ -285,27 +359,27 @@ ul.color-picker li a { // ------------------------------------------------- .umb-cropper{ - position: relative; + position: relative; } .umb-cropper img, .umb-cropper-gravity img{ - position: relative; - max-width: 100%; - height: auto; - top: 0; - left: 0; + position: relative; + max-width: 100%; + height: auto; + top: 0; + left: 0; } .umb-cropper img { - max-width: none; + max-width: none; } .umb-cropper .overlay, .umb-cropper-gravity .overlay { - top: 0; - left: 0; - cursor: move; - z-index: 6001; - position: absolute; + top: 0; + left: 0; + cursor: move; + z-index: @zindexCropperOverlay; + position: absolute; } .umb-cropper .viewport{ @@ -317,43 +391,43 @@ ul.color-picker li a { } .umb-cropper-gravity .viewport{ - overflow: hidden; - position: relative; - width: 100%; - height: 100%; + overflow: hidden; + position: relative; + width: 100%; + height: 100%; } .umb-cropper .viewport:after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 5999; - -moz-opacity: .75; - opacity: .75; - filter: alpha(opacity=7); - -webkit-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); - -moz-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); - box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: @zindexCropperOverlay - 1; + -moz-opacity: .75; + opacity: .75; + filter: alpha(opacity=7); + -webkit-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + -moz-box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); + box-shadow: inset 0 0 0 20px white,inset 0 0 0 21px rgba(0,0,0,.1),inset 0 0 20px 21px rgba(0,0,0,.2); } .umb-cropper-gravity .overlay{ - width: 14px; - height: 14px; - text-align: center; - border-radius: 20px; - background: @turquoise; - border: 3px solid @white; - opacity: 0.8; + width: 14px; + height: 14px; + text-align: center; + border-radius: 20px; + background: @turquoise; + border: 3px solid @white; + opacity: 0.8; } .umb-cropper-gravity .overlay i { - font-size: 26px; - line-height: 26px; - opacity: 0.8 !important; + font-size: 26px; + line-height: 26px; + opacity: 0.8 !important; } .umb-cropper .crop-container { @@ -361,16 +435,16 @@ ul.color-picker li a { } .umb-cropper .crop-slider { - padding: 10px; - border-top: 1px solid @gray-10; - margin-top: 10px; - display: flex; - align-items: center; - justify-content: center; - flex-wrap: wrap; - @media (min-width: 769px) { + padding: 10px; + border-top: 1px solid @gray-10; + margin-top: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + @media (min-width: 769px) { padding: 10px 50px 10px 50px; - } + } } .umb-cropper .crop-slider i { @@ -438,6 +512,7 @@ ul.color-picker li a { top: 3px; right: 3px; cursor: pointer; + z-index: 1; } .umb-close-cropper:hover { diff --git a/src/Umbraco.Web.UI.Client/src/less/sections.less b/src/Umbraco.Web.UI.Client/src/less/sections.less index 03d25a78f5..b4e4e2ffbc 100644 --- a/src/Umbraco.Web.UI.Client/src/less/sections.less +++ b/src/Umbraco.Web.UI.Client/src/less/sections.less @@ -75,15 +75,11 @@ ul.sections li.avatar { height: 75px; padding: 22px 0 2px 0; text-align: center; - margin: 0 0 0 -4px; border-bottom: 1px solid @purple-d1; } ul.sections li.avatar a { - margin: 0 auto; padding: 0; - width: 40px; - height: 40px; border: none } diff --git a/src/Umbraco.Web.UI.Client/src/less/tree.less b/src/Umbraco.Web.UI.Client/src/less/tree.less index c859fae991..c9ab44ea21 100644 --- a/src/Umbraco.Web.UI.Client/src/less/tree.less +++ b/src/Umbraco.Web.UI.Client/src/less/tree.less @@ -119,6 +119,7 @@ .umb-tree div > a.umb-options { visibility: hidden; + flex: 1 0 auto; } .umb-tree div:hover > a.umb-options { visibility: visible; @@ -156,8 +157,8 @@ .umb-tree li > div a:not(.umb-options) { padding: 6px 0; - width: 100%; display: flex; + flex: 1 1 100%; } .umb-tree li > div:hover a:not(.umb-options) { @@ -239,6 +240,7 @@ .umb-tree .umb-tree-node-checked i:before { /*check box*/ content: "\e165" !important; + font-family: inherit; } a.umb-options { diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 635d75d895..d72a9085ec 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -100,6 +100,27 @@ .color-green, .color-green i{color: @green-d1 !important;} .color-yellow, .color-yellow i{color: @yellow-d1 !important;} +/* Colors based on http://zavoloklom.github.io/material-design-color-palette/colors.html */ +.color-black, .color-black i { color: #000 !important; } +.color-blue-grey, .color-blue-grey i { color: #607d8b !important; } +.color-grey, .color-grey i { color: #9e9e9e !important; } +.color-brown, .color-brown i { color: #795548 !important; } +.color-blue, .color-blue i { color: #2196f3 !important; } +.color-light-blue, .color-light-blue i {color: #03a9f4 !important; } +.color-cyan, .color-cyan i { color: #00bcd4 !important; } +.color-green, .color-green i { color: #4caf50 !important; } +.color-light-green, .color-light-green i {color: #8bc34a !important; } +.color-lime, .color-lime i { color: #cddc39 !important; } +.color-yellow, .color-yellow i { color: #ffeb3b !important; } +.color-amber, .color-amber i { color: #ffc107 !important; } +.color-orange, .color-orange i { color: #ff9800 !important; } +.color-deep-orange, .color-deep-orange i { color: #ff5722 !important; } +.color-red, .color-red i { color: #f44336 !important; } +.color-pink, .color-pink i { color: #e91e63 !important; } +.color-purple,.color-purple i { color: #9c27b0 !important; } +.color-deep-purple, .color-deep-purple i { color: #673ab7 !important; } +.color-indigo, .color-indigo i { color: #3f51b5 !important; } + // Scaffolding // ------------------------- @@ -218,6 +239,9 @@ // COMPONENT VARIABLES // -------------------------------------------------- +// Drawer +@drawerWidth: 400px; + // Z-index master list // ------------------------- @@ -230,6 +254,12 @@ @zindexModalBackdrop: 1040; @zindexModal: 1050; +@zindexUmbOverlay: 7500; +@zindexOverlayBackdrop: 2000; + +// Sticky bar has a z-index of "500", which is set from javascript in directive +// so set z-index of cropper should be lower to be behind sticky bar. +@zindexCropperOverlay: 499; // Sprite icons path // ------------------------- diff --git a/src/Umbraco.Web.UI.Client/src/routes.js b/src/Umbraco.Web.UI.Client/src/routes.js index 2fb5f9beee..3fc4d3f78e 100644 --- a/src/Umbraco.Web.UI.Client/src/routes.js +++ b/src/Umbraco.Web.UI.Client/src/routes.js @@ -31,6 +31,12 @@ app.config(function ($routeProvider) { userService.getCurrentUser({ broadcastEvent: broadcast }).then(function (user) { //is auth, check if we allow or reject if (isRequired) { + + //This checks the current section and will force a redirect to 'content' as the default + if ($route.current.params.section.toLowerCase() === "default" || $route.current.params.section.toLowerCase() === "umbraco" || $route.current.params.section === "") { + $route.current.params.section = "content"; + } + // U4-5430, Benjamin Howarth // We need to change the current route params if the user only has access to a single section // To do this we need to grab the current user's allowed sections, then reject the promise with the correct path. @@ -98,26 +104,24 @@ app.config(function ($routeProvider) { resolve: doLogout() }) .when('/:section', { + //This allows us to dynamically change the template for this route since you cannot inject services into the templateUrl method. template: "
      ", //This controller will execute for this route, then we can execute some code in order to set the template Url controller: function ($scope, $route, $routeParams, $location, sectionService) { - if ($routeParams.section.toLowerCase() === "default" || $routeParams.section.toLowerCase() === "umbraco" || $routeParams.section === "") { - $routeParams.section = "content"; - } - - //We are going to check the currently loaded sections for the user and if the section we are navigating + + //We are going to check the currently loaded sections for the user and if the section we are navigating //to has a custom route path we'll use that sectionService.getSectionsForUser().then(function(sections) { - //find the one we're requesting + //find the one we're requesting var found = _.find(sections, function(s) { return s.alias === $routeParams.section; - }) + }) if (found && found.routePath) { //there's a custom route path so redirect $location.path(found.routePath); } - else { + else { //there's no custom route path so continue as normal $routeParams.url = "dashboard.aspx?app=" + $routeParams.section; $scope.templateUrl = 'views/common/dashboard.html'; @@ -138,8 +142,8 @@ app.config(function ($routeProvider) { }) .when('/:section/:tree/:method', { templateUrl: function (rp) { - - //if there is no method registered for this then show the dashboard + + //if there is no method registered for this then show the dashboard if (!rp.method) return "views/common/dashboard.html"; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.controller.js index ec1ad6e663..7f7eed8e4c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.controller.js @@ -7,14 +7,13 @@ angular.module("umbraco") $scope.icons = icons; }); - $scope.submitClass = function (icon) { + $scope.submitClass = function(icon){ if($scope.color) { $scope.submit(icon + " " + $scope.color); } - else { - $scope.submit(icon); + else { + $scope.submit(icon); } }; - } ); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html index 4e482c26b1..f21fdf0b06 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/iconpicker.html @@ -1,7 +1,7 @@
      -
      -
      - +
      - + + No icons were found. + +
      - +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.html index c71b70faf8..70889f2cd7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/insertmacro.html @@ -1,4 +1,4 @@ -
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 18158a5ff2..331010e3ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -1,399 +1,412 @@ angular.module("umbraco").controller("Umbraco.Dialogs.LoginController", - function ($scope, $cookies, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService, $q) { + function ($scope, $cookies, $location, currentUserResource, formHelper, mediaHelper, umbRequestHelper, Upload, localizationService, userService, externalLoginInfo, resetPasswordCodeInfo, $timeout, authResource, dialogService, $q) { - $scope.invitedUser = null; - $scope.invitedUserPasswordModel = { - password: "", - confirmPassword: "", - buttonState: "", - passwordPolicies: null, - passwordPolicyText: "" - } - $scope.avatarFile = { - filesHolder: null, - uploadStatus: null, - uploadProgress: 0, - maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB", - acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), - uploaded: false - } + $scope.invitedUser = null; + $scope.invitedUserPasswordModel = { + password: "", + confirmPassword: "", + buttonState: "", + passwordPolicies: null, + passwordPolicyText: "" + } + $scope.loginStates = { + submitButton: "init" + } + $scope.avatarFile = { + filesHolder: null, + uploadStatus: null, + uploadProgress: 0, + maxFileSize: Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB", + acceptedFileTypes: mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes), + uploaded: false + } + $scope.togglePassword = function () { + var elem = $("form[name='loginForm'] input[name='password']"); + elem.attr("type", (elem.attr("type") === "text" ? "password" : "text")); + } - function init() { - // Check if it is a new user - var inviteVal = $location.search().invite; - if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { + function init() { + // Check if it is a new user + var inviteVal = $location.search().invite; + if (inviteVal && (inviteVal === "1" || inviteVal === "2")) { - $q.all([ - //get the current invite user - authResource.getCurrentInvitedUser().then(function (data) { - $scope.invitedUser = data; - }, - function() { - //it failed so we should remove the search - $location.search('invite', null); - }), - //get the membership provider config for password policies - authResource.getMembershipProviderConfig().then(function (data) { - $scope.invitedUserPasswordModel.passwordPolicies = data; + $q.all([ + //get the current invite user + authResource.getCurrentInvitedUser().then(function (data) { + $scope.invitedUser = data; + }, + function () { + //it failed so we should remove the search + $location.search('invite', null); + }), + //get the membership provider config for password policies + authResource.getMembershipProviderConfig().then(function (data) { + $scope.invitedUserPasswordModel.passwordPolicies = data; - //localize the text - localizationService.localize("errorHandling_errorInPasswordFormat", - [ - $scope.invitedUserPasswordModel.passwordPolicies.minPasswordLength, - $scope.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars - ]).then(function(data) { - $scope.invitedUserPasswordModel.passwordPolicyText = data; + //localize the text + localizationService.localize("errorHandling_errorInPasswordFormat", + [ + $scope.invitedUserPasswordModel.passwordPolicies.minPasswordLength, + $scope.invitedUserPasswordModel.passwordPolicies.minNonAlphaNumericChars + ]).then(function (data) { + $scope.invitedUserPasswordModel.passwordPolicyText = data; + }); + }) + ]).then(function () { + + $scope.inviteStep = Number(inviteVal); + + }); + } + } + + $scope.changeAvatar = function (files, event) { + if (files && files.length > 0) { + upload(files[0]); + } + }; + + $scope.getStarted = function () { + $location.search('invite', null); + $scope.submit(true); + } + + function upload(file) { + + $scope.avatarFile.uploadProgress = 0; + + Upload.upload({ + url: umbRequestHelper.getApiUrl("currentUserApiBaseUrl", "PostSetAvatar"), + fields: {}, + file: file + }).progress(function (evt) { + + if ($scope.avatarFile.uploadStatus !== "done" && $scope.avatarFile.uploadStatus !== "error") { + // set uploading status on file + $scope.avatarFile.uploadStatus = "uploading"; + + // calculate progress in percentage + var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); + + // set percentage property on file + $scope.avatarFile.uploadProgress = progressPercentage; + } + + }).success(function (data, status, headers, config) { + + $scope.avatarFile.uploadProgress = 100; + + // set done status on file + $scope.avatarFile.uploadStatus = "done"; + + $scope.invitedUser.avatars = data; + + $scope.avatarFile.uploaded = true; + + }).error(function (evt, status, headers, config) { + + // set status done + $scope.avatarFile.uploadStatus = "error"; + + // If file not found, server will return a 404 and display this message + if (status === 404) { + $scope.avatarFile.serverErrorMessage = "File not found"; + } + else if (status == 400) { + //it's a validation error + $scope.avatarFile.serverErrorMessage = evt.message; + } + else { + //it's an unhandled error + //if the service returns a detailed error + if (evt.InnerException) { + $scope.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; + + //Check if its the common "too large file" exception + if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { + $scope.avatarFile.serverErrorMessage = "File too large to upload"; + } + + } else if (evt.Message) { + $scope.avatarFile.serverErrorMessage = evt.Message; + } + } }); - }) - ]).then(function () { - - $scope.inviteStep = Number(inviteVal); - - }); - } - } - - $scope.changeAvatar = function (files, event) { - if (files && files.length > 0) { - upload(files[0]); - } - }; - - $scope.getStarted = function() { - $location.search('invite', null); - $scope.submit(true); - } - - function upload(file) { - - $scope.avatarFile.uploadProgress = 0; - - Upload.upload({ - url: umbRequestHelper.getApiUrl("currentUserApiBaseUrl", "PostSetAvatar"), - fields: {}, - file: file - }).progress(function (evt) { - - if ($scope.avatarFile.uploadStatus !== "done" && $scope.avatarFile.uploadStatus !== "error") { - // set uploading status on file - $scope.avatarFile.uploadStatus = "uploading"; - - // calculate progress in percentage - var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); - - // set percentage property on file - $scope.avatarFile.uploadProgress = progressPercentage; } - }).success(function (data, status, headers, config) { + $scope.inviteSavePassword = function () { - $scope.avatarFile.uploadProgress = 100; + if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { - // set done status on file - $scope.avatarFile.uploadStatus = "done"; + $scope.invitedUserPasswordModel.buttonState = "busy"; - $scope.invitedUser.avatars = data; + currentUserResource.performSetInvitedUserPassword($scope.invitedUserPasswordModel.password) + .then(function (data) { - $scope.avatarFile.uploaded = true; + //success + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + $scope.invitedUserPasswordModel.buttonState = "success"; + //set the user and set them as logged in + $scope.invitedUser = data; + userService.setAuthenticationSuccessful(data); - }).error(function (evt, status, headers, config) { + $scope.inviteStep = 2; - // set status done - $scope.avatarFile.uploadStatus = "error"; + }, function (err) { - // If file not found, server will return a 404 and display this message - if (status === 404) { - $scope.avatarFile.serverErrorMessage = "File not found"; + //error + formHelper.handleError(err); + + $scope.invitedUserPasswordModel.buttonState = "error"; + + }); + } + }; + + var setFieldFocus = function (form, field) { + $timeout(function () { + $("form[name='" + form + "'] input[name='" + field + "']").focus(); + }); } - else if (status == 400) { - //it's a validation error - $scope.avatarFile.serverErrorMessage = evt.message; - } - else { - //it's an unhandled error - //if the service returns a detailed error - if (evt.InnerException) { - $scope.avatarFile.serverErrorMessage = evt.InnerException.ExceptionMessage; - //Check if its the common "too large file" exception - if (evt.InnerException.StackTrace && evt.InnerException.StackTrace.indexOf("ValidateRequestEntityLength") > 0) { - $scope.avatarFile.serverErrorMessage = "File too large to upload"; + var twoFactorloginDialog = null; + function show2FALoginDialog(view, callback) { + if (!twoFactorloginDialog) { + twoFactorloginDialog = dialogService.open({ + + //very special flag which means that global events cannot close this dialog + manualClose: true, + template: view, + modalClass: "login-overlay", + animation: "slide", + show: true, + callback: callback, + + }); + } + } + + function resetInputValidation() { + $scope.confirmPassword = ""; + $scope.password = ""; + $scope.login = ""; + if ($scope.loginForm) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); + } + if ($scope.requestPasswordResetForm) { + $scope.requestPasswordResetForm.email.$setValidity("auth", true); + } + if ($scope.setPasswordForm) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + } + + $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; + + $scope.showLogin = function () { + $scope.errorMsg = ""; + resetInputValidation(); + $scope.view = "login"; + setFieldFocus("loginForm", "username"); + } + + $scope.showRequestPasswordReset = function () { + $scope.errorMsg = ""; + resetInputValidation(); + $scope.view = "request-password-reset"; + $scope.showEmailResetConfirmation = false; + setFieldFocus("requestPasswordResetForm", "email"); + } + + $scope.showSetPassword = function () { + $scope.errorMsg = ""; + resetInputValidation(); + $scope.view = "set-password"; + setFieldFocus("setPasswordForm", "password"); + } + + var d = new Date(); + var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); + var konamiMode = $cookies.konamiLogin; + if (konamiMode == "1") { + $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; + } else { + localizationService.localize("login_greeting" + d.getDay()).then(function (label) { + $scope.greeting = label; + }); // weekday[d.getDay()]; + } + $scope.errorMsg = ""; + + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLoginInfo = externalLoginInfo; + $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; + $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; + + $scope.activateKonamiMode = function () { + if ($cookies.konamiLogin == "1") { + // somehow I can't update the cookie value using $cookies, so going native + document.cookie = "konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; + document.location.reload(); + } else { + document.cookie = "konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;"; + $scope.$apply(function () { + $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; + }); + } + } + + $scope.loginSubmit = function (login, password) { + + //TODO: Do validation properly like in the invite password update + + //if the login and password are not empty we need to automatically + // validate them - this is because if there are validation errors on the server + // then the user has to change both username & password to resubmit which isn't ideal, + // so if they're not empty, we'll just make sure to set them to valid. + if (login && password && login.length > 0 && password.length > 0) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); } - } else if (evt.Message) { - $scope.avatarFile.serverErrorMessage = evt.Message; - } + if ($scope.loginForm.$invalid) { + return; + } + + $scope.loginStates.submitButton = "busy"; + + userService.authenticate(login, password) + .then(function (data) { + $scope.loginStates.submitButton = "success"; + $scope.submit(true); + }, + function (reason) { + + //is Two Factor required? + if (reason.status === 402) { + $scope.errorMsg = "Additional authentication required"; + show2FALoginDialog(reason.data.twoFactorView, $scope.submit); + } + else { + $scope.loginStates.submitButton = "error"; + $scope.errorMsg = reason.errorMsg; + + //set the form inputs to invalid + $scope.loginForm.username.$setValidity("auth", false); + $scope.loginForm.password.$setValidity("auth", false); + } + }); + + //setup a watch for both of the model values changing, if they change + // while the form is invalid, then revalidate them so that the form can + // be submitted again. + $scope.loginForm.username.$viewChangeListeners.push(function () { + if ($scope.loginForm.$invalid) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); + } + }); + $scope.loginForm.password.$viewChangeListeners.push(function () { + if ($scope.loginForm.$invalid) { + $scope.loginForm.username.$setValidity('auth', true); + $scope.loginForm.password.$setValidity('auth', true); + } + }); + }; + + $scope.requestPasswordResetSubmit = function (email) { + + //TODO: Do validation properly like in the invite password update + + if (email && email.length > 0) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); + } + + $scope.showEmailResetConfirmation = false; + + if ($scope.requestPasswordResetForm.$invalid) { + return; + } + + $scope.errorMsg = ""; + + authResource.performRequestPasswordReset(email) + .then(function () { + //remove the email entered + $scope.email = ""; + $scope.showEmailResetConfirmation = true; + }, function (reason) { + $scope.errorMsg = reason.errorMsg; + $scope.requestPasswordResetForm.email.$setValidity("auth", false); + }); + + $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { + if ($scope.requestPasswordResetForm.email.$invalid) { + $scope.requestPasswordResetForm.email.$setValidity('auth', true); + } + }); + }; + + $scope.setPasswordSubmit = function (password, confirmPassword) { + + $scope.showSetPasswordConfirmation = false; + + if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { + $scope.setPasswordForm.password.$setValidity('auth', true); + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + + if ($scope.setPasswordForm.$invalid) { + return; + } + + //TODO: All of this logic can/should be shared! We should do validation the nice way instead of all of this manual stuff, see: inviteSavePassword + authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode) + .then(function () { + $scope.showSetPasswordConfirmation = true; + $scope.resetComplete = true; + + //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again + resetPasswordCodeInfo.resetCodeModel = null; + + }, function (reason) { + if (reason.data && reason.data.Message) { + $scope.errorMsg = reason.data.Message; + } + else { + $scope.errorMsg = reason.errorMsg; + } + $scope.setPasswordForm.password.$setValidity("auth", false); + $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); + }); + + $scope.setPasswordForm.password.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.password.$invalid) { + $scope.setPasswordForm.password.$setValidity('auth', true); + } + }); + $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { + if ($scope.setPasswordForm.confirmPassword.$invalid) { + $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); + } + }); } - }); - } - $scope.inviteSavePassword = function () { - - if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { - - $scope.invitedUserPasswordModel.buttonState = "busy"; - currentUserResource.performSetInvitedUserPassword($scope.invitedUserPasswordModel.password) - .then(function (data) { + //Now, show the correct panel: - //success - formHelper.resetForm({ scope: $scope, notifications: data.notifications }); - $scope.invitedUserPasswordModel.buttonState = "success"; - //set the user and set them as logged in - $scope.invitedUser = data; - userService.setAuthenticationSuccessful(data); - - $scope.inviteStep = 2; - - }, function(err) { - - //error - formHelper.handleError(err); - - $scope.invitedUserPasswordModel.buttonState = "error"; - - }); - } - }; - - var setFieldFocus = function (form, field) { - $timeout(function () { - $("form[name='" + form + "'] input[name='" + field + "']").focus(); - }); - } - - var twoFactorloginDialog = null; - function show2FALoginDialog(view, callback) { - if (!twoFactorloginDialog) { - twoFactorloginDialog = dialogService.open({ - - //very special flag which means that global events cannot close this dialog - manualClose: true, - template: view, - modalClass: "login-overlay", - animation: "slide", - show: true, - callback: callback, - - }); - } - } - - function resetInputValidation() { - $scope.confirmPassword = ""; - $scope.password = ""; - $scope.login = ""; - if ($scope.loginForm) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - if ($scope.requestPasswordResetForm) { - $scope.requestPasswordResetForm.email.$setValidity("auth", true); - } - if ($scope.setPasswordForm) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - } - - $scope.allowPasswordReset = Umbraco.Sys.ServerVariables.umbracoSettings.allowPasswordReset; - - $scope.showLogin = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "login"; - setFieldFocus("loginForm", "username"); - } - - $scope.showRequestPasswordReset = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "request-password-reset"; - $scope.showEmailResetConfirmation = false; - setFieldFocus("requestPasswordResetForm", "email"); - } - - $scope.showSetPassword = function () { - $scope.errorMsg = ""; - resetInputValidation(); - $scope.view = "set-password"; - setFieldFocus("setPasswordForm", "password"); - } - - var d = new Date(); - var konamiGreetings = new Array("Suze Sunday", "Malibu Monday", "Tequila Tuesday", "Whiskey Wednesday", "Negroni Day", "Fernet Friday", "Sancerre Saturday"); - var konamiMode = $cookies.konamiLogin; - if (konamiMode == "1") { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - } else { - localizationService.localize("login_greeting" + d.getDay()).then(function (label) { - $scope.greeting = label; - }); // weekday[d.getDay()]; - } - $scope.errorMsg = ""; - - $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - $scope.externalLoginProviders = externalLoginInfo.providers; - $scope.externalLoginInfo = externalLoginInfo; - $scope.resetPasswordCodeInfo = resetPasswordCodeInfo; - $scope.backgroundImage = Umbraco.Sys.ServerVariables.umbracoSettings.loginBackgroundImage; - - $scope.activateKonamiMode = function () { - if ($cookies.konamiLogin == "1") { - // somehow I can't update the cookie value using $cookies, so going native - document.cookie = "konamiLogin=; expires=Thu, 01 Jan 1970 00:00:01 GMT;"; - document.location.reload(); - } else { - document.cookie = "konamiLogin=1; expires=Tue, 01 Jan 2030 00:00:01 GMT;"; - $scope.$apply(function () { - $scope.greeting = "Happy " + konamiGreetings[d.getDay()]; - }); - } - } - - $scope.loginSubmit = function (login, password) { - - //TODO: Do validation properly like in the invite password update - - //if the login and password are not empty we need to automatically - // validate them - this is because if there are validation errors on the server - // then the user has to change both username & password to resubmit which isn't ideal, - // so if they're not empty, we'll just make sure to set them to valid. - if (login && password && login.length > 0 && password.length > 0) { - $scope.loginForm.username.$setValidity('auth', true); - $scope.loginForm.password.$setValidity('auth', true); - } - - if ($scope.loginForm.$invalid) { - return; - } - - userService.authenticate(login, password) - .then(function (data) { - $scope.submit(true); - }, - function (reason) { - - //is Two Factor required? - if (reason.status === 402) { - $scope.errorMsg = "Additional authentication required"; - show2FALoginDialog(reason.data.twoFactorView, $scope.submit); - } - else { - $scope.errorMsg = reason.errorMsg; - - //set the form inputs to invalid - $scope.loginForm.username.$setValidity("auth", false); - $scope.loginForm.password.$setValidity("auth", false); - } - }); - - //setup a watch for both of the model values changing, if they change - // while the form is invalid, then revalidate them so that the form can - // be submitted again. - $scope.loginForm.username.$viewChangeListeners.push(function () { - if ($scope.loginForm.username.$invalid) { - $scope.loginForm.username.$setValidity('auth', true); + if ($scope.resetPasswordCodeInfo.resetCodeModel) { + $scope.showSetPassword(); } - }); - $scope.loginForm.password.$viewChangeListeners.push(function () { - if ($scope.loginForm.password.$invalid) { - $scope.loginForm.password.$setValidity('auth', true); + else if ($scope.resetPasswordCodeInfo.errors.length > 0) { + $scope.view = "password-reset-code-expired"; } - }); - }; - - $scope.requestPasswordResetSubmit = function (email) { - - //TODO: Do validation properly like in the invite password update - - if (email && email.length > 0) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); - } - - $scope.showEmailResetConfirmation = false; - - if ($scope.requestPasswordResetForm.$invalid) { - return; - } - - $scope.errorMsg = ""; - - authResource.performRequestPasswordReset(email) - .then(function () { - //remove the email entered - $scope.email = ""; - $scope.showEmailResetConfirmation = true; - }, function (reason) { - $scope.errorMsg = reason.errorMsg; - $scope.requestPasswordResetForm.email.$setValidity("auth", false); - }); - - $scope.requestPasswordResetForm.email.$viewChangeListeners.push(function () { - if ($scope.requestPasswordResetForm.email.$invalid) { - $scope.requestPasswordResetForm.email.$setValidity('auth', true); + else { + $scope.showLogin(); } - }); - }; - $scope.setPasswordSubmit = function (password, confirmPassword) { + init(); - $scope.showSetPasswordConfirmation = false; - - if (password && confirmPassword && password.length > 0 && confirmPassword.length > 0) { - $scope.setPasswordForm.password.$setValidity('auth', true); - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - - if ($scope.setPasswordForm.$invalid) { - return; - } - - //TODO: All of this logic can/should be shared! We should do validation the nice way instead of all of this manual stuff, see: inviteSavePassword - authResource.performSetPassword($scope.resetPasswordCodeInfo.resetCodeModel.userId, password, confirmPassword, $scope.resetPasswordCodeInfo.resetCodeModel.resetCode) - .then(function () { - $scope.showSetPasswordConfirmation = true; - $scope.resetComplete = true; - - //reset the values in the resetPasswordCodeInfo angular so if someone logs out the change password isn't shown again - resetPasswordCodeInfo.resetCodeModel = null; - - }, function (reason) { - if (reason.data && reason.data.Message) { - $scope.errorMsg = reason.data.Message; - } - else { - $scope.errorMsg = reason.errorMsg; - } - $scope.setPasswordForm.password.$setValidity("auth", false); - $scope.setPasswordForm.confirmPassword.$setValidity("auth", false); - }); - - $scope.setPasswordForm.password.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.password.$invalid) { - $scope.setPasswordForm.password.$setValidity('auth', true); - } - }); - $scope.setPasswordForm.confirmPassword.$viewChangeListeners.push(function () { - if ($scope.setPasswordForm.confirmPassword.$invalid) { - $scope.setPasswordForm.confirmPassword.$setValidity('auth', true); - } - }); - } - - - //Now, show the correct panel: - - if ($scope.resetPasswordCodeInfo.resetCodeModel) { - $scope.showSetPassword(); - } - else if ($scope.resetPasswordCodeInfo.errors.length > 0) { - $scope.view = "password-reset-code-expired"; - } - else { - $scope.showLogin(); - } - - init(); - - }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 61d04d30fb..3fc4698565 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -1,242 +1,249 @@ 
      -
      +
      - + - - -
    - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/membergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/membergrouppicker.html index 48302cea02..e2ff9790d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/membergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/membergrouppicker.html @@ -1,7 +1,7 @@
    - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js new file mode 100644 index 0000000000..4d2b43c078 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.controller.js @@ -0,0 +1,178 @@ +(function () { + "use strict"; + + function HelpDrawerController($scope, $routeParams, $timeout, dashboardResource, localizationService, userService, eventsService, helpService, appState, tourService, $filter) { + + var vm = this; + var evts = []; + + vm.title = localizationService.localize("general_help"); + vm.subtitle = "Umbraco version" + " " + Umbraco.Sys.ServerVariables.application.version; + vm.section = $routeParams.section; + vm.tree = $routeParams.tree; + vm.sectionName = ""; + vm.customDashboard = null; + vm.tours = []; + + vm.closeDrawer = closeDrawer; + vm.startTour = startTour; + vm.getTourGroupCompletedPercentage = getTourGroupCompletedPercentage; + vm.showTourButton = showTourButton; + + function startTour(tour) { + tourService.startTour(tour); + closeDrawer(); + } + + function oninit() { + + tourService.getGroupedTours().then(function(groupedTours) { + vm.tours = groupedTours; + getTourGroupCompletedPercentage(); + }); + + // load custom help dashboard + dashboardResource.getDashboard("user-help").then(function (dashboard) { + vm.customDashboard = dashboard; + }); + + if (!vm.section) { + vm.section = "content"; + } + + setSectionName(); + + userService.getCurrentUser().then(function (user) { + + vm.userType = user.userType; + vm.userLang = user.locale; + + evts.push(eventsService.on("appState.treeState.changed", function (e, args) { + handleSectionChange(); + })); + + findHelp(vm.section, vm.tree, vm.usertype, vm.userLang); + + }); + + // check if a tour is running - if it is open the matching group + var currentTour = tourService.getCurrentTour(); + + if (currentTour) { + openTourGroup(currentTour.alias); + } + + } + + function closeDrawer() { + appState.setDrawerState("showDrawer", false); + } + + function handleSectionChange() { + $timeout(function () { + if (vm.section !== $routeParams.section || vm.tree !== $routeParams.tree) { + + vm.section = $routeParams.section; + vm.tree = $routeParams.tree; + + setSectionName(); + findHelp(vm.section, vm.tree, vm.usertype, vm.userLang); + + } + }); + } + + function findHelp(section, tree, usertype, userLang) { + + helpService.getContextHelpForPage(section, tree).then(function (topics) { + vm.topics = topics; + }); + + var rq = {}; + rq.section = vm.section; + rq.usertype = usertype; + rq.lang = userLang; + + if ($routeParams.url) { + rq.path = decodeURIComponent($routeParams.url); + + if (rq.path.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath) === 0) { + rq.path = rq.path.substring(Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath.length); + } + + if (rq.path.indexOf(".aspx") > 0) { + rq.path = rq.path.substring(0, rq.path.indexOf(".aspx")); + } + + } else { + rq.path = rq.section + "/" + $routeParams.tree + "/" + $routeParams.method; + } + + helpService.findVideos(rq).then(function(videos){ + vm.videos = videos; + }); + + } + + function setSectionName() { + // Get section name + var languageKey = "sections_" + vm.section; + localizationService.localize(languageKey).then(function (value) { + vm.sectionName = value; + }); + } + + function showTourButton(index, tourGroup) { + if(index !== 0) { + var prevTour = tourGroup.tours[index - 1]; + if(prevTour.completed) { + return true; + } + } else { + return true; + } + } + + function openTourGroup(tourAlias) { + angular.forEach(vm.tours, function (group) { + angular.forEach(group, function (tour) { + if (tour.alias === tourAlias) { + group.open = true; + } + }); + }); + } + + function getTourGroupCompletedPercentage() { + // Finding out, how many tours are completed for the progress circle + angular.forEach(vm.tours, function(group){ + var completedTours = 0; + angular.forEach(group.tours, function(tour){ + if(tour.completed) { + completedTours++; + } + }); + group.completedPercentage = Math.round((completedTours/group.tours.length)*100); + }); + } + + evts.push(eventsService.on("appState.tour.complete", function (event, tour) { + tourService.getGroupedTours().then(function(groupedTours) { + vm.tours = groupedTours; + openTourGroup(tour.alias); + getTourGroupCompletedPercentage(); + }); + })); + + $scope.$on('$destroy', function () { + for (var e in evts) { + eventsService.unsubscribe(evts[e]); + } + }); + + oninit(); + + } + + angular.module("umbraco").controller("Umbraco.Drawers.Help", HelpDrawerController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html new file mode 100644 index 0000000000..4829c8964a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -0,0 +1,130 @@ + + + + + + + + +
    + +
    Tours
    + +
    + +
    + + +
    + {{tourGroup.group}} + Other +
    + + +
    + +
    +
    +
    +
    +
    {{ $index + 1 }}
    + + {{ tour.name }} +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    + + + +
    +
    +
    +
    +
    {{property.caption}}
    +
    +
    +
    +
    +
    + + + + + +
    +
    Videos
    + +
    + + + + +
    + + + +
    + + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html index 3cc81fdcf6..6759da0fb4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/compositions/compositions.html @@ -7,9 +7,10 @@ style="width: 100%" ng-model="searchTerm" class="umb-search-field search-query input-block-level" - localize="placeholder" - placeholder="@placeholders_filter" - umb-auto-focus> + localize="placeholder" + placeholder="@placeholders_filter" + umb-auto-focus + no-dirty-check />
    @@ -17,20 +18,17 @@ - - + + - - + +
    • @@ -41,7 +39,7 @@ checklist-model="model.compositeContentTypes" checklist-value="compositeContentType.contentType.alias" ng-change="model.selectCompositeContentType(compositeContentType.contentType)" - ng-disabled="compositeContentType.allowed===false || compositeContentType.inherited"/> + ng-disabled="compositeContentType.allowed===false || compositeContentType.inherited" /> - - Yes, convert line breaks + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index d71352c3d9..aac4830d52 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -1,25 +1,25 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js index 5401ae5387..91c74311b3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.controller.js @@ -22,6 +22,8 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController", selectedSearchResults: [] }; + $scope.showTarget = $scope.model.hideTarget !== true; + if (dialogOptions.currentTarget) { $scope.model.target = dialogOptions.currentTarget; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html index 13bcfd9c3e..e1b13206df 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/linkpicker/linkpicker.html @@ -18,7 +18,7 @@ ng-model="model.target.name" /> - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html index 6bcbaad50c..9f9356d85e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/macropicker/macropicker.html @@ -13,7 +13,8 @@ class="umb-search-field search-query input-block-level" localize="placeholder" placeholder="@placeholders_filter" - umb-auto-focus /> + umb-auto-focus + no-dirty-check />
        @@ -27,9 +28,8 @@
      - + There are no macros available to insert @@ -52,9 +52,8 @@
    - + There are no parameters for this macro diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js index a6a2ddcbab..ccb033a57c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.controller.js @@ -56,6 +56,46 @@ angular.module("umbraco") $scope.target = dialogOptions.currentTarget; } + function onInit() { + if ($scope.startNodeId !== -1) { + entityResource.getById($scope.startNodeId, "media") + .then(function (ent) { + $scope.startNodeId = ent.id; + run(); + }); + } else { + run(); + } + } + + function run() { + //default root item + if (!$scope.target) { + if ($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { + entityResource.getById($scope.lastOpenedNode, "media") + .then(ensureWithinStartNode, gotoStartNode); + } else { + gotoStartNode(); + } + } else { + //if a target is specified, go look it up - generally this target will just contain ids not the actual full + //media object so we need to look it up + var id = $scope.target.udi ? $scope.target.udi : $scope.target.id + var altText = $scope.target.altText; + mediaResource.getById(id) + .then(function (node) { + $scope.target = node; + if (ensureWithinStartNode(node)) { + selectImage(node); + $scope.target.url = mediaHelper.resolveFile(node); + $scope.target.altText = altText; + $scope.openDetailsDialog(); + } + }, + gotoStartNode); + } + } + $scope.upload = function(v) { angular.element(".umb-file-dropzone-directive .file-select").click(); }; @@ -107,7 +147,7 @@ angular.module("umbraco") if (folder.id > 0) { entityResource.getAncestors(folder.id, "media") - .then(function(anc) { + .then(function(anc) { $scope.path = _.filter(anc, function(f) { return f.path.indexOf($scope.startNodeId) !== -1; @@ -218,32 +258,6 @@ angular.module("umbraco") $scope.gotoFolder({ id: $scope.startNodeId, name: "Media", icon: "icon-folder" }); } - //default root item - if (!$scope.target) { - if ($scope.lastOpenedNode && $scope.lastOpenedNode !== -1) { - entityResource.getById($scope.lastOpenedNode, "media") - .then(ensureWithinStartNode, gotoStartNode); - } else { - gotoStartNode(); - } - } else { - //if a target is specified, go look it up - generally this target will just contain ids not the actual full - //media object so we need to look it up - var id = $scope.target.udi ? $scope.target.udi : $scope.target.id - var altText = $scope.target.altText; - mediaResource.getById(id) - .then(function(node) { - $scope.target = node; - if (ensureWithinStartNode(node)) { - selectImage(node); - $scope.target.url = mediaHelper.resolveFile(node); - $scope.target.altText = altText; - $scope.openDetailsDialog(); - } - }, - gotoStartNode); - } - $scope.openDetailsDialog = function() { $scope.mediaPickerDetailsOverlay = {}; @@ -368,4 +382,7 @@ angular.module("umbraco") } } } + + onInit(); + }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html index 8c726f4a17..6ce53e7571 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/mediaPicker/mediapicker.html @@ -3,159 +3,147 @@ enctype="multipart/form-data" umb-image-upload="options"> -
    +
    -
    +
    - - + + - + -
    - - -
    +
    + + +
    -
    +
    -
    - +
    +
    - - + + - - + +
    - +
    - + -
    +
    - + -
    +
    -
    - - -
    +
    + + +
    -
    +
    -
    - Preview -
    +
    + Preview +
    - - + + -
    +
    -
    +
    -
    - - -
    +
    + + +
    -
    - - -
    +
    + + +
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js index b18aa96588..700b18b518 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco") - .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { + .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, dashboardResource, userService, historyService, eventsService, externalLoginInfo, authResource, currentUserResource, formHelper, localizationService) { $scope.history = historyService.getCurrent(); $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; @@ -169,10 +169,13 @@ angular.module("umbraco") $scope.showPasswordFields = !$scope.showPasswordFields; } - function clearPasswordFields() { + function clearPasswordFields() { $scope.changePasswordModel.value.oldPassword = ""; $scope.changePasswordModel.value.newPassword = ""; $scope.changePasswordModel.value.confirm = ""; } - + + dashboardResource.getDashboard("user-dialog").then(function (dashboard) { + $scope.dashboard = dashboard; + }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index 328bb02d76..8021d66a43 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -11,6 +11,7 @@

    +
      @@ -122,4 +126,14 @@
    +
    +
    +
    +
    +

    {{property.caption}}

    +
    +
    +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html index 665645ad17..4768052844 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/usergrouppicker/usergrouppicker.html @@ -1,7 +1,6 @@
    - +
    @@ -9,17 +8,18 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.controller.js new file mode 100644 index 0000000000..bdd7eb65d0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function NodeNameController($scope) { + + var vm = this; + var element = angular.element($scope.model.currentStep.element); + + vm.error = false; + + vm.initNextStep = initNextStep; + + function initNextStep() { + if(element.val().toLowerCase() === 'home') { + $scope.model.nextStep(); + } else { + vm.error = true; + } + } + + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbIntroCreateContent.NodeNameController", NodeNameController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.html new file mode 100644 index 0000000000..53badd7843 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatecontent/nodename/nodename.html @@ -0,0 +1,29 @@ +
    + + + + + + + +
    Please enter Home in the field
    +
    + + + + + + +
    + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.controller.js new file mode 100644 index 0000000000..fee52eb506 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function DocTypeNameController($scope) { + + var vm = this; + var element = angular.element($scope.model.currentStep.element); + + vm.error = false; + + vm.initNextStep = initNextStep; + + function initNextStep() { + if(element.val().toLowerCase() === 'home page') { + $scope.model.nextStep(); + } else { + vm.error = true; + } + } + + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbIntroCreateDocType.DocTypeNameController", DocTypeNameController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.html new file mode 100644 index 0000000000..a0b4b529e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/doctypename/doctypename.html @@ -0,0 +1,29 @@ +
    + + + + + + + +
    Please enter Home Page in the field
    +
    + + + + + + +
    + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.controller.js new file mode 100644 index 0000000000..1ebe878928 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function PropertyNameController($scope) { + + var vm = this; + var element = angular.element($scope.model.currentStep.element); + + vm.error = false; + + vm.initNextStep = initNextStep; + + function initNextStep() { + if (element.val().toLowerCase() === 'welcome text') { + $scope.model.nextStep(); + } else { + vm.error = true; + } + } + + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbIntroCreateDocType.PropertyNameController", PropertyNameController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.html new file mode 100644 index 0000000000..5748f65a95 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/propertyname/propertyname.html @@ -0,0 +1,29 @@ +
    + + + + + + + +
    Please enter Welcome Text in the field
    +
    + + + + + + +
    + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.controller.js new file mode 100644 index 0000000000..c477204cb7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function TabNameController($scope) { + + var vm = this; + var element = angular.element($scope.model.currentStep.element); + + vm.error = false; + + vm.initNextStep = initNextStep; + + function initNextStep() { + if (element.val().toLowerCase() === 'home') { + $scope.model.nextStep(); + } else { + vm.error = true; + } + } + + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbIntroCreateDocType.TabNameController", TabNameController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.html new file mode 100644 index 0000000000..baa0d3da9a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrocreatedoctype/tabname/tabname.html @@ -0,0 +1,29 @@ +
    + + + + + + + +
    Please enter Home in the field
    +
    + + + + + + +
    + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.controller.js new file mode 100644 index 0000000000..ebda50481d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.controller.js @@ -0,0 +1,24 @@ +(function () { + "use strict"; + + function FolderNameController($scope) { + + var vm = this; + var element = angular.element($scope.model.currentStep.element); + + vm.error = false; + + vm.initNextStep = initNextStep; + + function initNextStep() { + if(element.val().toLowerCase() === "my images") { + $scope.model.nextStep(); + } else { + vm.error = true; + } + } + + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbIntroMediaSection.FolderNameController", FolderNameController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.html new file mode 100644 index 0000000000..1a9bd41226 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/foldername/foldername.html @@ -0,0 +1,29 @@ +
    + + + + + + + +
    Please enter My Images in the field
    +
    + + + + + + +
    + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.controller.js new file mode 100644 index 0000000000..0989076e95 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.controller.js @@ -0,0 +1,41 @@ +(function () { + "use strict"; + + function UploadImagesController($scope, editorState, mediaResource) { + + var vm = this; + var element = angular.element($scope.model.currentStep.element); + + vm.error = false; + + vm.initNextStep = initNextStep; + + function initNextStep() { + + vm.error = false; + vm.buttonState = "busy"; + + var currentNode = editorState.getCurrent(); + + // make sure we have uploaded at least one image + mediaResource.getChildren(currentNode.id) + .then(function (data) { + + var children = data; + + if(children.items && children.items.length > 0) { + $scope.model.nextStep(); + } else { + vm.error = true; + } + + vm.buttonState = "init"; + + }); + + } + + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbIntroMediaSection.UploadImagesController", UploadImagesController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.html new file mode 100644 index 0000000000..e7e8750823 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintromediasection/uploadimages/uploadimages.html @@ -0,0 +1,29 @@ +
    + + + + + + + +
    Please upload an image
    +
    + + + + + + +
    + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.controller.js new file mode 100644 index 0000000000..3fdeb8d3c0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.controller.js @@ -0,0 +1,21 @@ +(function () { + "use strict"; + + function TemplatesTreeController($scope) { + + var vm = this; + var eventElement = angular.element($scope.model.currentStep.eventElement); + + function onInit() { + // check if tree is already open - if it is - go to next step + if(eventElement.hasClass("icon-navigation-down")) { + $scope.model.nextStep(); + } + } + + onInit(); + + } + + angular.module("umbraco").controller("Umbraco.Tours.UmbIntroRenderInTemplate.TemplatesTreeController", TemplatesTreeController); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.html b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.html new file mode 100644 index 0000000000..cc8896c964 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/tours/umbintrorenderintemplate/templatetree/templatetree.html @@ -0,0 +1,22 @@ +
    + + + + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-backdrop.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-backdrop.html new file mode 100644 index 0000000000..da1f61ee4a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-backdrop.html @@ -0,0 +1,19 @@ +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 0aa58c8fae..18020ffd59 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -5,9 +5,8 @@
      -
    • - +
    • + {{action.name}} 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 d3eed009c3..faf32173cc 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,114 +1,115 @@
      - - + + - -
    • -
    + + + + + + + +
    + +
    + + +
    + + +
    - + - - - 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 e9d50939f2..9fa8fa6891 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,7 +1,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html new file mode 100644 index 0000000000..b801d63cca --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-tour.html @@ -0,0 +1,86 @@ +
    + +
    +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + +
    + +
    +

    Congratulations!

    +

    You have reached the end of the {{model.name}} tour - way to go!

    +
    + + + + + +
    + +
    + + +
    + + +
    + + +

    Oh, we got lost!

    +
    + +

    We lost the next step {{ model.currentStep.title }} and don't know where to go.

    +

    Please go back and start the tour again.

    +
    + + + +
    +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-content.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-content.html new file mode 100644 index 0000000000..6bf6db6dc9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-content.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-footer.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-footer.html new file mode 100644 index 0000000000..e504fa7634 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-footer.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-header.html new file mode 100644 index 0000000000..900c7f76c9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-header.html @@ -0,0 +1,4 @@ +
    +
    {{ title }}
    +
    {{ description }}
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-view.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-view.html new file mode 100644 index 0000000000..a0c69a70f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer-view.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer.html new file mode 100644 index 0000000000..89c9963810 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbdrawer/umb-drawer.html @@ -0,0 +1,3 @@ +
    +
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-content.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-content.html new file mode 100644 index 0000000000..0010a2bc6d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-content.html @@ -0,0 +1,4 @@ +
    +
    +
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-counter.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-counter.html new file mode 100644 index 0000000000..bd4e0127d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-counter.html @@ -0,0 +1 @@ +
    {{ currentStep }}/{{ totalSteps }}
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-footer.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-footer.html new file mode 100644 index 0000000000..82cc711cfa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-footer.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-header.html new file mode 100644 index 0000000000..e34aa2025c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step-header.html @@ -0,0 +1,4 @@ +
    +
    {{title}}
    +
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html new file mode 100644 index 0000000000..90423ed603 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umbtour/umb-tour-step.html @@ -0,0 +1,10 @@ +
    + + +
    + +
    + +
    + +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html index 46e1adbde2..8117952b59 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html @@ -1,6 +1,8 @@
    - + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-date-time-picker.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-date-time-picker.html index 6b37381113..2d12673a1f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-date-time-picker.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-date-time-picker.html @@ -1,6 +1,12 @@ -
    - - - - +
    + +
    + + + + +
    + +
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 98e2443085..214b276d00 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -1,10 +1,11 @@ -
    +
    -
  • +
  • - +
    @@ -247,7 +256,7 @@
    -
    +
    @@ -255,7 +264,7 @@
    -
    +
    + data-element="overlay-compositions" + ng-if="compositionsDialogModel.show" + model="compositionsDialogModel" + position="right" + view="compositionsDialogModel.view"> + data-element="overlay-property-settings" + ng-if="propertySettingsDialogModel.show" + model="propertySettingsDialogModel" + position="right" + view="propertySettingsDialogModel.view">
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html index 412a57288d..9df33e7e0e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-media-grid.html @@ -1,9 +1,9 @@ -
    -
    +
    +
    - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-progress-circle.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-progress-circle.html new file mode 100644 index 0000000000..3864042e9f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-progress-circle.html @@ -0,0 +1,7 @@ +
    + + + + +
    {{ percentage }}%
    +
    \ No newline at end of file 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 9b60e5e92f..8c5c7fcc4a 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 @@ -47,7 +47,17 @@
    - {{item[column.alias]}} + + +
    + {{item[column.alias]}} +
    + + + + + +
    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 6b05593e11..211060c03f 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 @@ -1,4 +1,4 @@ -
    +
    @@ -23,7 +23,9 @@ -
    -
  • +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index 2ad4534115..c1a17662a2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -1,4 +1,4 @@ -
    +

    - Restore {{currentNode.name}} to under {{target.name}}? + Restore {{currentNode.name}} under {{target.name}}?

    -

    {{error.errorMsg}}

    -

    {{error.data.Message}}

    +
    {{error.errorMsg}}
    +
    {{error.data.Message}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js index d0823d1815..2cb4e7be50 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js @@ -18,10 +18,25 @@ function startUpVideosDashboardController($scope, xmlhelper, $log, $http) { angular.module("umbraco").controller("Umbraco.Dashboard.StartupVideosController", startUpVideosDashboardController); -function startUpDynamicContentController(dashboardResource, assetsService) { +function startUpDynamicContentController($timeout, dashboardResource, assetsService, tourService, eventsService) { var vm = this; + var evts = []; + vm.loading = true; vm.showDefault = false; + + vm.startTour = startTour; + + function onInit() { + // load tours + tourService.getGroupedTours().then(function(groupedTours) { + vm.tours = groupedTours; + }); + } + + function startTour(tour) { + tourService.startTour(tour); + } // default dashboard content vm.defaultDashboard = { @@ -67,6 +82,17 @@ function startUpDynamicContentController(dashboardResource, assetsService) { ] }; + evts.push(eventsService.on("appState.tour.complete", function (name, completedTour) { + $timeout(function(){ + angular.forEach(vm.tours, function (tourGroup) { + angular.forEach(tourGroup, function (tour) { + if(tour.alias === completedTour.alias) { + tour.completed = true; + } + }); + }); + }); + })); //proxy remote css through the local server assetsService.loadCss( dashboardResource.getRemoteDashboardCssUrl("content") ); @@ -90,6 +116,10 @@ function startUpDynamicContentController(dashboardResource, assetsService) { vm.loading = false; vm.showDefault = true; }); + + + onInit(); + } angular.module("umbraco").controller("Umbraco.Dashboard.StartUpDynamicContentController", startUpDynamicContentController); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html index bbf94629a0..0ad779cf3e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/developerdashboardvideos.html @@ -1,5 +1,5 @@

    Hours of Umbraco training videos are only a click away

    -

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

    +

    Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco, then visit umbraco.tv for even more Umbraco videos.

  • - + +
    + {{video.title}} +
    {{video.title}}
    +
    +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemanagement.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemanagement.html index e80e7775da..1c166c54ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemanagement.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/examinemanagement.html @@ -215,7 +215,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html index f23b8f5df9..0ecaeb80df 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html @@ -19,7 +19,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html index 7ef9537080..3b382367c4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/forms/formsdashboardintro.html @@ -2,7 +2,7 @@
- +

Umbraco Forms

diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html index 5550fe8b10..4875e4b1ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/create.html @@ -4,7 +4,7 @@
Create an item under {{currentNode.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.controller.js index bde9cac30d..0cd199ae4d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.controller.js @@ -77,14 +77,14 @@ function includePropsPreValsController($rootScope, $scope, localizationService, // Return a helper with preserved width of cells var fixHelper = function (e, ui) { - var h = ui.clone(); - - h.children().each(function () { - $(this).width($(this).width()); + ui.children().each(function () { + $(this).width($(this).width()); }); - h.css("background-color", "lightgray"); - return h; + var row = ui.clone(); + row.css("background-color", "lightgray"); + + return row; }; $scope.sortableOptions = { @@ -96,6 +96,10 @@ function includePropsPreValsController($rootScope, $scope, localizationService, cursor: 'move', items: '> tr', tolerance: 'pointer', + forcePlaceholderSize: true, + start: function(e, ui){ + ui.placeholder.height(ui.item.height()); + }, update: function (e, ui) { // Get the new and old index for the moved element (using the text as the identifier) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index 5cf7de7de6..2e210e635f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -20,7 +20,7 @@
- +
Set the title of the overlay.
model.subTitlemodel.subtitle String Set the subtitle of the overlay.
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js index 799cc5894c..00e6c6edb4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/list/list.listviewlayout.controller.js @@ -30,15 +30,14 @@ vm.dragLeave = dragLeave; vm.onFilesQueue = onFilesQueue; vm.onUploadComplete = onUploadComplete; + markAsSensitive(); function activate() { - if ($scope.entityType === 'media') { mediaTypeHelper.getAllowedImagetypes(vm.nodeId).then(function (types) { vm.acceptedMediatypes = types; }); } - } function selectAll($event) { @@ -87,6 +86,27 @@ $scope.getContent($scope.contentId); } + function markAsSensitive() { + angular.forEach($scope.options.includeProperties, function (option) { + option.isSensitive = false; + + angular.forEach($scope.items, + function (item) { + + angular.forEach(item.properties, + function (property) { + + if (option.alias === property.alias) { + option.isSensitive = property.isSensitive; + } + + }); + + }); + + }); + } + activate(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js index 53424a7d6e..7a3abdd0e6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js @@ -46,6 +46,7 @@ function MarkdownEditorController($scope, $element, assetsService, dialogService // init the md editor after this digest because the DOM needs to be ready first // so run the init on a timeout $timeout(function () { + $scope.markdownEditorInitComplete = false; var converter2 = new Markdown.Converter(); var editor2 = new Markdown.Editor(converter2, "-" + $scope.model.alias); editor2.run(); @@ -59,7 +60,12 @@ function MarkdownEditorController($scope, $element, assetsService, dialogService editor2.hooks.set("onPreviewRefresh", function () { // We must manually update the model as there is no way to hook into the markdown editor events without exstensive edits to the library. if ($scope.model.value !== $("textarea", $element).val()) { - angularHelper.getCurrentForm($scope).$setDirty(); + if ($scope.markdownEditorInitComplete) { + //only set dirty after init load to avoid "unsaved" dialogue when we don't want it + angularHelper.getCurrentForm($scope).$setDirty(); + } else { + $scope.markdownEditorInitComplete = true; + } $scope.model.value = $("textarea", $element).val(); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js index 8a7b20498d..bf7462942a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.controller.js @@ -1,7 +1,7 @@ //this controller simply tells the dialogs service to open a mediaPicker window //with a specified callback, this callback will receive an object with a selection on it angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerController", - function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout, userService, $location) { + function ($rootScope, $scope, dialogService, entityResource, mediaResource, mediaHelper, $timeout, userService, $location, localizationService) { //check the pre-values for multi-picker var multiPicker = $scope.model.config.multiPicker && $scope.model.config.multiPicker !== '0' ? true : false; @@ -19,6 +19,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.images = []; $scope.ids = []; + $scope.isMultiPicker = multiPicker; + if ($scope.model.value) { var ids = $scope.model.value.split(','); @@ -26,17 +28,47 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl // the mediaResource has server side auth configured for which the user must have // access to the media section, if they don't they'll get auth errors. The entityResource // acts differently in that it allows access if the user has access to any of the apps that - // might require it's use. Therefore we need to use the metatData property to get at the thumbnail + // might require it's use. Therefore we need to use the metaData property to get at the thumbnail // value. - entityResource.getByIds(ids, "Media").then(function (medias) { + entityResource.getByIds(ids, "Media").then(function(medias) { - _.each(medias, function (media, i) { + // The service only returns item results for ids that exist (deleted items are silently ignored). + // This results in the picked items value to be set to contain only ids of picked items that could actually be found. + // Since a referenced item could potentially be restored later on, instead of changing the selected values here based + // on whether the items exist during a save event - we should keep "placeholder" items for picked items that currently + // could not be fetched. This will preserve references and ensure that the state of an item does not differ depending + // on whether it is simply resaved or not. + // This is done by remapping the int/guid ids into a new array of items, where we create "Deleted item" placeholders + // when there is no match for a selected id. This will ensure that the values being set on save, are the same as before. + + medias = _.map(ids, + function(id) { + var found = _.find(medias, + function(m) { + // We could use coercion (two ='s) here .. but not sure if this works equally well in all browsers and + // it's prone to someone "fixing" it at some point without knowing the effects. Rather use toString() + // compares and be completely sure it works. + return m.udi.toString() === id.toString() || m.id.toString() === id.toString(); + }); + if (found) { + return found; + } else { + return { + name: localizationService.dictionary.mediaPicker_deletedItem, + id: $scope.model.config.idType !== "udi" ? id : null, + udi: $scope.model.config.idType === "udi" ? id : null, + icon: "icon-picture", + thumbnail: null, + trashed: true + }; + } + }); - //only show non-trashed items - if (media.parentId >= -1) { - - if (!media.thumbnail) { + _.each(medias, + function(media, i) { + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } @@ -44,12 +76,10 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl if ($scope.model.config.idType === "udi") { $scope.ids.push(media.udi); - } - else { + } else { $scope.ids.push(media.id); } - } - }); + }); $scope.sync(); }); @@ -82,8 +112,8 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl submit: function(model) { _.each(model.selectedImages, function(media, i) { - - if (!media.thumbnail) { + // if there is no thumbnail, try getting one if the media is not a placeholder item + if (!media.thumbnail && media.id && media.metaData) { media.thumbnail = mediaHelper.resolveFileFromEntity(media, true); } @@ -101,17 +131,18 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl $scope.mediaPickerOverlay.show = false; $scope.mediaPickerOverlay = null; - } }; - }; $scope.sortableOptions = { + disabled: !$scope.isMultiPicker, + items: "li:not(.add-wrapper)", + cancel: ".unsortable", update: function(e, ui) { var r = []; - //TODO: Instead of doing this with a half second delay would be better to use a watch like we do in the - // content picker. THen we don't have to worry about setting ids, render models, models, we just set one and let the + // TODO: Instead of doing this with a half second delay would be better to use a watch like we do in the + // content picker. Then we don't have to worry about setting ids, render models, models, we just set one and let the // watch do all the rest. $timeout(function(){ angular.forEach($scope.images, function(value, key) { @@ -142,5 +173,4 @@ angular.module('umbraco').controller("Umbraco.PropertyEditors.MediaPickerControl //update the display val again if it has changed from the server setupViewModel(); }; - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index f3aab992dd..7607a9391d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -1,47 +1,48 @@
-
    -
  • +

    +

    + +
    + +
    -
  • -
- - - - - - + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js index 9b4fb217eb..51c5baf55b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.controller.js @@ -86,6 +86,10 @@ }; $scope.add = function ($event) { + if (!angular.isArray($scope.model.value)) { + $scope.model.value = []; + } + if ($scope.newCaption == "") { $scope.hasError = true; } else { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index d17310d65a..0a6bededae 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -139,6 +139,7 @@ angular.module("umbraco") //wait for queue to end $q.all(await).then(function () { + //create a baseline Config to exten upon var baseLineConfigObj = { mode: "exact", @@ -149,14 +150,16 @@ angular.module("umbraco") extended_valid_elements: extendedValidElements, menubar: false, statusbar: false, + relative_urls: false, height: editorConfig.dimensions.height, width: editorConfig.dimensions.width, maxImageSize: editorConfig.maxImageSize, toolbar: toolbar, content_css: stylesheets, - relative_urls: false, style_formats: styleFormats, - language: language + language: language, + //see http://archive.tinymce.com/wiki.php/Configuration:cache_suffix + cache_suffix: "?umb__rnd=" + Umbraco.Sys.ServerVariables.application.cacheBuster }; if (tinyMceConfig.customConfig) { @@ -356,7 +359,8 @@ angular.module("umbraco") //this is instead of doing a watch on the model.value = faster $scope.model.onValueChanged = function (newVal, oldVal) { //update the display val again if it has changed from the server; - tinyMceEditor.setContent(newVal, { format: 'raw' }); + //uses an empty string in the editor when the value is null + tinyMceEditor.setContent(newVal || "", { format: 'raw' }); //we need to manually fire this event since it is only ever fired based on loading from the DOM, this // is required for our plugins listening to this event to execute tinyMceEditor.fire('LoadContent', null); @@ -374,6 +378,9 @@ angular.module("umbraco") // element might still be there even after the modal has been hidden. $scope.$on('$destroy', function () { unsubscribe(); + if (tinyMceEditor !== undefined && tinyMceEditor != null) { + tinyMceEditor.destroy() + } }); }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 702d19a509..65a62f599c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -76,6 +76,7 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", icon.isCustom = false; break; case "styleselect": + case "fontsizeselect": icon.name = "icon-list"; icon.isCustom = true; break; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/sensitivevalue/sensitivevalue.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/sensitivevalue/sensitivevalue.html new file mode 100644 index 0000000000..6460d882b2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/sensitivevalue/sensitivevalue.html @@ -0,0 +1,5 @@ +
+ + Hide this property value from content editors that don't have access to view sensitive information + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js index 404c311d8b..321ac13555 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js @@ -92,7 +92,6 @@ $scope.model.config.ticksPositions = _.map($scope.model.config.ticksPositions.split(','), function (item) { return parseInt(item.trim()); }); - console.log($scope.model.config.ticksPositions); } if (!$scope.model.config.ticksLabels) { @@ -215,4 +214,4 @@ assetsService.loadCss("lib/slider/bootstrap-slider.css"); assetsService.loadCss("lib/slider/bootstrap-slider-custom.css"); } -angular.module("umbraco").controller("Umbraco.PropertyEditors.SliderController", sliderController); \ No newline at end of file +angular.module("umbraco").controller("Umbraco.PropertyEditors.SliderController", sliderController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index 3bb909c717..734903e46c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -1,37 +1,42 @@ function textboxController($scope) { - // macro parameter editor doesn't contains a config object, - // so we create a new one to hold any properties + // so we create a new one to hold any properties if (!$scope.model.config) { $scope.model.config = {}; } - if (!$scope.model.config.maxChars) { - $scope.model.config.maxChars = false; - } - $scope.model.maxlength = false; if ($scope.model.config && $scope.model.config.maxChars) { $scope.model.maxlength = true; - if($scope.model.value == undefined) { + } + + if (!$scope.model.config.maxChars) { + // 500 is the maximum number that can be stored + // in the database, so set it to the max, even + // if no max is specified in the config + $scope.model.config.maxChars = 500; + } + + if ($scope.model.maxlength) { + if ($scope.model.value === undefined) { $scope.model.count = ($scope.model.config.maxChars * 1); } else { $scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length; } } - $scope.model.change = function() { + $scope.model.change = function () { if ($scope.model.config && $scope.model.config.maxChars) { - if($scope.model.value == undefined) { + if ($scope.model.value === undefined) { $scope.model.count = ($scope.model.config.maxChars * 1); } else { $scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length; } - if($scope.model.count < 0) { + if ($scope.model.count < 0) { $scope.model.value = $scope.model.value.substring(0, ($scope.model.config.maxChars * 1)); $scope.model.count = 0; } } } } -angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController); \ No newline at end of file +angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index d8c51ce9e0..77656ac618 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -4,11 +4,11 @@ val-server="value" ng-required="model.validation.mandatory" ng-trim="false" - ng-keyup="model.change()" /> + ng-keyup="model.change()" /> Required -
- {{model.count}} - characters left -
- \ No newline at end of file +
+ {{model.count}} + characters left +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html b/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html index 4c58356732..9b03935efc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/scripts/edit.html @@ -1,4 +1,4 @@ -
+
@@ -21,6 +21,7 @@
diff --git a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html index 2d59b644d0..279cdc538d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/templates/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/templates/edit.html @@ -1,4 +1,4 @@ -
+
@@ -30,6 +30,7 @@
@@ -113,6 +118,7 @@ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index ec072b9268..0f3519c7ba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, $timeout, $location, $routeParams, formHelper, usersResource, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { + function UserEditController($scope, eventsService, $q, $timeout, $location, $routeParams, formHelper, usersResource, userService, contentEditingHelper, localizationService, notificationsService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper) { var vm = this; @@ -16,7 +16,7 @@ vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; vm.acceptedFileTypes = mediaHelper.formatFileTypes(Umbraco.Sys.ServerVariables.umbracoSettings.imageFileTypes); vm.usernameIsEmail = Umbraco.Sys.ServerVariables.umbracoSettings.usernameIsEmail; - + //create the initial model for change password vm.changePasswordModel = { config: {}, @@ -31,8 +31,10 @@ vm.disableUser = disableUser; vm.enableUser = enableUser; vm.unlockUser = unlockUser; + vm.changeAvatar = changeAvatar; vm.clearAvatar = clearAvatar; vm.save = save; + vm.toggleChangePassword = toggleChangePassword; function init() { @@ -92,7 +94,7 @@ }); } - function getLocalDate(date, format) { + function getLocalDate(date, culture, format) { if(date) { var dateVal; var serverOffset = Umbraco.Sys.ServerVariables.application.serverTimeOffset; @@ -105,7 +107,7 @@ dateVal = moment(date, "YYYY-MM-DD HH:mm:ss"); } - return dateVal.format(format); + return dateVal.locale(culture).format(format); } } @@ -117,38 +119,83 @@ function save() { - vm.page.saveButtonState = "busy"; - vm.user.resetPasswordValue = null; + if (formHelper.submitForm({ scope: $scope, statusMessage: vm.labels.saving })) { - //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here - if(vm.user.changePassword) { - vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser; + //anytime a user is changing another user's password, we are in effect resetting it so we need to set that flag here + if (vm.user.changePassword) { + vm.user.changePassword.reset = !vm.user.changePassword.oldPassword && !vm.user.isCurrentUser; + } + + vm.page.saveButtonState = "busy"; + vm.user.resetPasswordValue = null; + + //save current nav to be restored later so that the tabs dont change + var currentNav = vm.user.navigation; + + usersResource.saveUser(vm.user) + .then(function (saved) { + + //if the user saved, then try to execute all extended save options + extendedSave(saved).then(function(result) { + //if all is good, then reset the form + formHelper.resetForm({ scope: $scope, notifications: saved.notifications }); + }, function(err) { + //otherwise show the notifications for the user being saved + formHelper.showNotifications(saved); + }); + + vm.user = _.omit(saved, "navigation"); + //restore + vm.user.navigation = currentNav; + setUserDisplayState(); + formatDatesToLocal(vm.user); + + vm.changePasswordModel.isChanging = false; + //the user has a password if they are not states: Invited, NoCredentials + vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; + + vm.page.saveButtonState = "success"; + + }, function (err) { + + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: err + }); + //show any notifications + if (err.data) { + formHelper.showNotifications(err.data); + } + vm.page.saveButtonState = "error"; + }); } + } - contentEditingHelper.contentEditorPerformSave({ - statusMessage: vm.labels.saving, - saveMethod: usersResource.saveUser, - scope: $scope, - content: vm.user, - // We do not redirect on failure for users - this is because it is not possible to actually save a user - // when server side validation fails - as opposed to content where we are capable of saving the content - // item if server side validation fails - redirectOnFailure: false, - rebindCallback: function (orignal, saved) { } - }).then(function (saved) { + /** + * Used to emit the save event and await any async operations being performed by editor extensions + * @param {any} savedUser + */ + function extendedSave(savedUser) { - vm.user = saved; - setUserDisplayState(); - formatDatesToLocal(vm.user); + //used to track any promises added by the event handlers to be awaited + var promises = []; + + var args = { + //getPromise: getPromise, + user: savedUser, + //a promise can be added by the event handler if the handler needs an async operation to be awaited + addPromise: function (p) { + promises.push(p); + } + }; - vm.changePasswordModel.isChanging = false; - vm.page.saveButtonState = "success"; - - //the user has a password if they are not states: Invited, NoCredentials - vm.changePasswordModel.config.hasPassword = vm.user.userState !== 3 && vm.user.userState !== 4; - }, function (err) { - vm.page.saveButtonState = "error"; - }); + //emit the event + eventsService.emit("editors.user.editController.save", args); + + //await all promises to complete + var resultPromise = $q.all(promises); + + return resultPromise; } function goToPage(ancestor) { @@ -310,7 +357,7 @@ }); } - $scope.changeAvatar = function (files, event) { + function changeAvatar(files, event) { if (files && files.length > 0) { upload(files[0]); } @@ -393,11 +440,14 @@ } function formatDatesToLocal(user) { - user.formattedLastLogin = getLocalDate(user.lastLoginDate, "MMMM Do YYYY, HH:mm"); - user.formattedLastLockoutDate = getLocalDate(user.lastLockoutDate, "MMMM Do YYYY, HH:mm"); - user.formattedCreateDate = getLocalDate(user.createDate, "MMMM Do YYYY, HH:mm"); - user.formattedUpdateDate = getLocalDate(user.updateDate, "MMMM Do YYYY, HH:mm"); - user.formattedLastPasswordChangeDate = getLocalDate(user.lastPasswordChangeDate, "MMMM Do YYYY, HH:mm"); + // get current backoffice user and format dates + userService.getCurrentUser().then(function (currentUser) { + user.formattedLastLogin = getLocalDate(user.lastLoginDate, currentUser.locale, "LLL"); + user.formattedLastLockoutDate = getLocalDate(user.lastLockoutDate, currentUser.locale, "LLL"); + user.formattedCreateDate = getLocalDate(user.createDate, currentUser.locale, "LLL"); + user.formattedUpdateDate = getLocalDate(user.updateDate, currentUser.locale, "LLL"); + user.formattedLastPasswordChangeDate = getLocalDate(user.lastPasswordChangeDate, currentUser.locale, "LLL"); + }); } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.html b/src/Umbraco.Web.UI.Client/src/views/users/user.html index e043b50d2c..32c1c3d641 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.html @@ -1,404 +1,27 @@
+ + - + - + - - - - +
-
- -
- - - - - - - - - - Required - - - - - - Required - - - - - - Required - - - - - - - - - - - - - - - - Add - - - - - - - - - - - - - - Add - - - - - - - - - - - - - - Add - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- -
- - -
- - - - - - - - - - - - -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - - - - - - - - - - -
-


Password reset to value: {{vm.user.resetPasswordValue}}

-
- -
- - -
-
- Status: -
-
- - {{vm.user.userDisplayState.name}} - -
-
- -
-
- Last login: -
-
- {{ vm.user.formattedLastLogin }} - {{ vm.user.name | umbWordLimit:1 }} has not logged in yet -
-
- -
-
- Failed login attempts: -
-
- {{ vm.user.failedPasswordAttempts }} -
-
- -
-
- Last lockout date: -
-
- - {{ vm.user.name | umbWordLimit:1 }} hasn't been locked out - - {{ vm.user.formattedLastLockoutDate }} -
-
- -
-
- Password is last changed: -
-
- - The password hasn't been changed - - {{ vm.user.formattedLastPasswordChangeDate }} -
-
- -
-
- User is created: -
-
- {{ vm.user.formattedCreateDate }} -
-
- -
-
- User is last updated: -
-
- {{ vm.user.formattedUpdateDate }} -
-
- -
- -
- -
+ +
@@ -408,33 +31,30 @@ - + - + - + @@ -445,25 +65,22 @@ - + - + - +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html index 931ab64775..efb88f6297 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.html @@ -53,6 +53,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html new file mode 100644 index 0000000000..67f204aa2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -0,0 +1,361 @@ +
+ +
+ + + + + + + + + + + Required + + + + + + Required + + + + + + Required + + + + + + + + + + + + + + + + Add + + + + + + + + + + + + + + Add + + + + + + + + + + + + + + Add + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + +
+ + + + + + + + + + + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + + + + + + + + + +
+


Password reset to value: {{model.user.resetPasswordValue}}

+
+ +
+ + +
+
+ Status: +
+
+ + {{model.user.userDisplayState.name}} + +
+
+ +
+
+ Last login: +
+
+ {{ model.user.formattedLastLogin }} + {{ model.user.name | umbWordLimit:1 }} has not logged in yet +
+
+ +
+
+ Failed login attempts: +
+
+ {{ model.user.failedPasswordAttempts }} +
+
+ +
+
+ Last lockout date: +
+
+ + {{ model.user.name | umbWordLimit:1 }} hasn't been locked out + + {{ model.user.formattedLastLockoutDate }} +
+
+ +
+
+ Password is last changed: +
+
+ + The password hasn't been changed + + {{ model.user.formattedLastPasswordChangeDate }} +
+
+ +
+
+ User is created: +
+
+ {{ model.user.formattedCreateDate }} +
+
+ +
+
+ User is last updated: +
+
+ {{ model.user.formattedUpdateDate }} +
+
+ +
+ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js index 96ebf4f07b..e60fd4be74 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/users/users.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UsersController($scope, $timeout, $location, usersResource, userGroupsResource, localizationService, contentEditingHelper, usersHelper, formHelper, notificationsService, dateHelper) { + function UsersController($scope, $timeout, $location, $routeParams, usersResource, userGroupsResource, userService, localizationService, contentEditingHelper, usersHelper, formHelper, notificationsService, dateHelper) { var vm = this; var localizeSaving = localizationService.localize("general_saving"); @@ -20,6 +20,19 @@ { label: "Oldest", key: "CreateDate", direction: "Ascending" }, { label: "Last login", key: "LastLoginDate", direction: "Descending" } ]; + + angular.forEach(vm.userSortData, function (userSortData) { + var key = "user_sort" + userSortData.key + userSortData.direction; + localizationService.localize(key).then(function (value) { + var reg = /^\[[\S\s]*]$/g; + var result = reg.test(value); + if (result === false) { + // Only translate if key exists + userSortData.label = value; + } + }); + }); + vm.userStatesFilter = []; vm.newUser.userGroups = []; vm.usersViewState = 'overview'; @@ -111,6 +124,13 @@ vm.usersOptions.orderBy = "Name"; vm.usersOptions.orderDirection = "Ascending"; + if ($routeParams.create) { + setUsersViewState("createUser"); + } + else if ($routeParams.invite) { + setUsersViewState("inviteUser"); + } + // Get users getUsers(); @@ -155,6 +175,17 @@ if (state === "createUser") { clearAddUserForm(); + + $location.search("create", "true"); + $location.search("invite", null); + } + else if (state === "inviteUser") { + $location.search("create", null); + $location.search("invite", "true"); + } + else if (state === "overview") { + $location.search("create", null); + $location.search("invite", null); } vm.usersViewState = state; @@ -284,10 +315,10 @@ vm.selectedBulkUserGroups = _.clone(firstSelectedUser.userGroups); vm.userGroupPicker = { - title: "Select user groups", + title: localizationService.localize("user_selectUserGroups"), view: "usergrouppicker", selection: vm.selectedBulkUserGroups, - closeButtonLabel: "Cancel", + closeButtonLabel: localizationService.localize("general_cancel"), show: true, submit: function (model) { usersResource.setUserGroupsOnUsers(model.selection, vm.selection).then(function (data) { @@ -320,10 +351,10 @@ function openUserGroupPicker(event) { vm.userGroupPicker = { - title: "Select user groups", + title: localizationService.localize("user_selectUserGroups"), view: "usergrouppicker", selection: vm.newUser.userGroups, - closeButtonLabel: "Cancel", + closeButtonLabel: localizationService.localize("general_cancel"), show: true, submit: function (model) { // apply changes @@ -583,7 +614,10 @@ dateVal = moment(user.lastLoginDate, "YYYY-MM-DD HH:mm:ss"); } - user.formattedLastLogin = dateVal.format("MMMM Do YYYY, HH:mm"); + // get current backoffice user and format date + userService.getCurrentUser().then(function (currentUser) { + user.formattedLastLogin = dateVal.locale(currentUser.locale).format("LLL"); + }); } }); } diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js index 1c7b99cc01..e9e41f9073 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js @@ -1,6 +1,6 @@ describe('edit media controller tests', function () { var scope, controller, routeParams, httpBackend; - routeParams = {id: 1234, create: false}; + routeParams = { id: 1234, create: false }; beforeEach(module('umbraco')); @@ -11,7 +11,7 @@ describe('edit media controller tests', function () { httpBackend = $httpBackend; scope = $rootScope.$new(); - + //have the contentMocks register its expect urls on the httpbackend //see /mocks/content.mocks.js for how its setup mediaMocks.register(); @@ -20,7 +20,7 @@ describe('edit media controller tests', function () { //this controller requires an angular form controller applied to it scope.contentForm = angularHelper.getNullForm("contentForm"); - + controller = $controller('Umbraco.Editors.Media.EditController', { $scope: scope, $routeParams: routeParams @@ -37,8 +37,8 @@ describe('edit media controller tests', function () { })); describe('media edit controller save', function () { - - it('it should have an media object', function() { + + it('it should have an media object', function () { //controller should have a content object expect(scope.content).not.toBeUndefined(); @@ -48,22 +48,29 @@ describe('edit media controller tests', function () { }); it('it should have a tabs collection', function () { - expect(scope.content.tabs.length).toBe(1); + expect(scope.content.tabs.length).toBe(2); }); - it('it should have a properties collection on each tab', function () { - $(scope.content.tabs).each(function(i, tab){ - expect(tab.properties.length).toBeGreaterThan(0); - }); + it('it should have added an info tab', function () { + expect(scope.content.tabs[1].id).toBe(-1); + expect(scope.content.tabs[1].alias).toBe("_umb_infoTab"); + }); + + it('all other tabs than the info tab should have a properties collection', function () { + $(scope.content.tabs).each(function (i, tab) { + if (tab.id !== -1 && tab.alias !== '_umb_infoTab') { + expect(tab.properties.length).toBeGreaterThan(0); + } + }); }); it('it should change updateDate on save', function () { - var currentUpdateDate = scope.content.updateDate; + var currentUpdateDate = scope.content.updateDate; - setTimeout(function(){ - scope.save(scope.content); - expect(scope.content.updateDate).toBeGreaterThan(currentUpdateDate); - }, 1000); + setTimeout(function () { + scope.save(scope.content); + expect(scope.content.updateDate).toBeGreaterThan(currentUpdateDate); + }, 1000); }); }); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js index 265689988d..9a6e884ff6 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/directives/val-email.spec.js @@ -27,7 +27,8 @@ expect(valEmailExpression.EMAIL_REGEXP.test('a@3b.c')).toBe(true); expect(valEmailExpression.EMAIL_REGEXP.test('a@b')).toBe(true); expect(valEmailExpression.EMAIL_REGEXP.test('abc@xyz.financial')).toBe(true); - + expect(valEmailExpression.EMAIL_REGEXP.test('admin@c.pizza')).toBe(true); + expect(valEmailExpression.EMAIL_REGEXP.test('admin+gmail-syntax@c.pizza')).toBe(true); }); }); diff --git a/src/WebPi/TBEX.xml b/src/WebPi/TBEX.xml deleted file mode 100644 index 827bd01574..0000000000 --- a/src/WebPi/TBEX.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - - Start Here - First time here? Complete the installation of your Umbraco Site, select a Starter Kit and Skin to get you started. - /install/default.aspx - http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/umbraco.png - - - Getting Started with Umbraco - So you've finished installing Umbraco in WebMatrix, what next? Learn how to build and configure your new site! - http://our.umbraco.org/wiki/how-tos/getting-started-with-umbraco-what-is-next-after-you-install - http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/umbraco.png - - - Extending Umbraco using WebMatrix - In this 5-minute video tutorial you'll learn how to edit templates, use the Razor syntax to add dynamic functionality and deploy an Umbraco site - all from within WebMatrix. - http://umbraco.com/help-and-support/video-tutorials/getting-started/working-with-webmatrix - http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/umbraco.png - - - Umbraco Forum - Where the friendliest CMS community on the planet helps each other - Search for documentation, get help and guidance from seasoned experts, download and collaborate on plugins and extensions. - http://our.umbraco.org/ - http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/community.png - - - Umbraco Products - We have additional products and services available to help you get the most out of Umbraco. - http://umbraco.com/products - http://umbraco.com/products - http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/products.png - - - Umbraco Community Projects - "The Deli" - Find free and commercial add-ons, extensions, skins, and starter sites. - http://our.umbraco.org/projects - http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/deli.png - - - - - - -/umbraco/ -http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/umbraco.png - - - - -http://our.umbraco.org/ -http://cdn.umbraco.org/cdn/umbraco/images/webmatrix/community.png - - - - - - - install - - - web.config - - - bin - - - umbraco - - - umbraco_client - - - - diff --git a/src/WebPi/installSQL1.sql b/src/WebPi/installSQL1.sql deleted file mode 100644 index 8abdf52c43..0000000000 --- a/src/WebPi/installSQL1.sql +++ /dev/null @@ -1,9 +0,0 @@ -/**********************************************************************/ -/* Install.SQL */ -/* Creates a login and makes the user a member of db roles */ -/* */ -/* Modifications for SQL AZURE - ON MASTER */ -/**********************************************************************/ - - -CREATE LOGIN PlaceHolderForUser WITH PASSWORD = 'PlaceHolderForPassword' \ No newline at end of file diff --git a/src/WebPi/installSQL2.sql b/src/WebPi/installSQL2.sql deleted file mode 100644 index 653d7d4fbf..0000000000 --- a/src/WebPi/installSQL2.sql +++ /dev/null @@ -1,15 +0,0 @@ -/**********************************************************************/ -/* CreateUser.SQL */ -/* Creates a user and makes the user a member of db roles */ -/* This script runs against the User database and requires connection string */ -/* Supports SQL Server and SQL AZURE */ -/**********************************************************************/ - --- Create database user and map to login --- and add user to the datareader, datawriter, ddladmin and securityadmin roles --- - -CREATE USER PlaceHolderForUser FOR LOGIN PlaceHolderForUser; -GO -EXEC sp_addrolemember 'db_owner', PlaceHolderForUser; -GO diff --git a/src/WebPi/manifest.xml b/src/WebPi/manifest.xml deleted file mode 100644 index cfd5740051..0000000000 --- a/src/WebPi/manifest.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/WebPi/parameters.xml b/src/WebPi/parameters.xml deleted file mode 100644 index 350520407d..0000000000 --- a/src/WebPi/parameters.xml +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/WebPi/umbraco/favicon.ico b/src/WebPi/umbraco/favicon.ico deleted file mode 100644 index e3ea01cf10..0000000000 Binary files a/src/WebPi/umbraco/favicon.ico and /dev/null differ diff --git a/tools/contributing/clonefork.png b/tools/contributing/clonefork.png new file mode 100644 index 0000000000..ef4e4f801d Binary files /dev/null and b/tools/contributing/clonefork.png differ diff --git a/tools/contributing/createpullrequest.png b/tools/contributing/createpullrequest.png new file mode 100644 index 0000000000..dd8fdaf478 Binary files /dev/null and b/tools/contributing/createpullrequest.png differ diff --git a/tools/contributing/defaultbranch.png b/tools/contributing/defaultbranch.png new file mode 100644 index 0000000000..0595e0b4ea Binary files /dev/null and b/tools/contributing/defaultbranch.png differ diff --git a/tools/contributing/forkrepository.png b/tools/contributing/forkrepository.png new file mode 100644 index 0000000000..30195ec247 Binary files /dev/null and b/tools/contributing/forkrepository.png differ diff --git a/tools/contributing/gulpbuild.png b/tools/contributing/gulpbuild.png new file mode 100644 index 0000000000..333bfe2349 Binary files /dev/null and b/tools/contributing/gulpbuild.png differ